Reactorのdeferの使い方

343
NO IMAGE

reactor-coreのMonoやFluxを使う上で、よくやってしまうリソース解放に関する誤りを、Mono#deferの使い方とともに説明します。
reactor-coreのその他の使い方については、Reactor 3 Reference Guidを参考にして下さい。

前準備

reactor-coreに従って、reactor-coreを使ったJavaプログラミングが出来る環境を準備します。

問題のあるコード

MonoFluxの処理は、subscribeblock等の終端処理が呼ばれた際に実行されます。このため、終端処理が呼ばれるたびに、doOnTerminate等のハンドラーが実行されます。以下のように、Monoの外側で作成したリソースをdoOnTerminatecloseすると、2回目の終端処理の呼び出しはすでにcloseされているリソースに対して実行されてしまいます。

    private static class _SampleCloseable implements Closeable {

        private boolean _closed;

        @Override
        public synchronized void close() {
            // 2回以上、closeが呼ばれるとエラーになる。
            if (_closed) {
                throw new RuntimeException("Already closed.");
            }
            _closed = true;
        }

    }

    @Test
    public void testInvalidClose() throws Exception {
        var closeable = new _SampleCloseable();
        var mono = Mono.just("good").doOnTerminate(() -> {
            closeable.close();
        });
        // 内部でcloseが呼ばれる
        var value = mono.block();
        Assertions.assertEquals("good", value);
        // 内部でcloseが呼ばれる(2回目なのでエラーになる)
        Assertions.assertThrows(RuntimeException.class, () -> mono.block());
    }

deferを使用した修正

deferを使用することで、Monoの内部でリソースの初期化を行う事が出来ます。

    @Test
    public void testDefer() throws Exception {
        var mono = Mono.defer(() -> {
            // リソースの初期化をdefer内で行います。
            var closeable = new _SampleCloseable();
            return Mono.just("good").doOnTerminate(() -> {
                closeable.close();
            });
        });
        // block実行時に、リソースが初期化されて、閉じられます
        var value = mono.block();
        Assertions.assertEquals("good", value);
        // block実行時に、リソースが初期化されて、閉じられます
        value = mono.block();
        Assertions.assertEquals("good", value);
    }

結論

deferを使うことで、リソースの初期化と解放をMonoやFluxに閉じて行う事が出来ます。
MonoFluxの生成と同じスコープでリソースの初期化をしているケースでは、こちらの問題が発生する可能性を常に考慮する必要があります。