web-dev-qa-db-ja.com

Spring WebFlux:1つの接続のみがサブスクライバーの受信を許可

Spring 5WebfluxとKotlinを使用して簡単なアプリを作成しています。私は次の方法でPUTエンドポイントを実装しようとしています:

_PUT("/confs/{id}", {
    val id = it.pathVariable("id")
    ServerResponse.ok().body(service.save(it.bodyToMono(Item::class.Java)), Item::class.Java)
})
_

保存の秘訣は、アイテムから都市名を読み取り、地理座標を解決し、元のアイテムで上書きしてから、Spring Data MongoReactiveリポジトリを使用してMongoに保存しようとすることです。

_fun save(item: Mono<Item>): Mono<Item> {
    val geo = item.flatMap {
            val city = it.location?.city ?: "Somewhere"
            geoService.resolveGeoFromCity(city)
    }

    val zipped = item.zipWith(geo)
        .map {
            it.t1.location?.geo = it.t2
            it.t1
        }

    return repo.saveAll(zipped)
        .toMono()
}
_

地理座標を解決するためのコードは次のとおりです。

_@Service
class GeoService() {

    val client = WebClient.create("https://maps.googleapis.com/maps/api/geocode/")

    fun resolveGeoFromCity(city: String): Mono<Geo> {
        return client.get()
                .uri("json?address=$city&key=$API_KEY&language=en")
                .exchange()
                .flatMap { it.bodyToMono(String::class.Java) }
                .map { parse(it) }
    }

    private fun parse(response: String): Geo {
        val locationMap = JsonPath.read<Map<String, Double>>(response, "$.results[0].geometry.location")
        return Geo(locationMap["lat"] ?: 0.0, locationMap["lng"] ?: 0.0)
    }

}
_

問題は、PUTリクエストを行うと、次のスタックトレースを取得したことです。 val geo = Mono.just(Geo(0.0, 0.0))を使用して(WebClientを使用せずに)Monoをスタブしようとしましたが、正常に動作します。

機能を犠牲にすることなくそれを修正する方法は?)

_    2018-01-01 01:41:00.595 ERROR 15120 --- [ctor-http-nio-4] .a.w.r.e.DefaultErrorWebExceptionHandler : Failed to handle request [PUT http://localhost:8097/confs/5a49675c910d123b1057207a]

    Java.lang.IllegalStateException: Only one connection receive subscriber allowed.
        at reactor.ipc.netty.channel.FluxReceive.startReceiver(FluxReceive.Java:276) ~[reactor-netty-0.7.2.RELEASE.jar:0.7.2.RELEASE]
        at reactor.ipc.netty.channel.FluxReceive.subscribe(FluxReceive.Java:124) ~[reactor-netty-0.7.2.RELEASE.jar:0.7.2.RELEASE]
        at reactor.core.publisher.FluxMap.subscribe(FluxMap.Java:62) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxOnAssembly.subscribe(FluxOnAssembly.Java:252) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.ipc.netty.ByteBufFlux.subscribe(ByteBufFlux.Java:242) ~[reactor-netty-0.7.2.RELEASE.jar:0.7.2.RELEASE]
        at reactor.core.publisher.FluxPeek.subscribe(FluxPeek.Java:83) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxOnAssembly.subscribe(FluxOnAssembly.Java:252) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.ipc.netty.ByteBufFlux.subscribe(ByteBufFlux.Java:242) ~[reactor-netty-0.7.2.RELEASE.jar:0.7.2.RELEASE]
        at reactor.core.publisher.FluxMap.subscribe(FluxMap.Java:62) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxOnAssembly.subscribe(FluxOnAssembly.Java:252) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxFlatMap.subscribe(FluxFlatMap.Java:97) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxOnAssembly.subscribe(FluxOnAssembly.Java:252) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxDoFinallyFuseable.subscribe(FluxDoFinallyFuseable.Java:48) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxOnAssembly.subscribe(FluxOnAssembly.Java:252) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxMapFuseable.subscribe(FluxMapFuseable.Java:63) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.FluxOnAssembly.subscribe(FluxOnAssembly.Java:252) ~
[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoPeekTerminal.subscribe(MonoPeekTerminal.Java:61) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoPeekFuseable.subscribe(MonoPeekFuseable.Java:74) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.Java:52) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.Java:52) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.Java:52) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.Java:44) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.Java:44) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.Java:44) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.Mono.subscribe(Mono.Java:3008) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.Java:167) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.Java:56) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.Java:44) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:71) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoPeekTerminal.subscribe(MonoPeekTerminal.Java:61) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.Java:76) ~[reactor-core-3.1.2.RELEASE.jar:3.1.2.RELEASE]
        at reactor.ipc.netty.channel.ChannelOperations.applyHandler(ChannelOperations.Java:383) ~[reactor-netty-0.7.2.RELEASE.jar:0.7.2.RELEASE]
        at reactor.ipc.netty.http.server.HttpServerOperations.onHandlerStart(HttpServerOperations.Java:359) ~[reactor-netty-0.7.2.RELEASE.jar:0.7.2.RELEASE]
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.Java:163) ~[netty-common-4.1.17.Final.jar:4.1.17.Final]
        at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.Java) ~[netty-common-4.1.17.Final.jar:4.1.17.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.Java:403) ~[netty-common-4.1.17.Final.jar:4.1.17.Final]
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.Java:463) ~[netty-transport-4.1.17.Final.jar:4.1.17.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.Java:858) ~[netty-common-4.1.17.Final.jar:4.1.17.Final]
        at Java.lang.Thread.run(Thread.Java:745) ~[na:1.8.0_112]
        Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
    Assembly trace from producer [reactor.core.publisher.FluxMap] :
        reactor.core.publisher.Flux.map(Flux.Java:4855)
        reactor.ipc.netty.ByteBufFlux.fromInbound(ByteBufFlux.Java:68)
        reactor.ipc.netty.NettyInbound.receive(NettyInbound.Java:90)
        org.springframework.http.server.reactive.ReactorServerHttpRequest.getBody(ReactorServerHttpRequest.Java:148)
        org.springframework.http.codec.DecoderHttpMessageReader.readMono(DecoderHttpMessageReader.Java:93)
        org.springframework.http.codec.DecoderHttpMessageReader.readMono(DecoderHttpMessageReader.Java:123)
        org.springframework.web.reactive.function.BodyExtractors.lambda$null$0(BodyExtractors.Java:101)
        Java.util.Optional.map(Optional.Java:215)
        org.springframework.web.reactive.function.BodyExtractors.readWithMessageReaders(BodyExtractors.Java:256)
        org.springframework.web.reactive.function.BodyExtractors.lambda$toMono$2(BodyExtractors.Java:96)
        org.springframework.web.reactive.function.server.DefaultServerRequest.body(DefaultServerRequest.Java:126)
        org.springframework.web.reactive.function.server.DefaultServerRequest.body(DefaultServerRequest.Java:120)
        org.springframework.web.reactive.function.server.DefaultServerRequest.bodyToMono(DefaultServerRequest.Java:145)
        com.example.confs.web.ConferenceRouter$routes$1$2.invoke(ConferenceRouter.kt:31)
        com.example.confs.web.ConferenceRouter$routes$1$2.invoke(ConferenceRouter.kt:16)
        org.springframework.web.reactive.function.server.RouterFunctionDsl$PUT$1.handle(RouterFunctionDsl.kt:200)
        org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.Java:61)
        org.springframework.web.reactive.DispatcherHandler.invokeHandler(DispatcherHandler.Java:168)
        org.springframework.web.reactive.DispatcherHandler.lambda$handle$1(DispatcherHandler.Java:160)
        reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.Java:118)
        reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.Java:67)
        reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.Java:76)
        reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.Java:271)
        reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.Java:803)
        reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.Java:115)
        reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.Java:67)
        reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.Java:67)
        reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.Java:67)
        reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.Java:67)
        reactor.core.publisher.Operators$ScalarSubscription.request(Operators.Java:1649)
        reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.Java:1463)
        reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.Java:1337)
        reactor.core.publisher.MonoJust.subscribe(MonoJust.Java:54)
        reactor.core.publisher.MonoDefer.subscribe(MonoDefer.Java:52)
        reactor.core.publisher.Mono.subscribe(Mono.Java:3008)
        reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.Java:75)
        reactor.core.publisher.Operators.complete(Operators.Java:125)
        reactor.core.publisher.MonoEmpty.subscribe(MonoEmpty.Java:45)
        reactor.core.publisher.MonoSwitchIfEmpty.subscribe(MonoSwitchIfEmpty.Java:44)
        reactor.core.publisher.MonoSwitchIfEmpty.subscribe(MonoSwitchIfEmpty.Java:44)
        reactor.core.publisher.MonoSwitchIfEmpty.subscribe(MonoSwitchIfEmpty.Java:44)
        reactor.core.publisher.MonoSwitchIfEmpty.subscribe(MonoSwitchIfEmpty.Java:44)
        reactor.core.publisher.MonoMapFuseable.subscribe(MonoMapFuseable.Java:59)
        reactor.core.publisher.Mono.subscribe(Mono.Java:3008)
        reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.Java:418)
        reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.Java:210)
        reactor.core.publisher.FluxIterable.subscribe(FluxIterable.Java:128)
        reactor.core.publisher.FluxIterable.subscribe(FluxIterable.Java:61)
        reactor.core.publisher.FluxConcatMap.subscribe(FluxConcatMap.Java:121)
        reactor.core.publisher.MonoNext.subscribe(MonoNext.Java:40)
        reactor.core.publisher.MonoSwitchIfEmpty.subscribe(MonoSwitchIfEmpty.Java:44)
        reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.Java:60)
        reactor.core.publisher.MonoFlatMap.subscribe(MonoFlatMap.Java:60)
        reactor.core.publisher.MonoDefer.subscribe(MonoDefer.Java:52)
        reactor.core.publisher.MonoPeekTerminal.subscribe(MonoPeekTerminal.Java:61)
        reactor.core.publisher.MonoPeekFuseable.subscribe(MonoPeekFuseable.Java:74)
        reactor.core.publisher.MonoDefer.subscribe(MonoDefer.Java:52)
        reactor.core.publisher.MonoDefer.subscribe(MonoDefer.Java:52)
        reactor.core.publisher.MonoDefer.subscribe(MonoDefer.Java:52)
        reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.Java:44)
        reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.Java:44)
        reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.Java:44)
        reactor.core.publisher.Mono.subscribe(Mono.Java:3008)
        reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.Java:167)
        reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.Java:56)
        reactor.core.publisher.MonoOnErrorResume.subscribe(MonoOnErrorResume.Java:44)
        reactor.core.publisher.MonoPeekTerminal.subscribe(MonoPeekTerminal.Java:61)
        reactor.ipc.netty.channel.ChannelOperations.applyHandler(ChannelOperations.Java:383)
        reactor.ipc.netty.http.server.HttpServerOperations.onHandlerStart(HttpServerOperations.Java:359)
        io.netty.util.concurrent.AbstractEventExecutor.safeExecute$$$capture(AbstractEventExecutor.Java:163)
        io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.Java:403)
        io.netty.channel.nio.NioEventLoop.run(NioEventLoop.Java:463)
        io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.Java:858)
    Error has been observed by the following operator(s):
        |_  Flux.map(ByteBufFlux.Java:68)
        |_  Flux.doOnNext(ByteBufFlux.Java:230)
        |_  Flux.map(ReactorServerHttpRequest.Java:148)
        |_  Flux.flatMap(AbstractJackson2Decoder.Java:95)
        |_  Flux.doFinally(AbstractJackson2Decoder.Java:95)
        |_  Flux.map(AbstractJackson2Decoder.Java:117)
        |_  Flux.singleOrEmpty(AbstractJackson2Decoder.Java:87)
        |_  Operators.error(FluxReceive.Java:276)
        |_  Mono.onErrorMap(DefaultServerRequest.Java:146)
        |_  Mono.map(ConferenceService.kt:27)
        |_  Mono.map(ConferenceService.kt:32)
        |_  Mono.zipWith(ConferenceService.kt:47)
        |_  Mono.map(ConferenceService.kt:48)
        |_  Flux.flatMap(SimpleReactiveMongoRepository.Java:318)
        |_  MonoExtensionsKt.toMono(ConferenceService.kt:55)
        |_  Mono.map(ConferenceService.kt:56)
        |_  Flux.map(AbstractJackson2Encoder.Java:99)
        |_  Mono.flatMap(DispatcherHandler.Java:177)
        |_  Mono.onErrorResume(DispatcherHandler.Java:177)
        |_  Mono.flatMap(DispatcherHandler.Java:161)
        |_  Mono.defer(DefaultWebFilterChain.Java:71)
        |_  Mono.doOnSuccess(MetricsWebFilter.Java:59)
        |_  Mono.doOnError(MetricsWebFilter.Java:60)
        |_  Mono.compose(MetricsWebFilter.Java:54)
        |_  Mono.defer(DefaultWebFilterChain.Java:71)
        |_  Mono.defer(DefaultWebFilterChain.Java:71)
_
5

WebClient.exchange()結果ストリームはユニキャストです

ここでの問題は、実際にはWebClientが接続ごとに1つのサブスクライバーしか許可しないことです。同じexchanged接続を2回サブスクライブしようとすると、_Java.lang.IllegalStateException: Only one connection receive subscriber allowed._が取得されます

同じ接続を2回再利用しようとした場所がわかりませんが、次の演算子の組み合わせを使用することで、その問題を解決できると思います。

_class GeoService() {
   val client = WebClient.create("https://maps.googleapis.com/maps/api/geocode/")

   fun resolveGeoFromCity(city: String): Mono<Geo> {
       return client.get()
            .uri("json?address=$city&key=$API_KEY&language=en")
            .exchange()
            .flatMap { it.bodyToMono(String::class.Java) }
            .map { parse(it) }
            .share();
   }
   ...
}
_

この例では、少なくとも1つのSubscriberがサブスクライブされる限り、フローは元のソースをマルチキャスト(共有)するように構成されています。すべてのサブスクライバーが同じ日付を受け取る必要がある場合は、_.share_を_.cache_演算子に置き換えることができます。

また、上記の手法に代わる方法があります。上記の演算子をプロセッサに置き換えて、同じ共有の可能性を得ることができます。

_class GeoService() {

   val client = WebClient.create("https://maps.googleapis.com/maps/api/geocode/")

   fun resolveGeoFromCity(city: String): Mono<Geo> {
       return client.get()
            .uri("json?address=$city&key=$API_KEY&language=en")
            .exchange()
            .flatMap { it.bodyToMono(String::class.Java) }
            .map { parse(it) }
            .subscribeWith(DirectProcessor.create());
   }
   ...
}
_

その場合、subscribeWithを呼び出した直後にソースのデータの消費をサブスクライブして実行するため、その場合、データの一部などが失われる可能性があります。

なぜMono.just(..)を使用すると、すべてが正常に機能するのですか?

まず第一に、_.just_はコールド演算子であり、任意の時点で同じデータを受信する可能な限り多くのサブスクライバーを許可します。そのため、接続から同じデータのチャンクを2回消費しようとしても、例外は発生しませんでした。

10
Oleh Dokuka

同様の問題がありました。修正は、この依存関係を指定することでした:

org.springframework:spring-webflux:5.1.4.RELEASE

スプリングブートを使用したので、以前のバージョンをデプロイします。残念ながら、この問題へのリンクは現在存在していません。

これで、私のgradleは次のようになります。

compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
compile('org.springframework.boot:spring-boot-starter-webflux')
// Next 2 dependencies are temporally here until the one above does not resolves next to at least 5.1.4 - where 
// webflux issue is resolved:
// https://github.com/rstoyanchev/spr-issue-migration-test-2/issues/17323
compile('org.springframework:spring-webflux:5.1.4.RELEASE')
compile('org.springframework:spring-web:5.1.4.RELEASE')
1

私はここで非常によく似た例をしました:

このルーターは名前から地理位置を取得し、別のサービスを使用して日の出と日の入りの時刻を抽出します。

私はMonoの and メソッドを使用しました。

internal fun buildResponse(address: Mono<String>) =
        address.transform(geoLocationService::fromAddress).and(this::sunriseSunset, ::LocationResponse)

internal fun sunriseSunset(geographicCoordinates: GeographicCoordinates) =
        geographicCoordinates.toMono().transform(sunriseSunsetService::fromGeographicCoordinates)

これの詳細

0
Juan Medina