From 7a9046b909f0a317c851b3d851d228d7efaf8ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 18 Feb 2022 09:57:43 +0100 Subject: [PATCH 001/312] Merge #2928 into 3.5.0 Keep spotless ratchetFrom at origin/main. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2130489396..85905f77e0 100644 --- a/build.gradle +++ b/build.gradle @@ -118,7 +118,7 @@ spotless { enforceCheck false } else { - String spotlessBranch = "origin/3.4.x" + String spotlessBranch = "origin/main" println "[Spotless] Local run detected, ratchet from $spotlessBranch" ratchetFrom spotlessBranch } From 6de1cd7562289aa5d412d3b372f869ba63d184c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 18 Feb 2022 12:00:26 +0100 Subject: [PATCH 002/312] Start producing 3.5 snapshots with 3.5.0-SNAPSHOT (#2929) --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 18fe035a0d..d074d28df0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=3.4.16-SNAPSHOT -bomVersion=2020.0.16 \ No newline at end of file +version=3.5.0-SNAPSHOT +bomVersion=2022.0.0-SNAPSHOT \ No newline at end of file From 747aa1abe551961ba413ccb37b08afaac4fad572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 22 Feb 2022 11:12:09 +0100 Subject: [PATCH 003/312] Improve readability of preliminary how-to-fix hint in CI The message is intended as a hint on how to fix failing steps above it. This commit makes it more explicit, both in text and by the choice of color coding. Previously, the separate hints had some colored background, green and orange respectively. This made it look like the japicmp step was the one detected as failed, which was counterintuitive. The message now explicitly tells the reader to look at the previous steps in the job, and the colors have been changed to: - drive focus to the whole message by having a header with orange bg - highlight the individual calls to action with white font on black bg - highlight the gradle command with black font on white bg --- .github/workflows/ci.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0251ed29d..ef8c049b43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,13 +26,15 @@ jobs: arguments: japicmp - name: how to fix if: failure() - # the foreground (38;5) color code 022 is green + # the foreground (38;5) color code 208 is orange. we also have bold, white bg (38;5;0;48;5;255m), white fg on black bg... run: | - echo "Tips for fixing errors in preliminary job:" - echo -e "\033[38;5;255;48;5;022m ✅ License headers of touched java files can be automatically fixed by running:\033[0m\n\033[38;5;208;48;5;022m./gradlew spotlessApply\033[0m" + echo -e "\n\033[38;5;0;48;5;208m \u001b[1m How to deal with errors in preliminary job: \u001b[0m\033[0m" + echo "(Have a look at the steps above to see what failed exactly)" + echo -e "\n - \u001b[1mSpotless (license headers)\u001b[0m failures on touched java files \033[38;5;255;48;5;0m\u001b[1mcan be automatically fixed by running\u001b[0m:" + echo -e " \033[38;5;0;48;5;255m ./gradlew spotlessApply \033[0m" + echo -e "\n - \u001b[1mAPI Compatibility\u001b[0m failures should be considered carefully and \033[38;5;255;48;5;0m\u001b[1mdiscussed with maintainers in the PR\u001b[0m" + echo " If there are failures, the detail should be available in the logs of the api compatibility step above" echo "" - echo -e "\033[38;5;255;48;5;202m ⚠️ JApiCmp failures should be considered carefully and discussed with maintainers in the PR\033[0m" - echo "If there are failures, the detail should be available in the logs of the api compatibility step above" exit -1 core-fast: name: core fast tests From b637e504130596c17c004462c67babfe2acd6b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 8 Mar 2022 16:06:17 +0100 Subject: [PATCH 004/312] Remove Kotlin extensions (#2949) This commit removes Kotlin extensions from core repo, since they have been replaced with those in reactor-kotlin-extensions repository. This also removes dependencies to Kotlin stdlib, language, plugins... as well as kotlin entries in the generated OSGI manifests. Finally, a japicmp exception is added for the removed classes. Fixes #2912. --- .github/renovate.json | 5 - build.gradle | 24 -- gradle/libs.versions.toml | 3 - reactor-core/build.gradle | 14 +- .../reactor/core/publisher/FluxExtensions.kt | 259 ------------------ .../reactor/core/publisher/MonoExtensions.kt | 161 ----------- .../reactor/core/publisher/MonoFunctions.kt | 73 ----- .../reactor/util/function/TupleExtensions.kt | 98 ------- .../core/publisher/FluxExtensionsTests.kt | 237 ---------------- .../core/publisher/MonoExtensionsTests.kt | 251 ----------------- .../core/publisher/MonoFunctionsTests.kt | 48 ---- .../util/function/TupleExtensionsTests.kt | 97 ------- reactor-test/build.gradle | 9 +- .../reactor/test/StepVerifierExtensions.kt | 137 --------- .../test/StepVerifierExtensionsTests.kt | 57 ---- 15 files changed, 10 insertions(+), 1463 deletions(-) delete mode 100644 reactor-core/src/main/kotlin/reactor/core/publisher/FluxExtensions.kt delete mode 100644 reactor-core/src/main/kotlin/reactor/core/publisher/MonoExtensions.kt delete mode 100644 reactor-core/src/main/kotlin/reactor/core/publisher/MonoFunctions.kt delete mode 100644 reactor-core/src/main/kotlin/reactor/util/function/TupleExtensions.kt delete mode 100644 reactor-core/src/test/kotlin/reactor/core/publisher/FluxExtensionsTests.kt delete mode 100644 reactor-core/src/test/kotlin/reactor/core/publisher/MonoExtensionsTests.kt delete mode 100644 reactor-core/src/test/kotlin/reactor/core/publisher/MonoFunctionsTests.kt delete mode 100644 reactor-core/src/test/kotlin/reactor/util/function/TupleExtensionsTests.kt delete mode 100644 reactor-test/src/main/kotlin/reactor/test/StepVerifierExtensions.kt delete mode 100644 reactor-test/src/test/kotlin/reactor/test/StepVerifierExtensionsTests.kt diff --git a/.github/renovate.json b/.github/renovate.json index f01e7bca21..bbb32c1ccb 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -61,11 +61,6 @@ "groupName": "Micrometer 1.3.0", "groupSlug": "micrometer", "allowedVersions": "=1.3.0" - }, - { - "matchPackagePrefixes": ["org.jetbrains.kotlin"], - "groupName": "Kotlin", - "allowedVersions": "<1.6.0" } ] } diff --git a/build.gradle b/build.gradle index 85905f77e0..686f36837b 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,6 @@ buildscript { plugins { - alias(libs.plugins.kotlin) alias(libs.plugins.artifactory) alias(libs.plugins.shadow) alias(libs.plugins.asciidoctor.convert) apply false @@ -215,27 +214,4 @@ configure(subprojects) { p -> // these apply once the above configure is done, but before project-specific build.gradle have applied apply plugin: "io.reactor.gradle.java-conventions" apply from: "${rootDir}/gradle/javadoc.gradle" - - // these apply AFTER project-specific build.gradle have applied - afterEvaluate { - if (p.plugins.hasPlugin("kotlin")) { - println "Applying Kotlin conventions to ${p.name}" - compileKotlin { - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs = ["-Xjsr305=strict"] - languageVersion = "1.3" //TODO kotlin languageVersion 1.3 is now deprecated - apiVersion = "1.3" - } - } - compileTestKotlin { - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs = ["-Xjsr305=strict"] - languageVersion = "1.3" //TODO kotlin languageVersion 1.3 is now deprecated - apiVersion = "1.3" - } - } - } - } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 66d00b172e..932661c782 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,6 @@ asciidoctor = "3.3.2" bytebuddy = "1.12.8" jmh = "1.34" junit = "5.8.2" -kotlin = "1.5.32" reactiveStreams = "1.0.3" [libraries] @@ -26,7 +25,6 @@ jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jsr166backport = "io.projectreactor:jsr166:1.0.0.RELEASE" jsr305 = "com.google.code.findbugs:jsr305:3.0.1" junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } logback = "ch.qos.logback:logback-classic:1.2.11" micrometer = "io.micrometer:micrometer-core:1.3.0" mockito = "org.mockito:mockito-core:4.3.1" @@ -46,7 +44,6 @@ bnd = { id = "biz.aQute.bnd.builder", version = "6.2.0" } download = { id = "de.undercouch.download", version = "5.0.1" } japicmp = { id = "me.champeau.gradle.japicmp", version = "0.3.1" } jcstress = { id = "io.github.reyerizo.gradle.jcstress", version = "0.8.13" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } nohttp = { id = "io.spring.nohttp", version = "0.0.10" } shadow = { id = "com.github.johnrengelman.shadow", version = "7.1.2" } spotless = { id = "com.diffplug.spotless", version = "6.3.0" } diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index ecd2808ff1..148c36a5d7 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -19,8 +19,8 @@ import me.champeau.gradle.japicmp.JapicmpTask apply plugin: 'idea' // needed to avoid IDEA seeing the jmh folder as source apply plugin: 'biz.aQute.bnd.builder' apply plugin: 'org.unbroken-dome.test-sets' -apply plugin: 'kotlin' apply plugin: 'jcstress' +apply plugin: 'java-library' ext { bndOptions = [ @@ -33,7 +33,6 @@ ext { "!javax.annotation", "!javax.annotation.meta", 'org.slf4j;resolution:=optional;version="[1.5.4,2)"', - "kotlin.*;resolution:=optional", "reactor.blockhound.*;resolution:=optional", "io.micrometer.*;resolution:=optional", "*" @@ -86,10 +85,6 @@ dependencies { // Optional Metrics compileOnly libs.micrometer - // Not putting kotlin-stdlib as implementation to not force it as a transitive lib - compileOnly libs.kotlin.stdlib - testImplementation libs.kotlin.stdlib - // Optional BlockHound support compileOnly libs.blockhound // Also make BlockHound visible in the CP of dedicated testset @@ -161,7 +156,12 @@ task japicmp(type: JapicmpTask) { // TODO after a .0 release, bump the gradle.properties baseline // TODO after a .0 release, remove the reactor-core exclusions below if any - classExcludes = [ ] + classExcludes = [ + "reactor.core.publisher.FluxExtensionsKt", + "reactor.core.publisher.MonoExtensionsKt", + "reactor.core.publisher.MonoWhenFunctionsKt", + "reactor.util.function.TupleExtensionsKt" + ] methodExcludes = [ 'reactor.core.publisher.Sinks$EmitFailureHandler#busyLooping(java.time.Duration)' ] } diff --git a/reactor-core/src/main/kotlin/reactor/core/publisher/FluxExtensions.kt b/reactor-core/src/main/kotlin/reactor/core/publisher/FluxExtensions.kt deleted file mode 100644 index 66629f99ad..0000000000 --- a/reactor-core/src/main/kotlin/reactor/core/publisher/FluxExtensions.kt +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:Suppress("DEPRECATION") - -package reactor.core.publisher - -import org.reactivestreams.Publisher -import java.util.stream.Stream -import kotlin.reflect.KClass - - -/** - * Extension to convert any [Publisher] of [T] to a [Flux]. - * - * Note this extension doesn't make much sense on a [Flux] but it won't be converted so it - * doesn't hurt. - * - * @author Simon Baslé - * @since 3.1.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun Publisher.toFlux(): Flux = Flux.from(this) - -/** - * Extension for transforming an [Iterator] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun Iterator.toFlux(): Flux = toIterable().toFlux() - -/** - * Extension for transforming an [Iterable] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun Iterable.toFlux(): Flux = Flux.fromIterable(this) - -/** - * Extension for transforming a [Sequence] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun Sequence.toFlux(): Flux = Flux.fromIterable(object : Iterable { - override fun iterator(): Iterator = this@toFlux.iterator() -}) - -/** - * Extension for transforming a [Stream] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun Stream.toFlux(): Flux = Flux.fromStream(this) - -/** - * Extension for transforming a [BooleanArray] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun BooleanArray.toFlux(): Flux = this.toList().toFlux() - -/** - * Extension for transforming a [ByteArray] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun ByteArray.toFlux(): Flux = this.toList().toFlux() - -/** - * Extension for transforming a [ShortArray] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun ShortArray.toFlux(): Flux = this.toList().toFlux() - -/** - * Extension for transforming a [IntArray] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun IntArray.toFlux(): Flux = this.toList().toFlux() - -/** - * Extension for transforming a [LongArray] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun LongArray.toFlux(): Flux = this.toList().toFlux() - -/** - * Extension for transforming a [FloatArray] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun FloatArray.toFlux(): Flux = this.toList().toFlux() - -/** - * Extension for transforming a [DoubleArray] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun DoubleArray.toFlux(): Flux = this.toList().toFlux() - -/** - * Extension for transforming an [Array] to a [Flux]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun Array.toFlux(): Flux = Flux.fromArray(this) - -private fun Iterator.toIterable() = object : Iterable { - override fun iterator(): Iterator = this@toIterable -} - -/** - * Extension for transforming an exception to a [Flux] that completes with the specified error. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toFlux()", "reactor.kotlin.core.publisher.toFlux")) -fun Throwable.toFlux(): Flux = Flux.error(this) - -/** - * Extension for [Flux.cast] providing a `cast()` variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("cast()", "reactor.kotlin.core.publisher.cast")) -inline fun Flux<*>.cast(): Flux = cast(T::class.java) - - -/** - * Extension for [Flux.doOnError] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("doOnError(exceptionType, onError)", "reactor.kotlin.core.publisher.doOnError")) -fun Flux.doOnError(exceptionType: KClass, onError: (E) -> Unit): Flux = - doOnError(exceptionType.java) { onError(it) } - -/** - * Extension for [Flux.onErrorMap] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("onErrorMap(exceptionType, mapper)", "reactor.kotlin.core.publisher.onErrorMap")) -fun Flux.onErrorMap(exceptionType: KClass, mapper: (E) -> Throwable): Flux = - onErrorMap(exceptionType.java) { mapper(it) } - -/** - * Extension for [Flux.ofType] providing a `ofType()` variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("ofType()", "reactor.kotlin.core.publisher.ofType")) -inline fun Flux<*>.ofType(): Flux = ofType(T::class.java) - -/** - * Extension for [Flux.onErrorResume] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("onErrorResume(exceptionType, fallback)", "reactor.kotlin.core.publisher.onErrorResume")) -fun Flux.onErrorResume(exceptionType: KClass, fallback: (E) -> Publisher): Flux = - onErrorResume(exceptionType.java) { fallback(it) } - -/** - * Extension for [Flux.onErrorReturn] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("onErrorReturn(exceptionType, value)", "reactor.kotlin.core.publisher.onErrorReturn")) -fun Flux.onErrorReturn(exceptionType: KClass, value: T): Flux = - onErrorReturn(exceptionType.java, value) - -/** - * Extension for flattening [Flux] of [Iterable] - * - * @author Igor Perikov - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("split()", "reactor.kotlin.core.publisher.split")) -fun Flux>.split(): Flux = this.flatMapIterable { it } - -/** - * Extension for [Flux.switchIfEmpty] accepting a function providing a Publisher. This allows having a deferred execution with - * the [switchIfEmpty] operator - * - * @author Kevin Davin - * @since 3.2 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("switchIfEmpty(s)", "reactor.kotlin.core.publisher.switchIfEmpty")) -fun Flux.switchIfEmpty(s: () -> Publisher): Flux = this.switchIfEmpty(Flux.defer { s() }) diff --git a/reactor-core/src/main/kotlin/reactor/core/publisher/MonoExtensions.kt b/reactor-core/src/main/kotlin/reactor/core/publisher/MonoExtensions.kt deleted file mode 100644 index 071aea8eb2..0000000000 --- a/reactor-core/src/main/kotlin/reactor/core/publisher/MonoExtensions.kt +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher - -import org.reactivestreams.Publisher -import java.util.concurrent.Callable -import java.util.concurrent.CompletableFuture -import java.util.function.Supplier -import kotlin.reflect.KClass - -/** - * Extension to convert any [Publisher] of [T] to a [Mono] that only emits its first - * element. - * - * Note this extension doesn't make much sense on a [Mono] but it won't be converted so it - * doesn't hurt. - * - * @author Simon Baslé - * @since 3.1.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toMono()", "reactor.kotlin.core.publisher.toMono")) -fun Publisher.toMono(): Mono = Mono.from(this) - -/** - * Extension to convert any [Supplier] of [T] to a [Mono] that emits supplied element. - * - * @author Sergio Dos Santos - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toMono()", "reactor.kotlin.core.publisher.toMono")) -fun (() -> T?).toMono(): Mono = Mono.fromSupplier(this) - -/** - * Extension for transforming an object to a [Mono]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toMono()", "reactor.kotlin.core.publisher.toMono")) -fun T.toMono(): Mono = Mono.just(this) - -/** - * Extension for transforming an [CompletableFuture] to a [Mono]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toMono()", "reactor.kotlin.core.publisher.toMono")) -fun CompletableFuture.toMono(): Mono = Mono.fromFuture(this) - -/** - * Extension for transforming an [Callable] to a [Mono]. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toMono()", "reactor.kotlin.core.publisher.toMono")) -fun Callable.toMono(): Mono = Mono.fromCallable(this::call) - -/** - * Extension for transforming an exception to a [Mono] that completes with the specified error. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("toMono()", "reactor.kotlin.core.publisher.toMono")) -fun Throwable.toMono(): Mono = Mono.error(this) - -/** - * Extension for [Mono.cast] providing a `cast()` variant. - * - * @author Sebastien - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("cast()", "reactor.kotlin.core.publisher.cast")) -inline fun Mono<*>.cast(): Mono = cast(T::class.java) - -/** - * Extension for [Mono.doOnError] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("doOnError(exceptionType, onError)", "reactor.kotlin.core.publisher.doOnError")) -fun Mono.doOnError(exceptionType: KClass, onError: (E) -> Unit): Mono = - doOnError(exceptionType.java) { onError(it) } - -/** - * Extension for [Mono.onErrorMap] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("onErrorMap(exceptionType, mapper)", "reactor.kotlin.core.publisher.onErrorMap")) -fun Mono.onErrorMap(exceptionType: KClass, mapper: (E) -> Throwable): Mono = - onErrorMap(exceptionType.java) { mapper(it) } - -/** - * Extension for [Mono.ofType] providing a `ofType()` variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("ofType()", "reactor.kotlin.core.publisher.ofType")) -inline fun Mono<*>.ofType(): Mono = ofType(T::class.java) - -/** - * Extension for [Mono.onErrorResume] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("onErrorResume(exceptionType, fallback)", "reactor.kotlin.core.publisher.onErrorResume")) -fun Mono.onErrorResume(exceptionType: KClass, fallback: (E) -> Mono): Mono = - onErrorResume(exceptionType.java) { fallback(it) } - -/** - * Extension for [Mono.onErrorReturn] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("onErrorReturn(exceptionType, value)", "reactor.kotlin.core.publisher.onErrorReturn")) -fun Mono.onErrorReturn(exceptionType: KClass, value: T): Mono = - onErrorReturn(exceptionType.java, value) - -/** - * Extension for [Mono.switchIfEmpty] accepting a function providing a Mono. This allows having a deferred execution with - * the [switchIfEmpty] operator - * - * @author Kevin Davin - * @since 3.2 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("switchIfEmpty(s)", "reactor.kotlin.core.publisher.switchIfEmpty")) -fun Mono.switchIfEmpty(s: () -> Mono): Mono = this.switchIfEmpty(Mono.defer { s() }) diff --git a/reactor-core/src/main/kotlin/reactor/core/publisher/MonoFunctions.kt b/reactor-core/src/main/kotlin/reactor/core/publisher/MonoFunctions.kt deleted file mode 100644 index 694777a046..0000000000 --- a/reactor-core/src/main/kotlin/reactor/core/publisher/MonoFunctions.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:JvmName("MonoWhenFunctionsKt") // TODO Remove in next major version -package reactor.core.publisher - -import org.reactivestreams.Publisher - -/** - * Aggregates this [Iterable] of void [Publisher]s into a new [Mono]. - * An alias for a corresponding [Mono.when] to avoid use of `when`, which is a keyword in Kotlin. - * - * TODO Move to MonoExtensions.kt in next major version - * - * @author DoHyung Kim - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("whenComplete()", "reactor.kotlin.core.publisher.whenComplete")) -fun Iterable>.whenComplete(): Mono = Mono.`when`(this) - -/** - * Merges this [Iterable] of [Mono]s into a new [Mono] by combining them - * with [combinator]. - * - * TODO Move to MonoExtensions.kt in next major version - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("zip(combinator)", "reactor.kotlin.core.publisher.zip")) -@Suppress("UNCHECKED_CAST") -inline fun Iterable>.zip(crossinline combinator: (List) -> R): Mono = - Mono.zip(this) { combinator(it.asList() as List) } - -/** - * Aggregates the given void [Publisher]s into a new void [Mono]. - * An alias for a corresponding [Mono.when] to avoid use of `when`, which is a keyword in Kotlin. - * - * @author DoHyung Kim - * @author Sebastien Deleuze - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("whenComplete(*sources)", "reactor.kotlin.core.publisher.whenComplete")) -fun whenComplete(vararg sources: Publisher<*>): Mono = MonoBridges.`when`(sources) - -/** - * Aggregates the given [Mono]s into a new [Mono]. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("zip(*monos, combinator)", "reactor.kotlin.core.publisher.zip")) -@Suppress("UNCHECKED_CAST") -fun zip(vararg monos: Mono<*>, combinator: (Array<*>) -> R): Mono = - MonoBridges.zip(combinator, monos) \ No newline at end of file diff --git a/reactor-core/src/main/kotlin/reactor/util/function/TupleExtensions.kt b/reactor-core/src/main/kotlin/reactor/util/function/TupleExtensions.kt deleted file mode 100644 index f56fa58725..0000000000 --- a/reactor-core/src/main/kotlin/reactor/util/function/TupleExtensions.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.util.function - - -/** - * Extension for [Tuple2] to work with destructuring declarations. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions and import reactor.kotlin.core.util.function.component1", - ReplaceWith("component1()", "reactor.kotlin.core.util.function.component1")) -operator fun Tuple2.component1(): T = t1 - -/** - * Extension for [Tuple2] to work with destructuring declarations. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions and import reactor.kotlin.core.util.function.component2", - ReplaceWith("component2()", "reactor.kotlin.core.util.function.component2")) -operator fun Tuple2<*, T>.component2(): T = t2 - -/** - * Extension for [Tuple3] to work with destructuring declarations. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions and import reactor.kotlin.core.util.function.component3", - ReplaceWith("component3()", "reactor.kotlin.core.util.function.component3")) -operator fun Tuple3<*, *, T>.component3(): T = t3 - -/** - * Extension for [Tuple4] to work with destructuring declarations. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions and import reactor.kotlin.core.util.function.component4", - ReplaceWith("component4()", "reactor.kotlin.core.util.function.component4")) -operator fun Tuple4<*, *, *, T>.component4(): T = t4 - -/** - * Extension for [Tuple5] to work with destructuring declarations. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions and import reactor.kotlin.core.util.function.component5", - ReplaceWith("component5()", "reactor.kotlin.core.util.function.component5")) -operator fun Tuple5<*, *, *, *, T>.component5(): T = t5 - -/** - * Extension for [Tuple6] to work with destructuring declarations. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions and import reactor.kotlin.core.util.function.component6", - ReplaceWith("component6()", "reactor.kotlin.core.util.function.component6")) -operator fun Tuple6<*, *, *, *, *, T>.component6(): T = t6 - -/** - * Extension for [Tuple7] to work with destructuring declarations. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions and import reactor.kotlin.core.util.function.component7", - ReplaceWith("component7()", "reactor.kotlin.core.util.function.component7")) -operator fun Tuple7<*, *, *, *, *, *, T>.component7(): T = t7 - -/** - * Extension for [Tuple8] to work with destructuring declarations. - * - * @author DoHyung Kim - * @since 3.1 - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions and import reactor.kotlin.core.util.function.component8", - ReplaceWith("component8()", "reactor.kotlin.core.util.function.component8")) -operator fun Tuple8<*, *, *, *, *, *, *, T>.component8(): T = t8 diff --git a/reactor-core/src/test/kotlin/reactor/core/publisher/FluxExtensionsTests.kt b/reactor-core/src/test/kotlin/reactor/core/publisher/FluxExtensionsTests.kt deleted file mode 100644 index 90480300ea..0000000000 --- a/reactor-core/src/test/kotlin/reactor/core/publisher/FluxExtensionsTests.kt +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import org.reactivestreams.Publisher -import reactor.test.StepVerifier -import reactor.test.test -import reactor.test.verifyError -import java.io.IOException - -@Suppress("deprecation") -class FluxExtensionsTests { - - @Test - fun `Iterator to Flux`() { - StepVerifier - .create(listOf("foo", "bar", "baz").listIterator().toFlux()) - .expectNext("foo", "bar", "baz") - .verifyComplete() - } - - @Test - fun `Iterable to Flux`() { - StepVerifier - .create(listOf("foo", "bar", "baz").asIterable().toFlux()) - .expectNext("foo", "bar", "baz") - .verifyComplete() - } - - @Test - fun `Sequence to Flux`() { - StepVerifier - .create(listOf("foo", "bar", "baz").asSequence().toFlux()) - .expectNext("foo", "bar", "baz") - .verifyComplete() - } - - @Test - fun `Stream to Flux`() { - StepVerifier - .create(listOf("foo", "bar", "baz").stream().toFlux()) - .expectNext("foo", "bar", "baz") - .verifyComplete() - } - - @Test - fun `ByteArray to Flux`() { - StepVerifier - .create(byteArrayOf(Byte.MAX_VALUE, Byte.MIN_VALUE, Byte.MAX_VALUE).toFlux()) - .expectNext(Byte.MAX_VALUE, Byte.MIN_VALUE, Byte.MAX_VALUE) - .verifyComplete() - } - - @Test - fun `ShortArray to Flux`() { - StepVerifier - .create(shortArrayOf(1, 2, 3).toFlux()) - .expectNext(1, 2, 3) - .verifyComplete() - } - - @Test - fun `IntArray to Flux`() { - StepVerifier - .create(intArrayOf(1, 2, 3).toFlux()) - .expectNext(1, 2, 3) - .verifyComplete() - } - - @Test - fun `LongArray to Flux`() { - StepVerifier - .create(longArrayOf(1, 2, 3).toFlux()) - .expectNext(1, 2, 3) - .verifyComplete() - } - - @Test - fun `FloatArray to Flux`() { - StepVerifier - .create(floatArrayOf(1.0F, 2.0F, 3.0F).toFlux()) - .expectNext(1.0F, 2.0F, 3.0F) - .verifyComplete() - } - - @Test - fun `DoubleArray to Flux`() { - StepVerifier - .create(doubleArrayOf(1.0, 2.0, 3.0).toFlux()) - .expectNext(1.0, 2.0, 3.0) - .verifyComplete() - } - - @Test - fun `BooleanArray to Flux`() { - StepVerifier - .create(booleanArrayOf(true, false, true).toFlux()) - .expectNext(true, false, true) - .verifyComplete() - } - - - @Test - fun `Throwable to Flux`() { - StepVerifier - .create(IllegalStateException().toFlux()) - .verifyError(IllegalStateException::class) - } - - @Test - fun `cast() with generic parameter`() { - val fluxOfAny: Flux = Flux.just("foo") - StepVerifier - .create(fluxOfAny.cast()) - .expectNext("foo").verifyComplete() - } - - @Test - fun doOnError() { - val fluxOnError: Flux = IllegalStateException().toFlux() - var invoked = false - fluxOnError.doOnError(IllegalStateException::class) { - invoked = true - }.subscribe() - Assertions.assertTrue(invoked) - } - - @Test - fun onErrorMap() { - StepVerifier - .create(IOException() - .toFlux() - .onErrorMap(IOException::class, ::IllegalStateException)) - .verifyError() - } - - @Test - fun `ofType() with generic parameter`() { - StepVerifier - .create(arrayOf("foo", 1).toFlux().ofType()) - .expectNext("foo").verifyComplete() - } - - @Test - fun onErrorResume() { - val flux = IOException().toFlux().onErrorResume(IOException::class) { "foo".toMono() } - StepVerifier - .create(flux) - .expectNext("foo") - .verifyComplete() - } - - @Test - fun onErrorReturn() { - StepVerifier - .create(IOException() - .toFlux() - .onErrorReturn(IOException::class, "foo")) - .expectNext("foo") - .verifyComplete() - } - - @Test - fun publisherToFlux() { - //fake naive publisher - val p: Publisher = Publisher { - it.onSubscribe(Operators.emptySubscription()) - it.onNext("a") - it.onNext("b") - it.onComplete() - } - - val f = p.toFlux() - - f.test() - .expectNext("a", "b") - .verifyComplete() - - assertThat(f).isNotSameAs(p) - } - - @Test - fun fluxToFlux() { - val f = Flux.range(1, 2) - - assertThat(f.toFlux()).isSameAs(f) - } - - @Test - fun monoToFlux() { - val m = Mono.just(2) - val f = m.toFlux() - - assertThat(f).isNotSameAs(m) - f.test() - .expectNext(2) - .verifyComplete() - } - - @Test - fun splitFlux() { - val f = listOf(listOf(1, 2), listOf(3, 4)).toFlux() - StepVerifier - .create(f.split()) - .expectNext(1, 2, 3, 4) - .verifyComplete() - } - - @Test - fun `switchIfEmpty with defer execution`() { - val flux: Flux = listOf(1, 2, 3) - .toFlux() - .switchIfEmpty { throw RuntimeException("error which should not happen due to defered execution") } - - StepVerifier - .create(flux) - .expectNext(1, 2, 3) - .verifyComplete() - } -} diff --git a/reactor-core/src/test/kotlin/reactor/core/publisher/MonoExtensionsTests.kt b/reactor-core/src/test/kotlin/reactor/core/publisher/MonoExtensionsTests.kt deleted file mode 100644 index 3cbc6e922e..0000000000 --- a/reactor-core/src/test/kotlin/reactor/core/publisher/MonoExtensionsTests.kt +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import org.reactivestreams.Publisher -import reactor.test.StepVerifier -import reactor.test.publisher.TestPublisher -import reactor.test.test -import reactor.test.verifyError -import java.io.IOException -import java.util.concurrent.Callable -import java.util.concurrent.CompletableFuture - -@Suppress("deprecation") -class MonoExtensionsTests { - - @Test - fun anyToMono() { - StepVerifier - .create("foo".toMono()) - .expectNext("foo") - .verifyComplete() - } - - @Test - fun supplierToMono() { - val supplier: () -> String = { "a" } - - val m = supplier.toMono() - - m.test() - .expectNext("a") - .verifyComplete() - } - - @Test - fun publisherToMono() { - //fake naive publisher - val p: Publisher = Publisher { - it.onSubscribe(Operators.emptySubscription()) - it.onNext("a") - it.onNext("b") - it.onComplete() - } - - val m = p.toMono() - - assertThat(m).isNotSameAs(p) - m.test() - .expectNext("a") - .verifyComplete() - } - @Test - fun fluxToMono() { - val f = Flux.range(1, 2) - val m = f.toMono() - - assertThat(m).isNotSameAs(f) - m.test() - .expectNext(1) - .verifyComplete() - } - - @Test - fun monoToMono() { - val m = Mono.just(2) - assertThat(m.toMono()).isSameAs(m) - } - - @Test - fun completableFutureToMono() { - val future = CompletableFuture() - - val verifier = StepVerifier.create(future.toMono()) - .expectNext("foo") - .expectComplete() - future.complete("foo") - verifier.verify() - } - - @Test - fun nullableCompletableFutureToMonoWithMap() { - val future = CompletableFuture() - - val verifier = StepVerifier.create(future.toMono().map { it.toUpperCase() }) - .expectComplete() - future.complete(null) - verifier.verify() - } - - @Test - fun callableToMono() { - val callable = Callable { "foo" } - val verifier = StepVerifier.create(callable.toMono()) - .expectNext("foo") - .expectComplete() - verifier.verify() - } - - @Test - fun nullableCallableToMonoWithMap() { - val callable = Callable { "foo" } - val verifier = StepVerifier.create(callable.toMono().map { it.toUpperCase() }) - .expectNext("FOO") - .expectComplete() - verifier.verify() - } - - @Test - fun nullableCallableToEmptyMonoWitMap() { - val callable = Callable { null } - val verifier = StepVerifier.create(callable.toMono().map { it.toUpperCase() }) - .expectComplete() - verifier.verify() - } - - @Test - fun nullableLambdaToEmptyMono() { - val callable = { null } - val verifier = StepVerifier.create(callable.toMono()) - .expectComplete() - verifier.verify() - } - - @Test - fun nullableLambdaToEmptyMonoWithMap() { - @Suppress("USELESS_CAST") - val callable = { null as String? } - val verifier = StepVerifier.create(callable.toMono().map { it.toUpperCase() }) - .expectComplete() - verifier.verify() - } - - @Test - fun throwableToMono() { - StepVerifier.create(IllegalStateException() - .toMono()) - .verifyError(IllegalStateException::class) - } - - @Test - fun `cast() with generic parameter`() { - val monoOfAny: Mono = Mono.just("foo") - StepVerifier - .create(monoOfAny.cast()) - .expectNext("foo") - .verifyComplete() - } - - @Test - fun doOnError() { - val monoOnError: Mono = IllegalStateException().toMono() - var invoked = false - monoOnError.doOnError(IllegalStateException::class) { - invoked = true - }.subscribe() - Assertions.assertTrue(invoked) - } - - @Test - fun onErrorMap() { - StepVerifier.create(IOException() - .toMono() - .onErrorMap(IOException::class, ::IllegalStateException)) - .verifyError() - } - - @Test - fun `ofType() with generic parameter`() { - StepVerifier.create("foo".toMono().ofType()).expectNext("foo").verifyComplete() - StepVerifier.create("foo".toMono().ofType()).verifyComplete() - } - - @Test - fun onErrorResume() { - val mono = IOException().toMono().onErrorResume(IOException::class) { "foo".toMono() } - StepVerifier.create(mono) - .expectNext("foo") - .verifyComplete() - } - - @Test - fun onErrorReturn() { - StepVerifier.create(IOException() - .toMono() - .onErrorReturn(IOException::class, "foo")) - .expectNext("foo") - .verifyComplete() - } - - @Test - fun `switchIfEmpty with defer execution`() { - val mono: Mono = "foo" - .toMono() - .switchIfEmpty { throw RuntimeException("error which should not happen due to defered execution") } - - StepVerifier - .create(mono) - .expectNext("foo") - .verifyComplete() - } - - @Test - fun `whenComplete on an Iterable of void Publishers`() { - val publishers = Array(3) { TestPublisher.create() } - publishers.forEach { it.complete() } - StepVerifier.create(publishers.asIterable().whenComplete()) - .verifyComplete() - } - - @Test - fun `whenComplete on an Iterable of String Publishers`() { - val publishers = Array(3) { TestPublisher.create() } - publishers.forEach { it.complete() } - StepVerifier.create(publishers.asIterable().whenComplete()) - .verifyComplete() - } - - @Test - fun `zip with an Iterable of Mono + and a combinator`() { - StepVerifier.create(listOf("foo1".toMono(), "foo2".toMono(), "foo3".toMono()) - .zip { it.reduce { acc, s -> acc + s }}) - .expectNext("foo1foo2foo3") - .verifyComplete() - } - - @Test - fun `zip on an Iterable of Monos with combinator`() { - StepVerifier.create(listOf("foo1", "foo2", "foo3").map { it.toMono() }.zip { it.joinToString() }) - .expectNext("foo1, foo2, foo3") - .verifyComplete() - } - -} diff --git a/reactor-core/src/test/kotlin/reactor/core/publisher/MonoFunctionsTests.kt b/reactor-core/src/test/kotlin/reactor/core/publisher/MonoFunctionsTests.kt deleted file mode 100644 index 6537ee418d..0000000000 --- a/reactor-core/src/test/kotlin/reactor/core/publisher/MonoFunctionsTests.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher - -import org.junit.jupiter.api.Test -import reactor.test.StepVerifier -import reactor.test.publisher.TestPublisher - -@Suppress("deprecation") -class MonoFunctionsTests { - - @Test - fun `whenComplete with void Publishers`() { - val publishers = Array(3) { TestPublisher.create() } - publishers.forEach { it.complete() } - StepVerifier.create(whenComplete(*publishers)) - .verifyComplete() - } - - @Test - fun `whenComplete with two Monos`() { - StepVerifier.create(whenComplete("foo1".toMono(), "foo2".toMono())) - .verifyComplete() - } - - @Test - fun `zip with Monos and combinator`() { - val mono = zip("foo1".toMono(), "foo2".toMono(), "foo3".toMono()) { it.joinToString() } - StepVerifier.create(mono) - .expectNext("foo1, foo2, foo3") - .verifyComplete() - } - -} diff --git a/reactor-core/src/test/kotlin/reactor/util/function/TupleExtensionsTests.kt b/reactor-core/src/test/kotlin/reactor/util/function/TupleExtensionsTests.kt deleted file mode 100644 index 6350178e8f..0000000000 --- a/reactor-core/src/test/kotlin/reactor/util/function/TupleExtensionsTests.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.util.function - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -object O1; object O2; object O3; object O4 -object O5; object O6; object O7; object O8 - -@Suppress("deprecation") -class TupleDestructuringTests { - - @Test - fun destructureTuple2() { - val (t1, t2) = Tuples.of(O1, O2) - assertEquals(t1, O1) - assertEquals(t2, O2) - } - - @Test - fun destructureTuple3() { - val (t1, t2, t3) = Tuples.of(O1, O2, O3) - assertEquals(t1, O1) - assertEquals(t2, O2) - assertEquals(t3, O3) - } - - @Test - fun destructureTuple4() { - val (t1, t2, t3, t4) = Tuples.of(O1, O2, O3, O4) - assertEquals(t1, O1) - assertEquals(t2, O2) - assertEquals(t3, O3) - assertEquals(t4, O4) - } - - @Test - fun destructureTuple5() { - val (t1, t2, t3, t4, t5) = Tuples.of(O1, O2, O3, O4, O5) - assertEquals(t1, O1) - assertEquals(t2, O2) - assertEquals(t3, O3) - assertEquals(t4, O4) - assertEquals(t5, O5) - } - - @Test - fun destructureTuple6() { - val (t1, t2, t3, t4, t5, t6) = Tuples.of(O1, O2, O3, O4, O5, O6) - assertEquals(t1, O1) - assertEquals(t2, O2) - assertEquals(t3, O3) - assertEquals(t4, O4) - assertEquals(t5, O5) - assertEquals(t6, O6) - } - - @Test - fun destructureTuple7() { - val (t1, t2, t3, t4, t5, t6, t7) = Tuples.of(O1, O2, O3, O4, O5, O6, O7) - assertEquals(t1, O1) - assertEquals(t2, O2) - assertEquals(t3, O3) - assertEquals(t4, O4) - assertEquals(t5, O5) - assertEquals(t6, O6) - assertEquals(t7, O7) - } - - @Test - fun destructureTuple8() { - val (t1, t2, t3, t4, t5, t6, t7, t8) = Tuples.of(O1, O2, O3, O4, O5, O6, O7, O8) - assertEquals(t1, O1) - assertEquals(t2, O2) - assertEquals(t3, O3) - assertEquals(t4, O4) - assertEquals(t5, O5) - assertEquals(t6, O6) - assertEquals(t7, O7) - assertEquals(t8, O8) - } -} diff --git a/reactor-test/build.gradle b/reactor-test/build.gradle index 3c8e06d94a..6cc1f31f59 100644 --- a/reactor-test/build.gradle +++ b/reactor-test/build.gradle @@ -16,7 +16,7 @@ import me.champeau.gradle.japicmp.JapicmpTask apply plugin: 'biz.aQute.bnd.builder' -apply plugin: 'kotlin' +apply plugin: 'java-library' description = 'Reactor Test support' @@ -28,7 +28,7 @@ ext { ].join(","), "Import-Package": [ "!javax.annotation", - "kotlin.*;resolution:=optional,*" + "*" ].join(","), "Bundle-Name" : "reactor-test", "Bundle-SymbolicName" : "io.projectreactor.reactor-test", @@ -40,10 +40,6 @@ dependencies { api project(":reactor-core") compileOnly libs.jsr305 - // Not putting kotlin-stdlib as implementation to not force it as a transitive lib - compileOnly libs.kotlin.stdlib - testImplementation libs.kotlin.stdlib - testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" @@ -87,6 +83,7 @@ task japicmp(type: JapicmpTask) { includeSynthetic = true // TODO after a .0 release, remove the reactor-test exclusions below if any + classExcludes = [ "reactor.test.StepVerifierExtensionsKt" ] methodExcludes = [ ] } diff --git a/reactor-test/src/main/kotlin/reactor/test/StepVerifierExtensions.kt b/reactor-test/src/main/kotlin/reactor/test/StepVerifierExtensions.kt deleted file mode 100644 index 5ca48c2eee..0000000000 --- a/reactor-test/src/main/kotlin/reactor/test/StepVerifierExtensions.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.test - -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.test.StepVerifier.Assertions -import reactor.test.StepVerifier.LastStep -import java.time.Duration -import kotlin.reflect.KClass - - -/** - * Extension for [StepVerifier.LastStep.expectError] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("expectError(kClass)", "reactor.kotlin.test.expectError")) -fun LastStep.expectError(kClass: KClass): StepVerifier = expectError(kClass.java) - -/** - * Extension for [StepVerifier.LastStep.expectError] providing a `expectError()` variant. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("expectError()", "reactor.kotlin.test.expectError")) -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun LastStep.expectError(): StepVerifier = expectError(T::class.java) - -/** - * Extension for [StepVerifier.LastStep.verifyError] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("verifyError(kClass)", "reactor.kotlin.test.verifyError")) -fun LastStep.verifyError(kClass: KClass): Duration = verifyError(kClass.java) - -/** - * Extension for [StepVerifier.LastStep.verifyError] providing a `verifyError()` variant. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("verifyError()", "reactor.kotlin.test.verifyError")) -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun LastStep.verifyError(): Duration = verifyError(T::class.java) - -/** - * Extension for [StepVerifier.Assertions.hasDroppedErrorOfType] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("hasDroppedErrorOfType(kClass)", "reactor.kotlin.test.hasDroppedErrorOfType")) -fun Assertions.hasDroppedErrorOfType(kClass: KClass): Assertions = hasDroppedErrorOfType(kClass.java) - -/** - * Extension for [StepVerifier.Assertions.hasDroppedErrorOfType] providing a `hasDroppedErrorOfType()` variant. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("hasDroppedErrorOfType()", "reactor.kotlin.test.hasDroppedErrorOfType")) -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun Assertions.hasDroppedErrorOfType(): Assertions = hasDroppedErrorOfType(T::class.java) - -/** - * Extension for [StepVerifier.Assertions.hasOperatorErrorOfType] providing a [KClass] based variant. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("hasOperatorErrorOfType(kClass)", "reactor.kotlin.test.hasOperatorErrorOfType")) -fun Assertions.hasOperatorErrorOfType(kClass: KClass): Assertions = hasOperatorErrorOfType(kClass.java) - -/** - * Extension for [StepVerifier.Assertions.hasOperatorErrorOfType] providing a `hasOperatorErrorOfType()` variant. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("hasDroppedErrorOfType()", "reactor.kotlin.test.hasDroppedErrorOfType")) -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun Assertions.hasOperatorErrorOfType(): Assertions = hasOperatorErrorOfType(T::class.java) - -/** - * Extension for testing [Flux] with [StepVerifier] API. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("test()", "reactor.kotlin.test.test")) -fun Flux.test(): StepVerifier.FirstStep = StepVerifier.create(this) - -/** - * Extension for testing [Flux] with [StepVerifier] API. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("test(n)", "reactor.kotlin.test.test")) -fun Flux.test(n: Long): StepVerifier.FirstStep = StepVerifier.create(this, n) - -/** - * Extension for testing [Mono] with [StepVerifier] API. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("test()", "reactor.kotlin.test.test")) -fun Mono.test(): StepVerifier.FirstStep = StepVerifier.create(this) - -/** - * Extension for testing [Mono] with [StepVerifier] API. - * - * @author Sebastien Deleuze - */ -@Deprecated("To be removed in 3.3.0.RELEASE, replaced by module reactor-kotlin-extensions", - ReplaceWith("test(n)", "reactor.kotlin.test.test")) -fun Mono.test(n: Long): StepVerifier.FirstStep = StepVerifier.create(this, n) diff --git a/reactor-test/src/test/kotlin/reactor/test/StepVerifierExtensionsTests.kt b/reactor-test/src/test/kotlin/reactor/test/StepVerifierExtensionsTests.kt deleted file mode 100644 index 8e90db9e3e..0000000000 --- a/reactor-test/src/test/kotlin/reactor/test/StepVerifierExtensionsTests.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.test - -import org.junit.jupiter.api.Test -import reactor.core.publisher.toMono - -@Suppress("deprecation") -class StepVerifierExtensionsTests { - - @Test - fun `expectError() with KClass parameter`() { - IllegalStateException() - .toMono() - .test() - .verifyError(IllegalStateException::class) - } - - @Test - fun `expectError() with generic parameter`() { - IllegalStateException() - .toMono() - .test() - .verifyError() - } - - @Test - fun `verifyError() with KClass parameter`() { - IllegalStateException() - .toMono() - .test() - .verifyError(IllegalStateException::class) - } - - @Test - fun `verifyError() with generic parameter`() { - IllegalStateException() - .toMono() - .test() - .verifyError() - } - -} From dbeb0ab0c817a33f04c8714a1080418ce26a3479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 9 Mar 2022 20:31:14 +0100 Subject: [PATCH 005/312] Remove deprecated Mono.doOn/AfterSuccessOrError (#2954) This commit removes the doOnSuccessOrError and the doAfterSuccessOrError operators from Mono. Fixes #2478. --- reactor-core/build.gradle | 6 +- .../java/reactor/core/publisher/Mono.java | 52 +-- .../marbles/doAfterSuccessOrError.svg | 244 ----------- .../doc-files/marbles/doOnSuccessOrError.svg | 131 ------ .../core/publisher/MonoPeekAfterTest.java | 407 +----------------- .../reactor/core/publisher/MonoPeekTest.java | 28 +- .../core/publisher/NextProcessorTest.java | 32 +- 7 files changed, 9 insertions(+), 891 deletions(-) delete mode 100644 reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/doAfterSuccessOrError.svg delete mode 100644 reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/doOnSuccessOrError.svg diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index b14e7c9462..78f68e9636 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -162,7 +162,11 @@ task japicmp(type: JapicmpTask) { "reactor.core.publisher.MonoWhenFunctionsKt", "reactor.util.function.TupleExtensionsKt" ] - methodExcludes = [ 'reactor.core.publisher.Sinks$EmitFailureHandler#busyLooping(java.time.Duration)' ] + methodExcludes = [ + 'reactor.core.publisher.Mono#doAfterSuccessOrError(java.util.function.BiConsumer)', + 'reactor.core.publisher.Mono#doOnSuccessOrError(java.util.function.BiConsumer)', + 'reactor.core.publisher.Sinks$EmitFailureHandler#busyLooping(java.time.Duration)', + ] } gradle.taskGraph.afterTask { task, state -> diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 3916ed6a51..061dd3fc74 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -2471,30 +2471,6 @@ public final Mono dematerialize() { return onAssembly(new MonoDematerialize<>(thiz)); } - /** - * Add behavior triggered after the {@link Mono} terminates, either by completing downstream successfully or with an error. - * The arguments will be null depending on success, success with data and error: - *
    - *
  • null, null : completed without data
  • - *
  • T, null : completed with data
  • - *
  • null, Throwable : failed with/without data
  • - *
- * - *

- * - *

- * The relevant signal is propagated downstream, then the {@link BiConsumer} is executed. - * - * @param afterSuccessOrError the callback to call after {@link Subscriber#onNext}, {@link Subscriber#onComplete} without preceding {@link Subscriber#onNext} or {@link Subscriber#onError} - * - * @return a new {@link Mono} - * @deprecated prefer using {@link #doAfterTerminate(Runnable)} or {@link #doFinally(Consumer)}. will be removed in 3.5.0 - */ - @Deprecated - public final Mono doAfterSuccessOrError(BiConsumer afterSuccessOrError) { - return doOnTerminalSignal(this, null, null, afterSuccessOrError); - } - /** * Add behavior (side-effect) triggered after the {@link Mono} terminates, either by * completing downstream successfully or with an error. @@ -2811,32 +2787,6 @@ public final Mono doOnSubscribe(Consumer onSubscribe) { return doOnSignal(this, onSubscribe, null, null, null); } - /** - * Add behavior triggered when the {@link Mono} terminates, either by emitting a value, - * completing empty or failing with an error. - * The value passed to the {@link Consumer} reflects the type of completion: - *

    - *
  • null, null : completing without data. handler is executed right before onComplete is propagated downstream
  • - *
  • T, null : completing with data. handler is executed right before onNext is propagated downstream
  • - *
  • null, Throwable : failing. handler is executed right before onError is propagated downstream
  • - *
- * - *

- * - *

- * The {@link BiConsumer} is executed before propagating either onNext, onComplete or onError downstream. - * - * @param onSuccessOrError the callback to call {@link Subscriber#onNext}, {@link Subscriber#onComplete} without preceding {@link Subscriber#onNext} or {@link Subscriber#onError} - * - * @return a new {@link Mono} - * @deprecated prefer using {@link #doOnNext(Consumer)}, {@link #doOnError(Consumer)}, {@link #doOnTerminate(Runnable)} or {@link #doOnSuccess(Consumer)}. will be removed in 3.5.0 - */ - @Deprecated - public final Mono doOnSuccessOrError(BiConsumer onSuccessOrError) { - Objects.requireNonNull(onSuccessOrError, "onSuccessOrError"); - return doOnTerminalSignal(this, v -> onSuccessOrError.accept(v, null), e -> onSuccessOrError.accept(null, e), null); - } - /** * Add behavior triggered when the {@link Mono} terminates, either by completing with a value, * completing empty or failing with an error. Unlike in {@link Flux#doOnTerminate(Runnable)}, diff --git a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/doAfterSuccessOrError.svg b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/doAfterSuccessOrError.svg deleted file mode 100644 index c9b0835445..0000000000 --- a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/doAfterSuccessOrError.svg +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - doAfterSuccessOrError( - - - ) - - - - - - - - - , - - - ( - - - ) - - - - - - - - - - - - - - - - - - - - - - doAfterSuccessOrError( - - - ) - - - - - - , - - - ( - - - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/doOnSuccessOrError.svg b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/doOnSuccessOrError.svg deleted file mode 100644 index d4a13a7499..0000000000 --- a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/doOnSuccessOrError.svg +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - doOnSuccessOrError(( - - - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - , - - - - - - - ) - - - ... - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoPeekAfterTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoPeekAfterTest.java index 8ba9dc80ca..4d02a3e329 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoPeekAfterTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoPeekAfterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -137,230 +137,6 @@ public void onSuccessFusionConditional() { assertThat(invoked.intValue()).isEqualTo(1); } - @Test - public void onSuccessOrErrorNormal() { - LongAdder invoked = new LongAdder(); - AtomicBoolean completedEmpty = new AtomicBoolean(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .hide() - .doOnSuccessOrError((v, t) -> { - if (v == null && t == null) completedEmpty.set(true); - if (t != null) error.set(t); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion(Fuseable.ANY, Fuseable.NONE) - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(completedEmpty.get()).as("unexpected empty completion").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(error).as("unexpected error").hasValue(null); - } - - @Test - public void onSuccessOrErrorNormalConditional() { - LongAdder invoked = new LongAdder(); - AtomicBoolean completedEmpty = new AtomicBoolean(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .hide() - .filter(v -> true) - .doOnSuccessOrError((v, t) -> { - if (v == null && t == null) completedEmpty.set(true); - if (t != null) error.set(t); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion(Fuseable.ANY, Fuseable.NONE) - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(completedEmpty.get()).as("unexpected empty completion").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(error).as("unexpected error").hasValue(null); - } - - @Test - public void onSuccessOrErrorFusion() { - LongAdder invoked = new LongAdder(); - AtomicBoolean completedEmpty = new AtomicBoolean(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .doOnSuccessOrError((v, t) -> { - if (v == null && t == null) completedEmpty.set(true); - if (t != null) error.set(t); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion() - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(completedEmpty.get()).as("unexpected empty completion").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(error).as("unexpected error").hasValue(null); - } - - @Test - public void onSuccessOrErrorFusionConditional() { - LongAdder invoked = new LongAdder(); - AtomicBoolean completedEmpty = new AtomicBoolean(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .filter(v -> true) - .doOnSuccessOrError((v, t) -> { - if (v == null && t == null) completedEmpty.set(true); - if (t != null) error.set(t); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion() - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(completedEmpty.get()).as("unexpected empty completion").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(error).as("unexpected error").hasValue(null); - } - - @Test - public void onAfterSuccessOrErrorNormal() { - LongAdder invoked = new LongAdder(); - AtomicBoolean completedEmpty = new AtomicBoolean(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doAfterSuccessOrError, which will be removed in 3.5.0 - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .hide() - .doAfterSuccessOrError((v, t) -> { - if (v == null && t == null) completedEmpty.set(true); - if (t != null) error.set(t); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion(Fuseable.ANY, Fuseable.NONE) - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(completedEmpty.get()).as("unexpected empty completion").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(error).as("unexpected error").hasValue(null); - } - - @Test - public void onAfterSuccessOrErrorNormalConditional() { - LongAdder invoked = new LongAdder(); - AtomicBoolean completedEmpty = new AtomicBoolean(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doAfterSuccessOrError, which will be removed in 3.5.0 - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .hide() - .filter(v -> true) - .doAfterSuccessOrError((v, t) -> { - if (v == null && t == null) completedEmpty.set(true); - if (t != null) error.set(t); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion(Fuseable.ANY, Fuseable.NONE) - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(completedEmpty.get()).as("unexpected empty completion").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(error).as("unexpected error").hasValue(null); - } - - @Test - public void onAfterSuccessOrErrorFusion() { - LongAdder invoked = new LongAdder(); - AtomicBoolean completedEmpty = new AtomicBoolean(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doAfterSuccessOrError, which will be removed in 3.5.0 - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .doAfterSuccessOrError((v, t) -> { - if (v == null && t == null) completedEmpty.set(true); - if (t != null) error.set(t); - invoked.increment(); - }); - - StepVerifier.create(mono.log()) - .expectFusion() - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(completedEmpty.get()).as("unexpected empty completion").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(error).as("unexpected error").hasValue(null); - } - - @Test - public void onAfterSuccessOrErrorFusionConditional() { - LongAdder invoked = new LongAdder(); - AtomicBoolean completedEmpty = new AtomicBoolean(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doAfterSuccessOrError, which will be removed in 3.5.0 - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .filter(v -> true) - .doAfterSuccessOrError((v, t) -> { - if (v == null && t == null) completedEmpty.set(true); - if (t != null) error.set(t); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion() - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(completedEmpty.get()).as("unexpected empty completion").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(error).as("unexpected error").hasValue(null); - } - @Test public void onAfterTerminateNormalConditional() { LongAdder invoked = new LongAdder(); @@ -431,95 +207,6 @@ public void onSuccessCallbackFailureInterruptsOnNext() { assertThat(invoked.intValue()).isEqualTo(1); } - @Test - public void onSuccessOrErrorCallbackFailureInterruptsOnNext() { - LongAdder invoked = new LongAdder(); - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = Mono.just("foo") - .doOnSuccessOrError((v, t) -> { - invoked.increment(); - throw new IllegalArgumentException(v); - }); - StepVerifier.create(mono) - .expectErrorMessage("foo") - .verify(); - - assertThat(invoked.intValue()).isEqualTo(1); - } - - @Test - public void afterSuccessOrErrorCallbackFailureInterruptsOnNextAndThrows() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - LongAdder invoked = new LongAdder(); - try { - @SuppressWarnings("deprecation") // Because of doAfterSuccessOrError, which will be removed in 3.5.0 - Mono mono = Mono.just("foo") - .doAfterSuccessOrError((v, t) -> { - invoked.increment(); - throw new IllegalArgumentException(v); - }); - StepVerifier.create(mono) - .expectNext("bar") //irrelevant - .expectErrorMessage("baz") //irrelevant - .verify(); - fail("Exception expected"); - } - catch (Throwable t) { - Throwable e = Exceptions.unwrap(t); - assertThat(e).isExactlyInstanceOf(AssertionError.class) - .hasMessage("expectation \"expectNext(bar)\" failed (expected value: bar; actual value: foo)"); - } - - assertThat(invoked.intValue()).isEqualTo(1); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("IllegalArgumentException") - .contains("foo"); - } - finally { - LoggerUtils.disableCapture(); - } - } - - @Test - public void afterTerminateCallbackFailureInterruptsOnNextAndThrows() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - LongAdder invoked = new LongAdder(); - try { - @SuppressWarnings("deprecation") // Because of doAfterSuccessOrError, which will be removed in 3.5.0 - Mono mono = Mono.just("foo") - .doAfterSuccessOrError((v, t) -> { - invoked.increment(); - throw new IllegalArgumentException(v); - }); - StepVerifier.create(mono) - .expectNext("bar") //irrelevant - .expectErrorMessage("baz") //irrelevant - .verify(); - fail("Exception expected"); - } - catch (Throwable t) { - Throwable e = Exceptions.unwrap(t); - assertThat(e).isExactlyInstanceOf(AssertionError.class) - .hasMessage("expectation \"expectNext(bar)\" failed (expected value: bar; actual value: foo)"); - } - - assertThat(invoked.intValue()).isEqualTo(1); - - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("foo") - .contains("IllegalArgumentException"); - } - finally { - LoggerUtils.disableCapture(); - } - } - @Test public void onSuccessNotCalledOnError() { LongAdder invoked = new LongAdder(); @@ -533,54 +220,6 @@ public void onSuccessNotCalledOnError() { assertThat(invoked.intValue()).isEqualTo(0); } - @Test - public void onSuccessOrErrorForOnError() { - LongAdder invoked = new LongAdder(); - AtomicReference value = new AtomicReference<>(); - AtomicReference error = new AtomicReference<>(); - - IllegalArgumentException err = new IllegalArgumentException("boom"); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono test = Mono.error(err) - .doOnSuccessOrError((v, t) -> { - invoked.increment(); - value.set(v); - error.set(t); - }); - StepVerifier.create(test) - .expectErrorMessage("boom") - .verify(); - - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(value).hasValue(null); - assertThat(error).hasValue(err); - } - - @Test - public void afterSuccessOrErrorForOnError() { - LongAdder invoked = new LongAdder(); - AtomicReference value = new AtomicReference<>(); - AtomicReference error = new AtomicReference<>(); - - IllegalArgumentException err = new IllegalArgumentException("boom"); - - @SuppressWarnings("deprecation") // Because of doAfterSuccessOrError, which will be removed in 3.5.0 - Mono mono = Mono.error(err).doAfterSuccessOrError((v, t) -> { - invoked.increment(); - value.set(v); - error.set(t); - }); - - StepVerifier.create(mono) - .expectErrorMessage("boom") - .verify(); - - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(value).hasValue(null); - assertThat(error).hasValue(err); - } - @Test public void afterTerminateForOnError() { LongAdder invoked = new LongAdder(); @@ -612,50 +251,6 @@ public void onSuccessForEmpty() { assertThat(value).hasValue(null); } - @Test - public void onSuccessOrErrorForEmpty() { - LongAdder invoked = new LongAdder(); - AtomicReference value = new AtomicReference<>(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = Mono.empty().doOnSuccessOrError((v, t) -> { - invoked.increment(); - value.set(v); - error.set(t); - }); - - StepVerifier.create(mono) - .expectComplete() - .verify(); - - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(value).hasValue(null); - assertThat(error).hasValue(null); - } - - @Test - public void afterSuccessOrErrorForEmpty() { - LongAdder invoked = new LongAdder(); - AtomicReference value = new AtomicReference<>(); - AtomicReference error = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doAfterSuccessOrError, which will be removed in 3.5.0 - Mono mono = Mono.empty() - .doAfterSuccessOrError((v, t) -> { - invoked.increment(); - value.set(v); - error.set(t); - }); - StepVerifier.create(mono) - .expectComplete() - .verify(); - - assertThat(invoked.intValue()).isEqualTo(1); - assertThat(value).hasValue(null); - assertThat(error).hasValue(null); - } - @Test public void afterTerminateForEmpty() { LongAdder invoked = new LongAdder(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoPeekTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoPeekTest.java index e91dc32830..b9d4d8e5e4 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoPeekTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoPeekTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,32 +33,6 @@ public class MonoPeekTest { - @Test - public void onMonoRejectedDoOnSuccessOrError() { - Mono mp = Mono.error(new Exception("test")); - AtomicReference ref = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = mp.doOnSuccessOrError((s, f) -> ref.set(f)); - - mono.subscribe(); - - assertThat(ref.get()).hasMessage("test"); - } - - @Test - public void onMonoSuccessDoOnSuccessOrError() { - Mono mp = Mono.just("test"); - AtomicReference ref = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = mp.doOnSuccessOrError((s, f) -> ref.set(s)); - - mono.subscribe(); - - assertThat(ref.get()).isEqualToIgnoringCase("test"); - } - @Test public void onMonoRejectedDoOnTerminate() { Mono mp = Mono.error(new Exception("test")); diff --git a/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java index 604881f413..e0c25ec6a3 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -255,21 +255,6 @@ void MonoProcessorResultNotAvailable() { }); } - @Test - void rejectedDoOnSuccessOrError() { - NextProcessor mp = new NextProcessor<>(null); - AtomicReference ref = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = mp.doOnSuccessOrError((s, f) -> ref.set(f)); - mono.subscribe(v -> {}, e -> {}); - mp.onError(new Exception("test")); - - assertThat(ref.get()).hasMessage("test"); - assertThat(mp.isSuccess()).isFalse(); - assertThat(mp.isError()).isTrue(); - } - @Test void rejectedDoOnTerminate() { NextProcessor mp = new NextProcessor<>(null); @@ -296,21 +281,6 @@ void rejectedSubscribeCallback() { assertThat(mp.isError()).isTrue(); } - @Test - void successDoOnSuccessOrError() { - NextProcessor mp = new NextProcessor<>(null); - AtomicReference ref = new AtomicReference<>(); - - @SuppressWarnings("deprecation") // Because of doOnSuccessOrError, which will be removed in 3.5.0 - Mono mono = mp.doOnSuccessOrError((s, f) -> ref.set(s)); - mono.subscribe(); - mp.onNext("test"); - - assertThat(ref.get()).isEqualToIgnoringCase("test"); - assertThat(mp.isSuccess()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - @Test void successDoOnTerminate() { NextProcessor mp = new NextProcessor<>(null); From 73c760e47e36d37b972a86e97559658930c3cd09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 10 Mar 2022 10:26:05 +0100 Subject: [PATCH 006/312] Remove deprecated context-related operators (#2953) This commit removes the following deprecated context-related operators: - `Mono.subscriberContext()` - `Flux` and `Mono` `subscriberContext(Context)` - `Flux` and `Mono` `subscriberContext(Function)` - `Context#putAll(Context)` - `Flux` and `Mono` `deferWithContext` - `Signal#getContext` It adds the methods to the list of japicmp exceptions. Fixes #2295. --- reactor-core/build.gradle | 9 ++ .../java/reactor/core/publisher/Flux.java | 81 +---------------- .../java/reactor/core/publisher/Mono.java | 91 +------------------ .../java/reactor/core/publisher/Signal.java | 14 +-- .../java/reactor/util/context/Context.java | 16 +--- 5 files changed, 20 insertions(+), 191 deletions(-) diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 78f68e9636..d79bccc81d 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -166,6 +166,15 @@ task japicmp(type: JapicmpTask) { 'reactor.core.publisher.Mono#doAfterSuccessOrError(java.util.function.BiConsumer)', 'reactor.core.publisher.Mono#doOnSuccessOrError(java.util.function.BiConsumer)', 'reactor.core.publisher.Sinks$EmitFailureHandler#busyLooping(java.time.Duration)', + 'reactor.core.publisher.Flux#deferWithContext(java.util.function.Function)', + 'reactor.core.publisher.Flux#subscriberContext(reactor.util.context.Context)', + 'reactor.core.publisher.Flux#subscriberContext(java.util.function.Function)', + 'reactor.core.publisher.Mono#deferWithContext(java.util.function.Function)', + 'reactor.core.publisher.Mono#subscriberContext()', + 'reactor.core.publisher.Mono#subscriberContext(reactor.util.context.Context)', + 'reactor.core.publisher.Mono#subscriberContext(java.util.function.Function)', + 'reactor.core.publisher.Signal#getContext()', + 'reactor.util.context.Context#putAll(reactor.util.context.Context)' ] } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 19521444fb..3709de8f3d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -803,28 +803,6 @@ public static Flux defer(Supplier> supplier) { return onAssembly(new FluxDefer<>(supplier)); } - /** - * Lazily supply a {@link Publisher} every time a {@link Subscription} is made on the - * resulting {@link Flux}, so the actual source instantiation is deferred until each - * subscribe and the {@link Function} can create a subscriber-specific instance. - * This operator behaves the same way as {@link #defer(Supplier)}, - * but accepts a {@link Function} that will receive the current {@link Context} as an argument. - * If the supplier doesn't generate a new instance however, this operator will - * effectively behave like {@link #from(Publisher)}. - * - *

- * - * - * @param contextualPublisherFactory the {@link Publisher} {@link Function} to call on subscribe - * @param the type of values passing through the {@link Flux} - * @return a deferred {@link Flux} deriving actual {@link Flux} from context values for each subscription - * @deprecated use {@link #deferContextual(Function)} - */ - @Deprecated - public static Flux deferWithContext(Function> contextualPublisherFactory) { - return deferContextual(view -> contextualPublisherFactory.apply(Context.of(view))); - } - /** * Lazily supply a {@link Publisher} every time a {@link Subscription} is made on the * resulting {@link Flux}, so the actual source instantiation is deferred until each @@ -4559,7 +4537,7 @@ public final Flux doOnComplete(Runnable onComplete) { * @return a {@link Flux} that cleans up matching elements that get discarded upstream of it. */ public final Flux doOnDiscard(final Class type, final Consumer discardHook) { - return subscriberContext(Operators.discardLocalAdapter(type, discardHook)); + return contextWrite(Operators.discardLocalAdapter(type, discardHook)); } /** @@ -6791,7 +6769,7 @@ public final Flux onBackpressureLatest() { */ public final Flux onErrorContinue(BiConsumer errorConsumer) { BiConsumer genericConsumer = errorConsumer; - return subscriberContext(Context.of( + return contextWrite(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.resume(genericConsumer) )); @@ -6875,7 +6853,7 @@ public final Flux onErrorContinue(Predicate errorPre @SuppressWarnings("unchecked") Predicate genericPredicate = (Predicate) errorPredicate; BiConsumer genericErrorConsumer = errorConsumer; - return subscriberContext(Context.of( + return contextWrite(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.resumeIf(genericPredicate, genericErrorConsumer) )); @@ -6892,7 +6870,7 @@ public final Flux onErrorContinue(Predicate errorPre * was used downstream */ public final Flux onErrorStop() { - return subscriberContext(Context.of( + return contextWrite(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.stop())); } @@ -8486,55 +8464,6 @@ public final void subscribe(Subscriber actual) { */ public abstract void subscribe(CoreSubscriber actual); - /** - * Enrich a potentially empty downstream {@link Context} by adding all values - * from the given {@link Context}, producing a new {@link Context} that is propagated - * upstream. - *

- * The {@link Context} propagation happens once per subscription (not on each onNext): - * it is done during the {@code subscribe(Subscriber)} phase, which runs from - * the last operator of a chain towards the first. - *

- * So this operator enriches a {@link Context} coming from under it in the chain - * (downstream, by default an empty one) and makes the new enriched {@link Context} - * visible to operators above it in the chain. - * - * @param mergeContext the {@link Context} to merge with a previous {@link Context} - * state, returning a new one. - * - * @return a contextualized {@link Flux} - * @see Context - * @deprecated Use {@link #contextWrite(ContextView)} instead. To be removed in 3.5.0. - */ - @Deprecated - public final Flux subscriberContext(Context mergeContext) { - return subscriberContext(c -> c.putAll(mergeContext.readOnly())); - } - - /** - * Enrich a potentially empty downstream {@link Context} by applying a {@link Function} - * to it, producing a new {@link Context} that is propagated upstream. - *

- * The {@link Context} propagation happens once per subscription (not on each onNext): - * it is done during the {@code subscribe(Subscriber)} phase, which runs from - * the last operator of a chain towards the first. - *

- * So this operator enriches a {@link Context} coming from under it in the chain - * (downstream, by default an empty one) and makes the new enriched {@link Context} - * visible to operators above it in the chain. - * - * @param doOnContext the function taking a previous {@link Context} state - * and returning a new one. - * - * @return a contextualized {@link Flux} - * @see Context - * @deprecated Use {@link #contextWrite(Function)} instead. To be removed in 3.5.0. - */ - @Deprecated - public final Flux subscriberContext(Function doOnContext) { - return contextWrite(doOnContext); - } - /** * Run subscribe, onSubscribe and request on a specified {@link Scheduler}'s {@link Worker}. * As such, placing this operator anywhere in the chain will also impact the execution diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 061dd3fc74..0806b855a9 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -215,25 +215,6 @@ public static Mono defer(Supplier> supplier) return onAssembly(new MonoDefer<>(supplier)); } - /** - * Create a {@link Mono} provider that will {@link Function#apply supply} a target {@link Mono} - * to subscribe to for each {@link Subscriber} downstream. - * This operator behaves the same way as {@link #defer(Supplier)}, - * but accepts a {@link Function} that will receive the current {@link Context} as an argument. - * - *

- * - *

- * @param contextualMonoFactory a {@link Mono} factory - * @param the element type of the returned Mono instance - * @return a deferred {@link Mono} deriving actual {@link Mono} from context values for each subscription - * @deprecated use {@link #deferContextual(Function)} instead. to be removed in 3.5.0. - */ - @Deprecated - public static Mono deferWithContext(Function> contextualMonoFactory) { - return deferContextual(view -> contextualMonoFactory.apply(Context.of(view))); - } - /** * Create a {@link Mono} provider that will {@link Function#apply supply} a target {@link Mono} * to subscribe to for each {@link Subscriber} downstream. @@ -821,21 +802,6 @@ public static Mono sequenceEqual(Publisher source1, return onAssembly(new MonoSequenceEqual<>(source1, source2, isEqual, prefetch)); } - /** - * Create a {@link Mono} emitting the {@link Context} available on subscribe. - * If no Context is available, the mono will simply emit the - * {@link Context#empty() empty Context}. - * - * @return a new {@link Mono} emitting current context - * @see #subscribe(CoreSubscriber) - * @deprecated Use {@link #deferContextual(Function)} or {@link #transformDeferredContextual(BiFunction)} to materialize - * the context. To obtain the same Mono of Context, use {@code Mono.deferContextual(Mono::just)}. To be removed in 3.5.0. - */ - @Deprecated - public static Mono subscriberContext() { - return onAssembly(MonoCurrentContext.INSTANCE); - } - /** * Uses a resource, generated by a supplier for each individual Subscriber, while streaming the value from a * Mono derived from the same resource and makes sure the resource is released if the @@ -2597,7 +2563,7 @@ public final Mono doOnCancel(Runnable onCancel) { * @return a {@link Mono} that cleans up matching elements that get discarded upstream of it. */ public final Mono doOnDiscard(final Class type, final Consumer discardHook) { - return subscriberContext(Operators.discardLocalAdapter(type, discardHook)); + return contextWrite(Operators.discardLocalAdapter(type, discardHook)); } /** @@ -3541,7 +3507,7 @@ public final Mono ofType(final Class clazz) { */ public final Mono onErrorContinue(BiConsumer errorConsumer) { BiConsumer genericConsumer = errorConsumer; - return subscriberContext(Context.of( + return contextWrite(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.resume(genericConsumer) )); @@ -3631,7 +3597,7 @@ public final Mono onErrorContinue(Predicate errorPre @SuppressWarnings("unchecked") Predicate genericPredicate = (Predicate) errorPredicate; BiConsumer genericErrorConsumer = errorConsumer; - return subscriberContext(Context.of( + return contextWrite(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.resumeIf(genericPredicate, genericErrorConsumer) )); @@ -3648,7 +3614,7 @@ public final Mono onErrorContinue(Predicate errorPre * was used downstream */ public final Mono onErrorStop() { - return subscriberContext(Context.of( + return contextWrite(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.stop())); } @@ -4367,55 +4333,6 @@ public final void subscribe(Subscriber actual) { */ public abstract void subscribe(CoreSubscriber actual); - /** - * Enrich a potentially empty downstream {@link Context} by adding all values - * from the given {@link Context}, producing a new {@link Context} that is propagated - * upstream. - *

- * The {@link Context} propagation happens once per subscription (not on each onNext): - * it is done during the {@code subscribe(Subscriber)} phase, which runs from - * the last operator of a chain towards the first. - *

- * So this operator enriches a {@link Context} coming from under it in the chain - * (downstream, by default an empty one) and makes the new enriched {@link Context} - * visible to operators above it in the chain. - * - * @param mergeContext the {@link Context} to merge with a previous {@link Context} - * state, returning a new one. - * - * @return a contextualized {@link Mono} - * @see Context - * @deprecated Use {@link #contextWrite(ContextView)} instead. To be removed in 3.5.0. - */ - @Deprecated - public final Mono subscriberContext(Context mergeContext) { - return subscriberContext(c -> c.putAll(mergeContext.readOnly())); - } - - /** - * Enrich a potentially empty downstream {@link Context} by applying a {@link Function} - * to it, producing a new {@link Context} that is propagated upstream. - *

- * The {@link Context} propagation happens once per subscription (not on each onNext): - * it is done during the {@code subscribe(Subscriber)} phase, which runs from - * the last operator of a chain towards the first. - *

- * So this operator enriches a {@link Context} coming from under it in the chain - * (downstream, by default an empty one) and makes the new enriched {@link Context} - * visible to operators above it in the chain. - * - * @param doOnContext the function taking a previous {@link Context} state - * and returning a new one. - * - * @return a contextualized {@link Mono} - * @see Context - * @deprecated Use {@link #contextWrite(Function)} instead. To be removed in 3.5.0. - */ - @Deprecated - public final Mono subscriberContext(Function doOnContext) { - return new MonoContextWrite<>(this, doOnContext); - } - /** * Run subscribe, onSubscribe and request on a specified {@link Scheduler}'s {@link Worker}. * As such, placing this operator anywhere in the chain will also impact the execution diff --git a/reactor-core/src/main/java/reactor/core/publisher/Signal.java b/reactor-core/src/main/java/reactor/core/publisher/Signal.java index 948f145139..8aa6c1ff44 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Signal.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Signal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -228,18 +228,6 @@ default boolean hasError() { */ SignalType getType(); - /** - * Return the readonly {@link Context} that is accessible by the time this {@link Signal} was - * emitted. - * - * @return an immutable {@link Context}, or an empty one if no context is available. - * @deprecated use {@link #getContextView()} instead. To be removed in 3.5.0 - */ - @Deprecated - default Context getContext() { - return Context.of(getContextView()); - } - /** * Return the readonly {@link ContextView} that is accessible by the time this {@link Signal} was * emitted. diff --git a/reactor-core/src/main/java/reactor/util/context/Context.java b/reactor-core/src/main/java/reactor/util/context/Context.java index 4239411e53..80da548e5c 100644 --- a/reactor-core/src/main/java/reactor/util/context/Context.java +++ b/reactor-core/src/main/java/reactor/util/context/Context.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -282,18 +282,4 @@ default Context putAll(ContextView other) { } return newContext; } - - /** - * See {@link #putAll(ContextView)}. - * - * @deprecated will be removed in 3.5, kept for backward compatibility with 3.3. Until - * then if you need to work around the deprecation, use {@link #putAll(ContextView)} - * combined with {@link #readOnly()} - * @param context the {@link Context} from which to copy entries - * @return a new {@link Context} with a merge of the entries from this context and the given context. - */ - @Deprecated - default Context putAll(Context context) { - return this.putAll(context.readOnly()); - } } From 1ce7f1007e736c130d13161406b67b7b974fe3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 10 Mar 2022 14:34:17 +0100 Subject: [PATCH 007/312] Change behavior of switchOnNext/switchMap default 0 prefetch (#2956) This commit changes the no-parameter variant of Flux switchMap and switchOnNext operators to default to a behavior of prefetch = 0. This was documented in 3.4.x when the parameterized variant was deprecated. Note that the prefetch > 0 implementation is slated for complete removal in 3.6.0. Old prefetch behavior could be emulated with `limitRate` on inners. Fixes #2607. --- .../java/reactor/core/publisher/Flux.java | 14 +++++-- .../core/publisher/FluxSwitchMapTest.java | 40 ++++++++----------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 3709de8f3d..4ca9cebb3c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -1904,6 +1904,10 @@ public static Flux range(int start, int count) { * completed. *

* + *

+ * This operator requests the {@code mergedPublishers} source for an unbounded amount of inner publishers, + * but doesn't request each inner {@link Publisher} unless the downstream has made + * a corresponding request (no prefetch on publishers emitted by {@code mergedPublishers}). * * @param mergedPublishers The {@link Publisher} of {@link Publisher} to switch on and mirror. * @param the produced type @@ -1911,7 +1915,8 @@ public static Flux range(int start, int count) { * @return a {@link FluxProcessor} accepting publishers and producing T */ public static Flux switchOnNext(Publisher> mergedPublishers) { - return switchOnNext(mergedPublishers, Queues.XS_BUFFER_SIZE); + return onAssembly(new FluxSwitchMapNoPrefetch<>(from(mergedPublishers), + identityFunction())); } /** @@ -8689,16 +8694,19 @@ public final Flux switchIfEmpty(Publisher alternate) { * *

* + *

+ * This operator requests the source for an unbounded amount, but doesn't + * request each generated {@link Publisher} unless the downstream has made + * a corresponding request (no prefetch of inner publishers). * * @param fn the {@link Function} to generate a {@link Publisher} for each source value * @param the type of the return value of the transformation function * * @return a new {@link Flux} that emits values from an alternative {@link Publisher} * for each source onNext - * */ public final Flux switchMap(Function> fn) { - return switchMap(fn, Queues.XS_BUFFER_SIZE); + return onAssembly(new FluxSwitchMapNoPrefetch<>(this, fn)); } /** diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxSwitchMapTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxSwitchMapTest.java index ac98ffc8d6..7b00167a85 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxSwitchMapTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxSwitchMapTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,12 @@ import reactor.test.ParameterizedTestWithName; import reactor.test.StepVerifier; import reactor.test.subscriber.AssertSubscriber; +import reactor.test.subscriber.TestSubscriber; import reactor.util.concurrent.Queues; import static org.assertj.core.api.Assertions.assertThat; import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST; +import static reactor.core.publisher.Sinks.EmitResult.FAIL_OVERFLOW; @SuppressWarnings("deprecation") public class FluxSwitchMapTest { @@ -125,8 +127,8 @@ public void noswitch(int prefetch) { } @Test - public void noswitchBackpressured() { - AssertSubscriber ts = AssertSubscriber.create(0); + public void noswitchBackpressuredZeroPrefetch() { + TestSubscriber ts = TestSubscriber.builder().initialRequest(0).build(); Sinks.Many sp1 = Sinks.unsafe().many().multicast().directBestEffort(); Sinks.Many sp2 = Sinks.unsafe().many().multicast().directBestEffort(); @@ -137,34 +139,24 @@ public void noswitchBackpressured() { sp1.emitNext(1, FAIL_FAST); - sp2.emitNext(10, FAIL_FAST); - sp2.emitNext(20, FAIL_FAST); - sp2.emitNext(30, FAIL_FAST); - sp2.emitNext(40, FAIL_FAST); - sp2.emitComplete(FAIL_FAST); - - ts.assertNoValues() - .assertNoError() - .assertNotComplete(); + assertThat(sp2.tryEmitNext(10)).as("tryEmit(10) before downstream request").isEqualTo(FAIL_OVERFLOW); ts.request(2); + sp2.tryEmitNext(11).orThrow(); + sp2.tryEmitNext(20).orThrow(); - ts.assertValues(10, 20) - .assertNoError() - .assertNotComplete(); - - sp1.emitComplete(FAIL_FAST); - - ts.assertValues(10, 20) - .assertNoError() - .assertNotComplete(); + assertThat(sp2.tryEmitNext(30)).as("tryEmit(30) before second downstream request").isEqualTo(FAIL_OVERFLOW); ts.request(2); + sp2.tryEmitNext(31).orThrow(); + sp2.tryEmitNext(40).orThrow(); + sp2.tryEmitComplete().orThrow(); - ts.assertValues(10, 20, 30, 40) - .assertNoError() - .assertComplete(); + assertThat(ts.getReceivedOnNext()).as("received 4 onNext").containsExactly(11, 20, 31, 40); + assertThat(ts.isTerminatedComplete()).as("not completed until source complete").isFalse(); + sp1.tryEmitComplete().orThrow(); + assertThat(ts.isTerminatedComplete()).as("completed once source complete").isTrue(); } @ParameterizedTestWithName From b96315ed47c8a9f0b6a0b92aa37c39367c4ce3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 10 Mar 2022 14:34:57 +0100 Subject: [PATCH 008/312] Remove deprecated ElasticScheduler (#2955) Fixes #2207. --- reactor-core/build.gradle | 8 +- .../scheduler/BoundedElasticScheduler.java | 1 + .../core/scheduler/ElasticScheduler.java | 358 ----------------- .../reactor/core/scheduler/Schedulers.java | 156 +------- .../core/scheduler/ElasticSchedulerTest.java | 361 ------------------ .../core/scheduler/SchedulersTest.java | 77 +--- .../core/scheduler/SchedulersMetricsTest.java | 7 +- .../test/scheduler/VirtualTimeScheduler.java | 8 +- .../publisher/ColdTestPublisherTests.java | 4 +- .../scheduler/VirtualTimeSchedulerTests.java | 11 +- 10 files changed, 18 insertions(+), 973 deletions(-) delete mode 100644 reactor-core/src/main/java/reactor/core/scheduler/ElasticScheduler.java delete mode 100644 reactor-core/src/test/java/reactor/core/scheduler/ElasticSchedulerTest.java diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index d79bccc81d..6f344a6346 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -174,7 +174,13 @@ task japicmp(type: JapicmpTask) { 'reactor.core.publisher.Mono#subscriberContext(reactor.util.context.Context)', 'reactor.core.publisher.Mono#subscriberContext(java.util.function.Function)', 'reactor.core.publisher.Signal#getContext()', - 'reactor.util.context.Context#putAll(reactor.util.context.Context)' + 'reactor.util.context.Context#putAll(reactor.util.context.Context)', + 'reactor.core.scheduler.Schedulers#elastic()', + 'reactor.core.scheduler.Schedulers#newElastic(java.lang.String)', + 'reactor.core.scheduler.Schedulers#newElastic(java.lang.String, int)', + 'reactor.core.scheduler.Schedulers#newElastic(java.lang.String, int, boolean)', + 'reactor.core.scheduler.Schedulers#newElastic(int, java.util.concurrent.ThreadFactory)', + 'reactor.core.scheduler.Schedulers$Factory#newElastic(int, java.util.concurrent.ThreadFactory)' ] } diff --git a/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticScheduler.java b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticScheduler.java index 80e92d3908..4cdaf8d4bb 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticScheduler.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticScheduler.java @@ -70,6 +70,7 @@ final class BoundedElasticScheduler implements Scheduler, Scannable { static final int DEFAULT_TTL_SECONDS = 60; + static final AtomicLong COUNTER = new AtomicLong(); static final AtomicLong EVICTOR_COUNTER = new AtomicLong(); static final ThreadFactory EVICTOR_FACTORY = r -> { diff --git a/reactor-core/src/main/java/reactor/core/scheduler/ElasticScheduler.java b/reactor-core/src/main/java/reactor/core/scheduler/ElasticScheduler.java deleted file mode 100644 index 22517a836f..0000000000 --- a/reactor-core/src/main/java/reactor/core/scheduler/ElasticScheduler.java +++ /dev/null @@ -1,358 +0,0 @@ -/* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.scheduler; - -import java.util.ArrayList; -import java.util.Deque; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Stream; - -import reactor.core.Disposable; -import reactor.core.Disposables; -import reactor.core.Scannable; -import reactor.util.annotation.Nullable; - -/** - * Dynamically creates ScheduledExecutorService-based Workers and caches the thread pools, reusing - * them once the Workers have been shut down. This scheduler is time-capable (can schedule - * with delay / periodically). - *

- * The maximum number of created thread pools is unbounded. - *

- * The default time-to-live for unused thread pools is 60 seconds, use the - * appropriate constructor to set a different value. - *

- * This scheduler is not restartable (may be later). - * - * @author Stephane Maldini - * @author Simon Baslé - */ -// To be removed in 3.5 -final class ElasticScheduler implements Scheduler, Scannable { - - static final AtomicLong COUNTER = new AtomicLong(); - - static final ThreadFactory EVICTOR_FACTORY = r -> { - Thread t = new Thread(r, "elastic-evictor-" + COUNTER.incrementAndGet()); - t.setDaemon(true); - return t; - }; - - static final CachedService SHUTDOWN = new CachedService(null); - - static final int DEFAULT_TTL_SECONDS = 60; - - final ThreadFactory factory; - - final int ttlSeconds; - - - final Deque cache; - - final Queue all; - - ScheduledExecutorService evictor; - - - volatile boolean shutdown; - - ElasticScheduler(ThreadFactory factory, int ttlSeconds) { - if (ttlSeconds < 0) { - throw new IllegalArgumentException("ttlSeconds must be positive, was: " + ttlSeconds); - } - this.ttlSeconds = ttlSeconds; - this.factory = factory; - this.cache = new ConcurrentLinkedDeque<>(); - this.all = new ConcurrentLinkedQueue<>(); - //evictor is now started in `start()`. make it look like it is constructed shutdown - this.shutdown = true; - } - - /** - * Instantiates the default {@link ScheduledExecutorService} for the ElasticScheduler - * ({@code Executors.newScheduledThreadPoolExecutor} with core and max pool size of 1). - */ - public ScheduledExecutorService createUndecoratedService() { - ScheduledThreadPoolExecutor poolExecutor = new ScheduledThreadPoolExecutor(1, factory); - poolExecutor.setMaximumPoolSize(1); - poolExecutor.setRemoveOnCancelPolicy(true); - return poolExecutor; - } - - @Override - public void start() { - if (!shutdown) { - return; - } - this.evictor = Executors.newScheduledThreadPool(1, EVICTOR_FACTORY); - this.evictor.scheduleAtFixedRate(this::eviction, - ttlSeconds, - ttlSeconds, - TimeUnit.SECONDS); - this.shutdown = false; - } - - @Override - public boolean isDisposed() { - return shutdown; - } - - @Override - public void dispose() { - if (shutdown) { - return; - } - shutdown = true; - - evictor.shutdownNow(); - - cache.clear(); - - CachedService cached; - - while ((cached = all.poll()) != null) { - cached.exec.shutdownNow(); - } - } - - CachedService pick() { - if (shutdown) { - return SHUTDOWN; - } - CachedService result; - ScheduledExecutorServiceExpiry e = cache.pollLast(); - if (e != null) { - return e.cached; - } - - result = new CachedService(this); - all.offer(result); - if (shutdown) { - all.remove(result); - return SHUTDOWN; - } - return result; - } - - @Override - public Disposable schedule(Runnable task) { - CachedService cached = pick(); - - return Schedulers.directSchedule(cached.exec, - task, - cached, - 0L, - TimeUnit.MILLISECONDS); - } - - @Override - public Disposable schedule(Runnable task, long delay, TimeUnit unit) { - CachedService cached = pick(); - - return Schedulers.directSchedule(cached.exec, - task, - cached, - delay, - unit); - } - - @Override - public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { - CachedService cached = pick(); - - return Disposables.composite(Schedulers.directSchedulePeriodically(cached.exec, - task, - initialDelay, - period, - unit), cached); - } - - @Override - public String toString() { - StringBuilder ts = new StringBuilder(Schedulers.ELASTIC) - .append('('); - if (factory instanceof ReactorThreadFactory) { - ts.append('\"').append(((ReactorThreadFactory) factory).get()).append('\"'); - } - ts.append(')'); - return ts.toString(); - } - - @Override - public Object scanUnsafe(Attr key) { - if (key == Attr.TERMINATED || key == Attr.CANCELLED) return isDisposed(); - if (key == Attr.CAPACITY) return Integer.MAX_VALUE; - if (key == Attr.BUFFERED) return cache.size(); //BUFFERED: number of workers alive - if (key == Attr.NAME) return this.toString(); - - return null; - } - - @Override - public Stream inners() { - return cache.stream() - .map(cached -> cached.cached); - } - - @Override - public Worker createWorker() { - return new ElasticWorker(pick()); - } - - void eviction() { - long now = System.currentTimeMillis(); - - List list = new ArrayList<>(cache); - for (ScheduledExecutorServiceExpiry e : list) { - if (e.expireMillis < now) { - if (cache.remove(e)) { - e.cached.exec.shutdownNow(); - all.remove(e.cached); - } - } - } - } - - static final class CachedService implements Disposable, Scannable { - - final ElasticScheduler parent; - final ScheduledExecutorService exec; - - CachedService(@Nullable ElasticScheduler parent) { - this.parent = parent; - if (parent != null) { - this.exec = Schedulers.decorateExecutorService(parent, parent.createUndecoratedService()); - } - else { - this.exec = Executors.newSingleThreadScheduledExecutor(); - this.exec.shutdownNow(); - } - } - - @Override - public void dispose() { - if (exec != null) { - if (this != SHUTDOWN && !parent.shutdown) { - ScheduledExecutorServiceExpiry e = new - ScheduledExecutorServiceExpiry(this, - System.currentTimeMillis() + parent.ttlSeconds * 1000L); - parent.cache.offerLast(e); - if (parent.shutdown) { - if (parent.cache.remove(e)) { - exec.shutdownNow(); - } - } - } - } - } - - @Override - public Object scanUnsafe(Attr key) { - if (key == Attr.NAME) return parent.scanUnsafe(key); - if (key == Attr.PARENT) return parent; - if (key == Attr.TERMINATED || key == Attr.CANCELLED) return isDisposed(); - if (key == Attr.CAPACITY) { - //assume 1 if unknown, otherwise use the one from underlying executor - Integer capacity = (Integer) Schedulers.scanExecutor(exec, key); - if (capacity == null || capacity == -1) return 1; - } - return Schedulers.scanExecutor(exec, key); - } - } - - static final class ScheduledExecutorServiceExpiry { - - final CachedService cached; - final long expireMillis; - - ScheduledExecutorServiceExpiry(CachedService cached, long expireMillis) { - this.cached = cached; - this.expireMillis = expireMillis; - } - } - - static final class ElasticWorker extends AtomicBoolean implements Worker, Scannable { - - final CachedService cached; - - final Disposable.Composite tasks; - - ElasticWorker(CachedService cached) { - this.cached = cached; - this.tasks = Disposables.composite(); - } - - @Override - public Disposable schedule(Runnable task) { - return Schedulers.workerSchedule(cached.exec, - tasks, - task, - 0L, - TimeUnit.MILLISECONDS); - } - - @Override - public Disposable schedule(Runnable task, long delay, TimeUnit unit) { - return Schedulers.workerSchedule(cached.exec, tasks, task, delay, unit); - } - - @Override - public Disposable schedulePeriodically(Runnable task, - long initialDelay, - long period, - TimeUnit unit) { - return Schedulers.workerSchedulePeriodically(cached.exec, - tasks, - task, - initialDelay, - period, - unit); - } - - @Override - public void dispose() { - if (compareAndSet(false, true)) { - tasks.dispose(); - cached.dispose(); - } - } - - @Override - public boolean isDisposed() { - return tasks.isDisposed(); - } - - @Override - public Object scanUnsafe(Attr key) { - if (key == Attr.TERMINATED || key == Attr.CANCELLED) return isDisposed(); - if (key == Attr.NAME) return cached.scanUnsafe(key) + ".worker"; - if (key == Attr.PARENT) return cached.parent; - - return cached.scanUnsafe(key); - } - } -} diff --git a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java index 35cc26a6a1..2a3a09006c 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,6 @@ *

    *
  • {@link #parallel()}: Optimized for fast {@link Runnable} non-blocking executions
  • *
  • {@link #single}: Optimized for low-latency {@link Runnable} one-off executions
  • - *
  • {@link #elastic()}: Optimized for longer executions, an alternative for blocking tasks where the number of active tasks (and threads) can grow indefinitely
  • *
  • {@link #boundedElastic()}: Optimized for longer executions, an alternative for blocking tasks where the number of active tasks (and threads) is capped
  • *
  • {@link #immediate}: to immediately run submitted {@link Runnable} instead of scheduling them (somewhat of a no-op or "null object" {@link Scheduler})
  • *
  • {@link #fromExecutorService(ExecutorService)} to create new instances around {@link java.util.concurrent.Executors}
  • @@ -176,27 +175,6 @@ public static Scheduler fromExecutorService(ExecutorService executorService, Str return scheduler; } - /** - * {@link Scheduler} that dynamically creates ExecutorService-based Workers and caches - * the thread pools, reusing them once the Workers have been shut down. - *

    - * The maximum number of created thread pools is unbounded. - *

    - * The default time-to-live for unused thread pools is 60 seconds, use the appropriate - * factory to set a different value. - *

    - * This scheduler is not restartable. - * - * @return default instance of a {@link Scheduler} that dynamically creates ExecutorService-based - * Workers and caches the threads, reusing them once the Workers have been shut - * down - * @deprecated use {@link #boundedElastic()}, to be removed in 3.5.0 - */ - @Deprecated - public static Scheduler elastic() { - return cache(CACHED_ELASTIC, ELASTIC, ELASTIC_SUPPLIER); - } - /** * {@link Scheduler} that dynamically creates a bounded number of ExecutorService-based * Workers, reusing them once the Workers have been shut down. The underlying daemon @@ -252,99 +230,6 @@ public static Scheduler immediate() { return ImmediateScheduler.instance(); } - /** - * {@link Scheduler} that dynamically creates ExecutorService-based Workers and caches - * the thread pools, reusing them once the Workers have been shut down. - *

    - * The maximum number of created thread pools is unbounded. - *

    - * The default time-to-live for unused thread pools is 60 seconds, use the appropriate - * factory to set a different value. - *

    - * This scheduler is not restartable. - * - * @param name Thread prefix - * - * @return a new {@link Scheduler} that dynamically creates ExecutorService-based - * Workers and caches the thread pools, reusing them once the Workers have been shut - * down - * @deprecated use {@link #newBoundedElastic(int, int, String)}, to be removed in 3.5.0 - */ - @Deprecated - public static Scheduler newElastic(String name) { - return newElastic(name, ElasticScheduler.DEFAULT_TTL_SECONDS); - } - - /** - * {@link Scheduler} that dynamically creates ExecutorService-based Workers and caches - * the thread pools, reusing them once the Workers have been shut down. - *

    - * The maximum number of created thread pools is unbounded. - *

    - * This scheduler is not restartable. - * - * @param name Thread prefix - * @param ttlSeconds Time-to-live for an idle {@link reactor.core.scheduler.Scheduler.Worker} - * - * @return a new {@link Scheduler} that dynamically creates ExecutorService-based - * Workers and caches the thread pools, reusing them once the Workers have been shut - * down - * @deprecated use {@link #newBoundedElastic(int, int, String, int)}, to be removed in 3.5.0 - */ - @Deprecated - public static Scheduler newElastic(String name, int ttlSeconds) { - return newElastic(name, ttlSeconds, false); - } - - /** - * {@link Scheduler} that dynamically creates ExecutorService-based Workers and caches - * the thread pools, reusing them once the Workers have been shut down. - *

    - * The maximum number of created thread pools is unbounded. - *

    - * This scheduler is not restartable. - * - * @param name Thread prefix - * @param ttlSeconds Time-to-live for an idle {@link reactor.core.scheduler.Scheduler.Worker} - * @param daemon false if the {@link Scheduler} requires an explicit {@link - * Scheduler#dispose()} to exit the VM. - * - * @return a new {@link Scheduler} that dynamically creates ExecutorService-based - * Workers and caches the thread pools, reusing them once the Workers have been shut - * down - * @deprecated use {@link #newBoundedElastic(int, int, String, int, boolean)}, to be removed in 3.5.0 - */ - @Deprecated - public static Scheduler newElastic(String name, int ttlSeconds, boolean daemon) { - return newElastic(ttlSeconds, - new ReactorThreadFactory(name, ElasticScheduler.COUNTER, daemon, false, - Schedulers::defaultUncaughtException)); - } - - /** - * {@link Scheduler} that dynamically creates ExecutorService-based Workers and caches - * the thread pools, reusing them once the Workers have been shut down. - *

    - * The maximum number of created thread pools is unbounded. - *

    - * This scheduler is not restartable. - * - * @param ttlSeconds Time-to-live for an idle {@link reactor.core.scheduler.Scheduler.Worker} - * @param threadFactory a {@link ThreadFactory} to use each thread initialization - * - * @return a new {@link Scheduler} that dynamically creates ExecutorService-based - * Workers and caches the thread pools, reusing them once the Workers have been shut - * down - * @deprecated use {@link #newBoundedElastic(int, int, ThreadFactory, int)}, to be removed in 3.5.0 - */ - @Deprecated - public static Scheduler newElastic(int ttlSeconds, ThreadFactory threadFactory) { - final Scheduler fromFactory = factory.newElastic(ttlSeconds, threadFactory); - fromFactory.start(); - return fromFactory; - } - - /** * {@link Scheduler} that dynamically creates a bounded number of ExecutorService-based * Workers, reusing them once the Workers have been shut down. The underlying (user) @@ -452,7 +337,7 @@ public static Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, Stri */ public static Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, String name, int ttlSeconds, boolean daemon) { return newBoundedElastic(threadCap, queuedTaskCap, - new ReactorThreadFactory(name, ElasticScheduler.COUNTER, daemon, false, + new ReactorThreadFactory(name, BoundedElasticScheduler.COUNTER, daemon, false, Schedulers::defaultUncaughtException), ttlSeconds); } @@ -698,7 +583,6 @@ public static Snapshot setFactoryWithSnapshot(Factory newFactory) { //nulling out CACHED references ensures that the schedulers won't be disposed //when setting the newFactory via setFactory Snapshot snapshot = new Snapshot( - CACHED_ELASTIC.getAndSet(null), CACHED_BOUNDED_ELASTIC.getAndSet(null), CACHED_PARALLEL.getAndSet(null), CACHED_SINGLE.getAndSet(null), @@ -720,7 +604,6 @@ public static void resetFrom(@Nullable Snapshot snapshot) { } //Restore the atomic references first, so that concurrent calls to Schedulers either //get a soon-to-be-shutdown instance or the restored instance - CachedScheduler oldElastic = CACHED_ELASTIC.getAndSet(snapshot.oldElasticScheduler); CachedScheduler oldBoundedElastic = CACHED_BOUNDED_ELASTIC.getAndSet(snapshot.oldBoundedElasticScheduler); CachedScheduler oldParallel = CACHED_PARALLEL.getAndSet(snapshot.oldParallelScheduler); CachedScheduler oldSingle = CACHED_SINGLE.getAndSet(snapshot.oldSingleScheduler); @@ -731,7 +614,6 @@ public static void resetFrom(@Nullable Snapshot snapshot) { factory = snapshot.oldFactory; //Shutdown the old CachedSchedulers, if any - if (oldElastic != null) oldElastic._dispose(); if (oldBoundedElastic != null) oldBoundedElastic._dispose(); if (oldParallel != null) oldParallel._dispose(); if (oldSingle != null) oldSingle._dispose(); @@ -947,12 +829,10 @@ public static Runnable onSchedule(Runnable runnable) { * Clear any cached {@link Scheduler} and call dispose on them. */ public static void shutdownNow() { - CachedScheduler oldElastic = CACHED_ELASTIC.getAndSet(null); CachedScheduler oldBoundedElastic = CACHED_BOUNDED_ELASTIC.getAndSet(null); CachedScheduler oldParallel = CACHED_PARALLEL.getAndSet(null); CachedScheduler oldSingle = CACHED_SINGLE.getAndSet(null); - if (oldElastic != null) oldElastic._dispose(); if (oldBoundedElastic != null) oldBoundedElastic._dispose(); if (oldParallel != null) oldParallel._dispose(); if (oldSingle != null) oldSingle._dispose(); @@ -993,24 +873,6 @@ public static Scheduler single(Scheduler original) { */ public interface Factory { - /** - * {@link Scheduler} that dynamically creates Workers resources and caches - * eventually, reusing them once the Workers have been shut down. - *

    - * The maximum number of created workers is unbounded. - * - * @param ttlSeconds Time-to-live for an idle {@link reactor.core.scheduler.Scheduler.Worker} - * @param threadFactory a {@link ThreadFactory} to use - * - * @return a new {@link Scheduler} that dynamically creates Workers resources and - * caches eventually, reusing them once the Workers have been shut down - * @deprecated use {@link Factory#newBoundedElastic(int, int, ThreadFactory, int)}, to be removed in 3.5.0 - */ - @Deprecated - default Scheduler newElastic(int ttlSeconds, ThreadFactory threadFactory) { - return new ElasticScheduler(threadFactory, ttlSeconds); - } - /** * {@link Scheduler} that dynamically creates a bounded number of ExecutorService-based * Workers, reusing them once the Workers have been shut down. The underlying (user or daemon) @@ -1064,9 +926,6 @@ default Scheduler newSingle(ThreadFactory threadFactory) { */ public static final class Snapshot implements Disposable { - @Nullable - final CachedScheduler oldElasticScheduler; - @Nullable final CachedScheduler oldBoundedElasticScheduler; @@ -1078,12 +937,10 @@ public static final class Snapshot implements Disposable { final Factory oldFactory; - private Snapshot(@Nullable CachedScheduler oldElasticScheduler, - @Nullable CachedScheduler oldBoundedElasticScheduler, + private Snapshot(@Nullable CachedScheduler oldBoundedElasticScheduler, @Nullable CachedScheduler oldParallelScheduler, @Nullable CachedScheduler oldSingleScheduler, Factory factory) { - this.oldElasticScheduler = oldElasticScheduler; this.oldBoundedElasticScheduler = oldBoundedElasticScheduler; this.oldParallelScheduler = oldParallelScheduler; this.oldSingleScheduler = oldSingleScheduler; @@ -1093,7 +950,6 @@ private Snapshot(@Nullable CachedScheduler oldElasticScheduler, @Override public boolean isDisposed() { return - (oldElasticScheduler == null || oldElasticScheduler.isDisposed()) && (oldBoundedElasticScheduler == null || oldBoundedElasticScheduler.isDisposed()) && (oldParallelScheduler == null || oldParallelScheduler.isDisposed()) && (oldSingleScheduler == null || oldSingleScheduler.isDisposed()); @@ -1101,7 +957,6 @@ public boolean isDisposed() { @Override public void dispose() { - if (oldElasticScheduler != null) oldElasticScheduler._dispose(); if (oldBoundedElasticScheduler != null) oldBoundedElasticScheduler._dispose(); if (oldParallelScheduler != null) oldParallelScheduler._dispose(); if (oldSingleScheduler != null) oldSingleScheduler._dispose(); @@ -1109,7 +964,6 @@ public void dispose() { } // Internals - static final String ELASTIC = "elastic"; // IO stuff static final String BOUNDED_ELASTIC = "boundedElastic"; // Blocking stuff with scale to zero static final String PARALLEL = "parallel"; //scale up common tasks static final String SINGLE = "single"; //non blocking tasks @@ -1119,14 +973,10 @@ public void dispose() { // Cached schedulers in atomic references: - static AtomicReference CACHED_ELASTIC = new AtomicReference<>(); static AtomicReference CACHED_BOUNDED_ELASTIC = new AtomicReference<>(); static AtomicReference CACHED_PARALLEL = new AtomicReference<>(); static AtomicReference CACHED_SINGLE = new AtomicReference<>(); - static final Supplier ELASTIC_SUPPLIER = - () -> newElastic(ELASTIC, ElasticScheduler.DEFAULT_TTL_SECONDS, true); - static final Supplier BOUNDED_ELASTIC_SUPPLIER = () -> newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, BOUNDED_ELASTIC, BoundedElasticScheduler.DEFAULT_TTL_SECONDS, true); diff --git a/reactor-core/src/test/java/reactor/core/scheduler/ElasticSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/ElasticSchedulerTest.java deleted file mode 100644 index 845464f1e9..0000000000 --- a/reactor-core/src/test/java/reactor/core/scheduler/ElasticSchedulerTest.java +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.scheduler; - -import java.time.Duration; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; -import com.pivovarit.function.ThrowingRunnable; -import org.assertj.core.data.Offset; - -import reactor.core.Disposable; -import reactor.core.Scannable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; -import reactor.util.Logger; -import reactor.util.Loggers; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * @author Stephane Maldini - * @author Simon Baslé - */ -@SuppressWarnings("deprecation") // This is because of #newElastic() calls, to be removed in 3.5. ElasticScheduler class would then also be removed. -public class ElasticSchedulerTest extends AbstractSchedulerTest { - - private static final Logger LOGGER = Loggers.getLogger(ElasticSchedulerTest.class); - - @Override - protected Scheduler scheduler() { - return Schedulers.newElastic("ElasticSchedulerTest"); - } - - @Override - protected boolean shouldCheckInterrupted() { - return true; - } - - @Override - protected boolean shouldCheckSupportRestart() { - return true; - } - - @Test - public void bothStartAndRestartDoNotThrow() { - Scheduler scheduler = afterTest.autoDispose(scheduler()); - assertThatCode(scheduler::start).as("start").doesNotThrowAnyException(); - - scheduler.dispose(); - assertThatCode(scheduler::start).as("restart").doesNotThrowAnyException(); - } - - @Test - public void negativeTime() throws Exception { - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> { - Schedulers.newElastic("test", -1); - }); - } - - @Test - @Timeout(10) - public void eviction() throws Exception { - Scheduler s = Schedulers.newElastic("test-recycle", 2); - ((ElasticScheduler)s).evictor.shutdownNow(); - - try{ - for (int i = 0; i < 100; i++) { - Disposable d = s.schedule(() -> { - try { - Thread.sleep(10000); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - - d.dispose(); - } - - while(((ElasticScheduler)s).cache.peek() != null){ - ((ElasticScheduler)s).eviction(); - Thread.sleep(100); - } - - assertThat(((ElasticScheduler)s).all).isEmpty(); - } - finally { - s.dispose(); - s.dispose();//noop - } - - assertThat(((ElasticScheduler)s).cache).isEmpty(); - assertThat(s.isDisposed()).isTrue(); - } - - @Test - public void scheduledDoesntReject() { - Scheduler s = scheduler(); - - assertThat(s.schedule(() -> {}, 100, TimeUnit.MILLISECONDS)) - .describedAs("direct delayed scheduling") - .isNotNull(); - assertThat(s.schedulePeriodically(() -> {}, 100, 100, TimeUnit.MILLISECONDS)) - .describedAs("direct periodic scheduling") - .isNotNull(); - - Scheduler.Worker w = s.createWorker(); - assertThat(w.schedule(() -> {}, 100, TimeUnit.MILLISECONDS)) - .describedAs("worker delayed scheduling") - .isNotNull(); - assertThat(w.schedulePeriodically(() -> {}, 100, 100, TimeUnit.MILLISECONDS)) - .describedAs("worker periodic scheduling") - .isNotNull(); - } - - @Test - public void smokeTestDelay() { - for (int i = 0; i < 20; i++) { - Scheduler s = Schedulers.newElastic("test"); - AtomicLong start = new AtomicLong(); - AtomicLong end = new AtomicLong(); - - try { - StepVerifier.create(Mono - .delay(Duration.ofMillis(100), s) - .doOnSubscribe(sub -> start.set(System.nanoTime())) - .doOnTerminate(() -> end.set(System.nanoTime())) - ) - .expectSubscription() - .expectNext(0L) - .verifyComplete(); - - long endValue = end.longValue(); - long startValue = start.longValue(); - long measuredDelay = endValue - startValue; - long measuredDelayMs = TimeUnit.NANOSECONDS.toMillis(measuredDelay); - assertThat(measuredDelayMs) - .as("iteration %s, measured delay %s nanos, start at %s nanos, end at %s nanos", i, measuredDelay, startValue, endValue) - .isGreaterThanOrEqualTo(100L) - .isLessThan(200L); - } - finally { - s.dispose(); - } - } - } - - @Test - public void smokeTestInterval() { - Scheduler s = scheduler(); - - try { - StepVerifier.create(Flux.interval(Duration.ofMillis(100), Duration.ofMillis(200), s)) - .expectSubscription() - .expectNoEvent(Duration.ofMillis(100)) - .expectNext(0L) - .expectNoEvent(Duration.ofMillis(200)) - .expectNext(1L) - .expectNoEvent(Duration.ofMillis(200)) - .expectNext(2L) - .thenCancel(); - } - finally { - s.dispose(); - } - } - - @Test - public void scanName() { - Scheduler withNamedFactory = Schedulers.newElastic("scanName", 1); - Scheduler withBasicFactory = Schedulers.newElastic(1, Thread::new); - Scheduler cached = Schedulers.elastic(); - - Scheduler.Worker workerWithNamedFactory = withNamedFactory.createWorker(); - Scheduler.Worker workerWithBasicFactory = withBasicFactory.createWorker(); - - try { - assertThat(Scannable.from(withNamedFactory).scan(Scannable.Attr.NAME)) - .as("withNamedFactory") - .isEqualTo("elastic(\"scanName\")"); - - assertThat(Scannable.from(withBasicFactory).scan(Scannable.Attr.NAME)) - .as("withBasicFactory") - .isEqualTo("elastic()"); - - assertThat(cached) - .as("elastic() is cached") - .is(SchedulersTest.CACHED_SCHEDULER); - assertThat(Scannable.from(cached).scan(Scannable.Attr.NAME)) - .as("default elastic()") - .isEqualTo("Schedulers.elastic()"); - - assertThat(Scannable.from(workerWithNamedFactory).scan(Scannable.Attr.NAME)) - .as("workerWithNamedFactory") - .isEqualTo("elastic(\"scanName\").worker"); - - assertThat(Scannable.from(workerWithBasicFactory).scan(Scannable.Attr.NAME)) - .as("workerWithBasicFactory") - .isEqualTo("elastic().worker"); - } - finally { - withNamedFactory.dispose(); - withBasicFactory.dispose(); - workerWithNamedFactory.dispose(); - workerWithBasicFactory.dispose(); - } - } - - @Test - public void scanCapacity() { - Scheduler scheduler = Schedulers.newElastic(2, Thread::new); - Scheduler.Worker worker = scheduler.createWorker(); - try { - assertThat(Scannable.from(scheduler).scan(Scannable.Attr.CAPACITY)).as("scheduler unbounded").isEqualTo(Integer.MAX_VALUE); - assertThat(Scannable.from(worker).scan(Scannable.Attr.CAPACITY)).as("worker capacity").isEqualTo(1); - } - finally { - worker.dispose(); - scheduler.dispose(); - } - } - - @Test - public void lifoEviction() throws InterruptedException { - Scheduler scheduler = Schedulers.newElastic("dequeueEviction", 1); - int otherThreads = Thread.activeCount(); - try { - - int cacheSleep = 100; //slow tasks last 100ms - int cacheCount = 100; //100 of slow tasks - int fastSleep = 10; //interval between fastTask scheduling - int fastCount = 200; //will schedule fast tasks up to 2s later - CountDownLatch latch = new CountDownLatch(cacheCount + fastCount); - for (int i = 0; i < cacheCount; i++) { - Mono.fromRunnable(ThrowingRunnable.unchecked(() -> Thread.sleep(cacheSleep))) - .subscribeOn(scheduler) - .doFinally(sig -> latch.countDown()) - .subscribe(); - } - - int oldActive = 0; - int activeAtBeginning = 0; - int activeAtEnd = Integer.MAX_VALUE; - for (int i = 0; i < fastCount; i++) { - Mono.just(i) - .subscribeOn(scheduler) - .doFinally(sig -> latch.countDown()) - .subscribe(); - - if (i == 0) { - activeAtBeginning = Math.max(0, Thread.activeCount() - otherThreads); - oldActive = activeAtBeginning; - LOGGER.info("{} threads active in round 1/{}", activeAtBeginning, fastCount); - } - else if (i == fastCount - 1) { - activeAtEnd = Math.max(0, Thread.activeCount() - otherThreads); - LOGGER.info("{} threads active in round {}/{}", activeAtEnd, i + 1, fastCount); - } - else { - int newActive = Math.max(0, Thread.activeCount() - otherThreads); - if (oldActive != newActive) { - oldActive = newActive; - LOGGER.info("{} threads active in round {}/{}", oldActive, i + 1, fastCount); - } - } - Thread.sleep(fastSleep); - } - - assertThat(latch.await(3, TimeUnit.SECONDS)).as("latch 3s").isTrue(); - assertThat(activeAtEnd).as("active in last round") - .isLessThan(activeAtBeginning) - .isCloseTo(1, Offset.offset(5)); - } - finally { - scheduler.dispose(); - LOGGER.info("{} threads active post shutdown", Thread.activeCount() - otherThreads); - } - } - - @Test - public void doesntRecycleWhileRunningAfterDisposed() throws Exception { - Scheduler s = Schedulers.newElastic("test-recycle"); - ((ElasticScheduler)s).evictor.shutdownNow(); - - try { - AtomicBoolean stop = new AtomicBoolean(false); - CountDownLatch started = new CountDownLatch(1); - Disposable d = s.schedule(() -> { - started.countDown(); - // simulate uninterruptible computation - for (;;) { - if (stop.get()) { - break; - } - } - }); - assertThat(started.await(10, TimeUnit.SECONDS)).as("latch timeout").isTrue(); - d.dispose(); - - Thread.sleep(100); - assertThat(((ElasticScheduler)s).cache).isEmpty(); - - stop.set(true); - - Thread.sleep(100); - assertThat(((ElasticScheduler)s).cache.size()).isEqualTo(1); - } - finally { - s.dispose(); - } - } - - @Test - public void recycleOnce() throws Exception { - Scheduler s = Schedulers.newElastic("test-recycle"); - ((ElasticScheduler)s).evictor.shutdownNow(); - - try { - Disposable d = s.schedule(() -> { - try { - Thread.sleep(10000); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - - // Dispose twice to test that the executor is returned to the pool only once - d.dispose(); - d.dispose(); - - Thread.sleep(100); - assertThat(((ElasticScheduler)s).cache.size()).isEqualTo(1); - } - finally { - s.dispose(); - } - } -} diff --git a/reactor-core/src/test/java/reactor/core/scheduler/SchedulersTest.java b/reactor-core/src/test/java/reactor/core/scheduler/SchedulersTest.java index a7ae1ea9b0..1bbedcd4e0 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/SchedulersTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/SchedulersTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,28 +60,18 @@ public class SchedulersTest { final static class TestSchedulers implements Schedulers.Factory { - @SuppressWarnings("deprecation") // to be removed with newElastic() in 3.5 - final Scheduler elastic = Schedulers.Factory.super.newElastic(60, Thread::new); final Scheduler boundedElastic = Schedulers.Factory.super.newBoundedElastic(2, Integer.MAX_VALUE, Thread::new, 60); final Scheduler single = Schedulers.Factory.super.newSingle(Thread::new); final Scheduler parallel = Schedulers.Factory.super.newParallel(1, Thread::new); TestSchedulers(boolean disposeOnInit) { if (disposeOnInit) { - elastic.dispose(); boundedElastic.dispose(); single.dispose(); parallel.dispose(); } } - @Override - @SuppressWarnings("deprecation") // to be removed with newElastic() in 3.5 - public final Scheduler newElastic(int ttlSeconds, ThreadFactory threadFactory) { - assertThat(((ReactorThreadFactory)threadFactory).get()).isEqualTo("unused"); - return elastic; - } - @Override public final Scheduler newBoundedElastic(int threadCap, int taskCap, ThreadFactory threadFactory, int ttlSeconds) { assertThat(((ReactorThreadFactory) threadFactory).get()).isEqualTo("unused"); @@ -331,35 +321,6 @@ public void singleSchedulerDefaultNonBlocking() throws InterruptedException { .hasMessageStartingWith("block()/blockFirst()/blockLast() are blocking, which is not supported in thread singleSchedulerDefaultNonBlocking-"); } - @Test - public void elasticSchedulerDefaultBlockingOk() throws InterruptedException { - @SuppressWarnings("deprecation") // to be removed with newElastic() in 3.5 - Scheduler scheduler = Schedulers.newElastic("elasticSchedulerDefaultNonBlocking"); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference errorRef = new AtomicReference<>(); - try { - scheduler.schedule(() -> { - try { - Mono.just("foo") - .hide() - .block(); - } - catch (Throwable t) { - errorRef.set(t); - } - finally { - latch.countDown(); - } - }); - latch.await(); - } - finally { - scheduler.dispose(); - } - - assertThat(errorRef.get()).isNull(); - } - @Test public void boundedElasticSchedulerDefaultBlockingOk() throws InterruptedException { Scheduler scheduler = Schedulers.newBoundedElastic(4, Integer.MAX_VALUE, "boundedElasticSchedulerDefaultNonBlocking"); @@ -541,9 +502,6 @@ public void testOverride() { Schedulers.setFactory(ts); assertThat(Schedulers.newSingle("unused")).isEqualTo(ts.single); - @SuppressWarnings("deprecation") // to be removed with newElastic() in 3.5 - Scheduler elastic = Schedulers.newElastic("unused"); - assertThat(elastic).isEqualTo(ts.elastic); assertThat(Schedulers.newBoundedElastic(4, Integer.MAX_VALUE, "unused")).isEqualTo(ts.boundedElastic); assertThat(Schedulers.newParallel("unused")).isEqualTo(ts.parallel); @@ -584,15 +542,12 @@ public void testShutdownOldOnSetFactory() { @Test public void shutdownNowClosesAllCachedSchedulers() { Scheduler oldSingle = Schedulers.single(); - @SuppressWarnings("deprecation") // to be removed with newElastic() in 3.5 - Scheduler oldElastic = Schedulers.elastic(); Scheduler oldBoundedElastic = Schedulers.boundedElastic(); Scheduler oldParallel = Schedulers.parallel(); Schedulers.shutdownNow(); assertThat(oldSingle.isDisposed()).as("single() disposed").isTrue(); - assertThat(oldElastic.isDisposed()).as("elastic() disposed").isTrue(); assertThat(oldBoundedElastic.isDisposed()).as("boundedElastic() disposed").isTrue(); assertThat(oldParallel.isDisposed()).as("parallel() disposed").isTrue(); } @@ -918,33 +873,6 @@ public void singleSchedulerThreadCheck() throws Exception{ } } - - @Test - @Timeout(5) - public void elasticSchedulerThreadCheck() throws Exception{ - @SuppressWarnings("deprecation") // to be removed with newElastic() in 3.5 - Scheduler s = Schedulers.newElastic("work"); - try { - Scheduler.Worker w = s.createWorker(); - - Thread currentThread = Thread.currentThread(); - AtomicReference taskThread = new AtomicReference<>(currentThread); - CountDownLatch latch = new CountDownLatch(1); - - w.schedule(() -> { - taskThread.set(Thread.currentThread()); - latch.countDown(); - }); - - latch.await(); - - assertThat(taskThread.get()).isNotEqualTo(currentThread); - } - finally { - s.dispose(); - } - } - @Test @Timeout(5) public void boundedElasticSchedulerThreadCheck() throws Exception { @@ -1184,8 +1112,7 @@ public void testDefaultMethods(){ }; - @SuppressWarnings("deprecation") // to be removed in 3.5 alongside Schedulers.elastic() - Scheduler elastic = Schedulers.elastic(); + Scheduler elastic = Schedulers.boundedElastic(); //noop elastic.dispose(); } diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/scheduler/SchedulersMetricsTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/scheduler/SchedulersMetricsTest.java index 3e9f54162f..4037dcee52 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/scheduler/SchedulersMetricsTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/scheduler/SchedulersMetricsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -207,11 +207,6 @@ public void disablingMetricsRemovesSchedulerMeters() { static Stream metricsSchedulers() { return Stream.of( arguments(Named.named("PARALLEL", (Supplier) () -> Schedulers.newParallel("A", 1))), - arguments(Named.named("ELASTIC", (Supplier) () -> { - @SuppressWarnings("deprecation") // To be removed in 3.5 alongside Schedulers.newElastic() - Scheduler newElastic = Schedulers.newElastic("A"); - return newElastic; - })), arguments(Named.named("BOUNDED_ELASTIC", (Supplier) () -> Schedulers.newBoundedElastic(4, Integer.MAX_VALUE, "A"))) ); } diff --git a/reactor-test/src/main/java/reactor/test/scheduler/VirtualTimeScheduler.java b/reactor-test/src/main/java/reactor/test/scheduler/VirtualTimeScheduler.java index 85f408c38e..91a70345d2 100644 --- a/reactor-test/src/main/java/reactor/test/scheduler/VirtualTimeScheduler.java +++ b/reactor-test/src/main/java/reactor/test/scheduler/VirtualTimeScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -448,12 +448,6 @@ static final class AllFactory implements Schedulers.Factory { this.s = s; } - @Override - @SuppressWarnings("deprecation") // To be removed in 3.5.0 - public Scheduler newElastic(int ttlSeconds, ThreadFactory threadFactory) { - return s; - } - @Override public Scheduler newBoundedElastic(int threadCap, int taskCap, ThreadFactory threadFactory, int ttlSeconds) { return s; diff --git a/reactor-test/src/test/java/reactor/test/publisher/ColdTestPublisherTests.java b/reactor-test/src/test/java/reactor/test/publisher/ColdTestPublisherTests.java index 4538cdf550..c9550f86ac 100644 --- a/reactor-test/src/test/java/reactor/test/publisher/ColdTestPublisherTests.java +++ b/reactor-test/src/test/java/reactor/test/publisher/ColdTestPublisherTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -431,7 +431,7 @@ void subscriberIncrementShouldBeVisibleBeforeCompletionPropagated() { cold.next("value"); final int timeout = 2; - StepVerifier.create(cold.mono().subscribeOn(Schedulers.elastic())) + StepVerifier.create(cold.mono().subscribeOn(Schedulers.boundedElastic())) .expectSubscription() .expectNext("value") .expectComplete() diff --git a/reactor-test/src/test/java/reactor/test/scheduler/VirtualTimeSchedulerTests.java b/reactor-test/src/test/java/reactor/test/scheduler/VirtualTimeSchedulerTests.java index e5b72568b2..c97fc33e7e 100644 --- a/reactor-test/src/test/java/reactor/test/scheduler/VirtualTimeSchedulerTests.java +++ b/reactor-test/src/test/java/reactor/test/scheduler/VirtualTimeSchedulerTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,27 +51,18 @@ public void cancelledAndEmptyConstantsAreNotSame() { @Test public void allEnabled() { assertThat(Schedulers.newParallel("")).isNotInstanceOf(VirtualTimeScheduler.class); - @SuppressWarnings("deprecation") // To be removed in 3.5 alongside Schedulers.newElastic - Scheduler elastic1 = Schedulers.newElastic(""); - assertThat(elastic1).isNotInstanceOf(VirtualTimeScheduler.class); assertThat(Schedulers.newBoundedElastic(4, Integer.MAX_VALUE, "")).isNotInstanceOf(VirtualTimeScheduler.class); assertThat(Schedulers.newSingle("")).isNotInstanceOf(VirtualTimeScheduler.class); VirtualTimeScheduler.getOrSet(); assertThat(Schedulers.newParallel("")).isInstanceOf(VirtualTimeScheduler.class); - @SuppressWarnings("deprecation") // To be removed in 3.5 alongside Schedulers.newElastic - Scheduler elastic2 = Schedulers.newElastic(""); - assertThat(elastic2).isInstanceOf(VirtualTimeScheduler.class); assertThat(Schedulers.newBoundedElastic(4, Integer.MAX_VALUE, "")).isInstanceOf(VirtualTimeScheduler.class); assertThat(Schedulers.newSingle("")).isInstanceOf(VirtualTimeScheduler.class); VirtualTimeScheduler t = VirtualTimeScheduler.get(); assertThat(Schedulers.newParallel("")).isSameAs(t); - @SuppressWarnings("deprecation") // To be removed in 3.5 alongside Schedulers.newElastic - Scheduler elastic3 = Schedulers.newElastic(""); - assertThat(elastic3).isSameAs(t); assertThat(Schedulers.newBoundedElastic(5, Integer.MAX_VALUE, "")).isSameAs(t); //same even though different parameter assertThat(Schedulers.newSingle("")).isSameAs(t); } From bf63f7fa5fe6ffe7e0beba18da8d814af9521606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 17 Mar 2022 09:52:10 +0100 Subject: [PATCH 009/312] Have concatMap default to 0 prefetch behavior (#2967) This commit changes the no-parameter variant of concatMap to avoid use of prefetch (similar to calling `concatMap(function, 0)`). concatMapDelayError is also similarly changed. The variants that take a prefetch parameter are kept. Fixes #2599. --- .../java/reactor/core/publisher/Flux.java | 36 +++-- .../publisher/AbstractFluxConcatMapTest.java | 137 +++++++++--------- .../FluxConcatMapNoPrefetchTest.java | 85 ++++++++++- .../core/publisher/FluxConcatMapTest.java | 22 +-- .../core/publisher/FluxDelayUntilTest.java | 4 +- .../core/publisher/FluxIntervalTest.java | 25 ++-- .../core/publisher/FluxWindowWhenTest.java | 4 +- .../core/publisher/scenarios/FluxTests.java | 11 +- 8 files changed, 203 insertions(+), 121 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 4ca9cebb3c..7105a4e56c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -3743,6 +3743,8 @@ public final Mono> collectSortedList(@Nullable Comparator com * *

    * Errors will immediately short circuit current concat backlog. + * Note that no prefetching is done on the source, which gets requested only if there + * is downstream demand. * *

    * @@ -3754,9 +3756,8 @@ public final Mono> collectSortedList(@Nullable Comparator com * * @return a concatenated {@link Flux} */ - public final Flux concatMap(Function> - mapper) { - return concatMap(mapper, Queues.XS_BUFFER_SIZE); + public final Flux concatMap(Function> mapper) { + return onAssembly(new FluxConcatMapNoPrefetch<>(this, mapper, FluxConcatMap.ErrorMode.IMMEDIATE)); } /** @@ -3778,7 +3779,8 @@ public final Flux concatMap(Function * Errors will immediately short circuit current concat backlog. The prefetch argument - * allows to give an arbitrary prefetch size to the upstream source. + * allows to give an arbitrary prefetch size to the upstream source, or to disable + * prefetching with {@code 0}. * *

    * @@ -3786,13 +3788,12 @@ public final Flux concatMap(FunctionDiscard Support: This operator discards elements it internally queued for backpressure upon cancellation. * * @param mapper the function to transform this sequence of T into concatenated sequences of V - * @param prefetch the number of values to prefetch from upstream source (set it to 0 if you don't want it to prefetch) + * @param prefetch the number of values to prefetch from upstream source, or {@code 0} to disable prefetching * @param the produced concatenated type * * @return a concatenated {@link Flux} */ - public final Flux concatMap(Function> - mapper, int prefetch) { + public final Flux concatMap(Function> mapper, int prefetch) { if (prefetch == 0) { return onAssembly(new FluxConcatMapNoPrefetch<>(this, mapper, FluxConcatMap.ErrorMode.IMMEDIATE)); } @@ -3821,6 +3822,8 @@ public final Flux concatMap(Function * @@ -3834,7 +3837,7 @@ public final Flux concatMap(Function Flux concatMapDelayError(Function> mapper) { - return concatMapDelayError(mapper, Queues.XS_BUFFER_SIZE); + return concatMapDelayError(mapper, 0); } /** @@ -3858,7 +3861,8 @@ public final Flux concatMapDelayError(Function * @@ -3866,14 +3870,13 @@ public final Flux concatMapDelayError(FunctionDiscard Support: This operator discards elements it internally queued for backpressure upon cancellation. * * @param mapper the function to transform this sequence of T into concatenated sequences of V - * @param prefetch the number of values to prefetch from upstream source + * @param prefetch the number of values to prefetch from upstream source, or {@code 0} to disable prefetching * @param the produced concatenated type * * @return a concatenated {@link Flux} * */ - public final Flux concatMapDelayError(Function> mapper, int prefetch) { + public final Flux concatMapDelayError(Function> mapper, int prefetch) { return concatMapDelayError(mapper, true, prefetch); } @@ -3897,7 +3900,8 @@ public final Flux concatMapDelayError(Function * Errors in the individual publishers will be delayed after the current concat * backlog if delayUntilEnd is false or after all sources if delayUntilEnd is true. - * The prefetch argument allows to give an arbitrary prefetch size to the upstream source. + * The prefetch argument allows to give an arbitrary prefetch size to the upstream source, + * or to disable prefetching with {@code 0}. * *

    * @@ -3907,14 +3911,14 @@ public final Flux concatMapDelayError(Function the produced concatenated type * * @return a concatenated {@link Flux} * */ - public final Flux concatMapDelayError(Function> mapper, boolean delayUntilEnd, int prefetch) { + public final Flux concatMapDelayError(Function> mapper, + boolean delayUntilEnd, int prefetch) { FluxConcatMap.ErrorMode errorMode = delayUntilEnd ? FluxConcatMap.ErrorMode.END : FluxConcatMap.ErrorMode.BOUNDARY; if (prefetch == 0) { return onAssembly(new FluxConcatMapNoPrefetch<>(this, mapper, errorMode)); diff --git a/reactor-core/src/test/java/reactor/core/publisher/AbstractFluxConcatMapTest.java b/reactor-core/src/test/java/reactor/core/publisher/AbstractFluxConcatMapTest.java index 774a747d60..14bbc18e43 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/AbstractFluxConcatMapTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/AbstractFluxConcatMapTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,37 +37,38 @@ @Timeout(5) public abstract class AbstractFluxConcatMapTest extends FluxOperatorTest { - abstract int implicitPrefetchValue(); + abstract int testBasePrefetchValue(); @Override protected final Scenario defaultScenarioOptions(Scenario defaultOptions) { return defaultOptions.shouldHitDropNextHookAfterTerminate(false) - .shouldHitDropErrorHookAfterTerminate(false) - .prefetch(implicitPrefetchValue() == 0 ? -1 : implicitPrefetchValue()); + .shouldHitDropErrorHookAfterTerminate(false) + .prefetch(testBasePrefetchValue() == 0 ? -1 : testBasePrefetchValue()) + .producerError(new RuntimeException("AbstractFluxConcatMapTest")); } @Override protected List> scenarios_operatorSuccess() { return Arrays.asList( - scenario(f -> f.concatMap(Flux::just, implicitPrefetchValue())), + scenario(f -> f.concatMap(Flux::just, testBasePrefetchValue())), - scenario(f -> f.concatMap(d -> Flux.just(d).hide(), implicitPrefetchValue())), + scenario(f -> f.concatMap(d -> Flux.just(d).hide(), testBasePrefetchValue())), - scenario(f -> f.concatMap(d -> Flux.empty(), implicitPrefetchValue())) + scenario(f -> f.concatMap(d -> Flux.empty(), testBasePrefetchValue())) .receiverEmpty(), - scenario(f -> f.concatMapDelayError(Flux::just, implicitPrefetchValue())), + scenario(f -> f.concatMapDelayError(Flux::just, testBasePrefetchValue())), - scenario(f -> f.concatMapDelayError(d -> Flux.just(d).hide(), implicitPrefetchValue())), + scenario(f -> f.concatMapDelayError(d -> Flux.just(d).hide(), testBasePrefetchValue())), - scenario(f -> f.concatMapDelayError(d -> Flux.empty(), implicitPrefetchValue())) + scenario(f -> f.concatMapDelayError(d -> Flux.empty(), testBasePrefetchValue())) .receiverEmpty(), //scenarios with fromCallable(() -> null) - scenario(f -> f.concatMap(d -> Mono.fromCallable(() -> null), implicitPrefetchValue())) + scenario(f -> f.concatMap(d -> Mono.fromCallable(() -> null), testBasePrefetchValue())) .receiverEmpty(), - scenario(f -> f.concatMapDelayError(d -> Mono.fromCallable(() -> null), implicitPrefetchValue())) + scenario(f -> f.concatMapDelayError(d -> Mono.fromCallable(() -> null), testBasePrefetchValue())) .shouldHitDropErrorHookAfterTerminate(true) .receiverEmpty() ); @@ -76,9 +77,9 @@ protected List> scenarios_operatorSuccess() { @Override protected List> scenarios_errorFromUpstreamFailure() { return Arrays.asList( - scenario(f -> f.concatMap(Flux::just, implicitPrefetchValue())), + scenario(f -> f.concatMap(Flux::just, testBasePrefetchValue())), - scenario(f -> f.concatMapDelayError(Flux::just, implicitPrefetchValue())) + scenario(f -> f.concatMapDelayError(Flux::just, testBasePrefetchValue())) .shouldHitDropErrorHookAfterTerminate(true) ); } @@ -88,16 +89,16 @@ protected List> scenarios_operatorError() { return Arrays.asList( scenario(f -> f.concatMap(d -> { throw exception(); - }, implicitPrefetchValue())), + }, testBasePrefetchValue())), - scenario(f -> f.concatMap(d -> null, implicitPrefetchValue())), + scenario(f -> f.concatMap(d -> null, testBasePrefetchValue())), scenario(f -> f.concatMapDelayError(d -> { throw exception(); - }, implicitPrefetchValue())) + }, testBasePrefetchValue())) .shouldHitDropErrorHookAfterTerminate(true), - scenario(f -> f.concatMapDelayError(d -> null, implicitPrefetchValue())) + scenario(f -> f.concatMapDelayError(d -> null, testBasePrefetchValue())) .shouldHitDropErrorHookAfterTerminate(true) ); } @@ -107,7 +108,7 @@ public void normal() { AssertSubscriber ts = AssertSubscriber.create(); Flux.range(1, 2) - .concatMap(v -> Flux.range(v, 2), implicitPrefetchValue()) + .concatMap(v -> Flux.range(v, 2), testBasePrefetchValue()) .subscribe(ts); ts.assertValues(1, 2, 2, 3) @@ -121,7 +122,7 @@ public void normal2() { Flux.range(1, 2) .hide() - .concatMap(v -> Flux.range(v, 2), implicitPrefetchValue()) + .concatMap(v -> Flux.range(v, 2), testBasePrefetchValue()) .subscribe(ts); ts.assertValues(1, 2, 2, 3) @@ -134,7 +135,7 @@ public void normalBoundary() { AssertSubscriber ts = AssertSubscriber.create(); Flux.range(1, 2) - .concatMapDelayError(v -> Flux.range(v, 2), implicitPrefetchValue()) + .concatMapDelayError(v -> Flux.range(v, 2), testBasePrefetchValue()) .subscribe(ts); ts.assertValues(1, 2, 2, 3) @@ -148,7 +149,7 @@ public void normalBoundary2() { Flux.range(1, 2) .hide() - .concatMapDelayError(v -> Flux.range(v, 2), implicitPrefetchValue()) + .concatMapDelayError(v -> Flux.range(v, 2), testBasePrefetchValue()) .subscribe(ts); ts.assertValues(1, 2, 2, 3) @@ -161,7 +162,7 @@ public void normalLongRun() { AssertSubscriber ts = AssertSubscriber.create(); Flux.range(1, 1000) - .concatMap(v -> Flux.range(v, 1000), implicitPrefetchValue()) + .concatMap(v -> Flux.range(v, 1000), testBasePrefetchValue()) .subscribe(ts); ts.assertValueCount(1_000_000) @@ -174,7 +175,7 @@ public void normalLongRunJust() { AssertSubscriber ts = AssertSubscriber.create(); Flux.range(1, 1000_000) - .concatMap(v -> Flux.just(v), implicitPrefetchValue()) + .concatMap(v -> Flux.just(v), testBasePrefetchValue()) .subscribe(ts); ts.assertValueCount(1_000_000) @@ -188,7 +189,7 @@ public void normalLongRun2() { Flux.range(1, 1000) .hide() - .concatMap(v -> Flux.range(v, 1000), implicitPrefetchValue()) + .concatMap(v -> Flux.range(v, 1000), testBasePrefetchValue()) .subscribe(ts); ts.assertValueCount(1_000_000) @@ -201,7 +202,7 @@ public void normalLongRunBoundary() { AssertSubscriber ts = AssertSubscriber.create(); Flux.range(1, 1000) - .concatMapDelayError(v -> Flux.range(v, 1000), implicitPrefetchValue()) + .concatMapDelayError(v -> Flux.range(v, 1000), testBasePrefetchValue()) .subscribe(ts); ts.assertValueCount(1_000_000) @@ -214,7 +215,7 @@ public void normalLongRunJustBoundary() { AssertSubscriber ts = AssertSubscriber.create(); Flux.range(1, 1000_000) - .concatMapDelayError(v -> Flux.just(v), implicitPrefetchValue()) + .concatMapDelayError(v -> Flux.just(v), testBasePrefetchValue()) .subscribe(ts); ts.assertValueCount(1_000_000) @@ -228,7 +229,7 @@ public void normalLongRunBoundary2() { Flux.range(1, 1000) .hide() - .concatMapDelayError(v -> Flux.range(v, 1000), implicitPrefetchValue()) + .concatMapDelayError(v -> Flux.range(v, 1000), testBasePrefetchValue()) .subscribe(ts); ts.assertValueCount(1_000_000) @@ -242,7 +243,7 @@ public void boundaryFusion() { Flux.range(1, 10000) .publishOn(Schedulers.single()) .map(t -> Thread.currentThread().getName().contains("single-") ? "single" : ("BAD-" + t + Thread.currentThread().getName())) - .concatMap(Flux::just, implicitPrefetchValue()) + .concatMap(Flux::just, testBasePrefetchValue()) .publishOn(Schedulers.boundedElastic()) .distinct() .as(StepVerifier::create) @@ -258,7 +259,7 @@ public void boundaryFusionDelayError() { Flux.range(1, 10000) .publishOn(Schedulers.single()) .map(t -> Thread.currentThread().getName().contains("single-") ? "single" : ("BAD-" + t + Thread.currentThread().getName())) - .concatMapDelayError(Flux::just, implicitPrefetchValue()) + .concatMapDelayError(Flux::just, testBasePrefetchValue()) .publishOn(Schedulers.boundedElastic()) .distinct() .as(StepVerifier::create) @@ -277,7 +278,7 @@ public void singleSubscriberOnlyBoundary() { Sinks.Many source1 = Sinks.unsafe().many().multicast().directBestEffort(); Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); - source.asFlux().concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), implicitPrefetchValue()) + source.asFlux().concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -316,7 +317,7 @@ public void mainErrorsImmediate() { Sinks.Many source1 = Sinks.unsafe().many().multicast().directBestEffort(); Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); - source.asFlux().concatMap(v -> v == 1 ? source1.asFlux() : source2.asFlux(), implicitPrefetchValue()) + source.asFlux().concatMap(v -> v == 1 ? source1.asFlux() : source2.asFlux(), testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -350,7 +351,7 @@ public void mainErrorsBoundary() { Sinks.Many source1 = Sinks.unsafe().many().multicast().directBestEffort(); Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); - source.asFlux().concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), implicitPrefetchValue()) + source.asFlux().concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -391,7 +392,7 @@ public void innerErrorsImmediate() { Sinks.Many source1 = Sinks.unsafe().many().multicast().directBestEffort(); Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); - source.asFlux().concatMap(v -> v == 1 ? source1.asFlux() : source2.asFlux(), implicitPrefetchValue()) + source.asFlux().concatMap(v -> v == 1 ? source1.asFlux() : source2.asFlux(), testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -422,7 +423,7 @@ public void syncFusionMapToNull() { Flux.range(1, 2) .map(v -> v == 2 ? null : v) - .concatMap(Flux::just, implicitPrefetchValue()) + .concatMap(Flux::just, testBasePrefetchValue()) .subscribe(ts); ts.assertValues(1) @@ -437,7 +438,7 @@ public void syncFusionMapToNullFilter() { Flux.range(1, 2) .map(v -> v == 2 ? null : v) .filter(v -> true) - .concatMap(Flux::just, implicitPrefetchValue()) + .concatMap(Flux::just, testBasePrefetchValue()) .subscribe(ts); ts.assertValues(1) @@ -456,7 +457,7 @@ public void asyncFusionMapToNull() { up.asFlux() .map(v -> v == 2 ? null : v) - .concatMap(Flux::just, implicitPrefetchValue()) + .concatMap(Flux::just, testBasePrefetchValue()) .subscribe(ts); ts.assertValues(1) @@ -477,7 +478,7 @@ public void asyncFusionMapToNullFilter() { up.asFlux() .map(v -> v == 2 ? null : v) .filter(v -> true) - .concatMap(Flux::just, implicitPrefetchValue()) + .concatMap(Flux::just, testBasePrefetchValue()) .subscribe(ts); ts.assertValues(1) @@ -493,7 +494,7 @@ public void scalarAndRangeBackpressured() { new Publisher[]{Flux.just(1), Flux.range(2, 3)}; Flux.range(0, 2) - .concatMap(v -> sources[v], implicitPrefetchValue()) + .concatMap(v -> sources[v], testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -511,7 +512,7 @@ public void publisherOfPublisherDelayError2() { StepVerifier.create(Flux.just(Flux.just(1, 2) .concatWith(Flux.error(new Exception("test"))), Flux.just(3, 4)) - .concatMap(f -> f, implicitPrefetchValue())) + .concatMap(f -> f, testBasePrefetchValue())) .expectNext(1, 2) .verifyErrorMessage("test"); } @@ -524,7 +525,7 @@ public void concatDelayErrorWithFluxError() { Flux.just( Flux.just(1, 2), Flux.error(new Exception("test")), - Flux.just(3, 4)), implicitPrefetchValue())) + Flux.just(3, 4)), testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyErrorMessage("test"); } @@ -537,7 +538,7 @@ public void concatDelayErrorWithMonoError() { Mono.error(new Exception("test")), Flux.just(3, 4) ); - StepVerifier.create(Flux.concatDelayError(sources, implicitPrefetchValue())) + StepVerifier.create(Flux.concatDelayError(sources, testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyErrorMessage("test"); } @@ -547,7 +548,7 @@ public void errorModeContinueNullPublisher() { Flux test = Flux .just(1, 2) .hide() - .concatMap(f -> null, implicitPrefetchValue()) + .concatMap(f -> null, testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); StepVerifier.create(test) @@ -570,7 +571,7 @@ public void errorModeContinueInternalError() { else { return Mono.just(f); } - }, implicitPrefetchValue()) + }, testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); StepVerifier.create(test) @@ -594,7 +595,7 @@ public void errorModeContinueInternalErrorHidden() { else { return Mono.just(f); } - }, implicitPrefetchValue()) + }, testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); StepVerifier.create(test) @@ -615,7 +616,7 @@ public void errorModeContinueWithCallable() { if(f == 1) { throw new ArithmeticException("boom"); } - }), implicitPrefetchValue()) + }), testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); StepVerifier.create(test) @@ -638,7 +639,7 @@ public void errorModeContinueDelayErrors() { else { return Mono.just(f); } - }, implicitPrefetchValue()) + }, testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); @@ -664,7 +665,7 @@ public void errorModeContinueDelayErrorsWithCallable() { else { return Mono.just(f); } - }, implicitPrefetchValue()) + }, testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); @@ -682,7 +683,7 @@ public void errorModeContinueInternalErrorStopStrategy() { Flux test = Flux .just(0, 1) .hide() - .concatMap(f -> Flux.range(f, 1).map(i -> 1/i).onErrorStop(), implicitPrefetchValue()) + .concatMap(f -> Flux.range(f, 1).map(i -> 1/i).onErrorStop(), testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); StepVerifier.create(test) @@ -699,7 +700,7 @@ public void errorModeContinueInternalErrorStopStrategyAsync() { Flux test = Flux .just(0, 1) .hide() - .concatMap(f -> Flux.range(f, 1).publishOn(Schedulers.parallel()).map(i -> 1 / i).onErrorStop(), implicitPrefetchValue()) + .concatMap(f -> Flux.range(f, 1).publishOn(Schedulers.parallel()).map(i -> 1 / i).onErrorStop(), testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); StepVerifier.create(test) @@ -716,7 +717,7 @@ public void errorModeContinueInternalErrorMono() { Flux test = Flux .just(0, 1) .hide() - .concatMap(f -> Mono.just(f).map(i -> 1/i), implicitPrefetchValue()) + .concatMap(f -> Mono.just(f).map(i -> 1/i), testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); StepVerifier.create(test) @@ -733,7 +734,7 @@ public void errorModeContinueInternalErrorMonoAsync() { Flux test = Flux .just(0, 1) .hide() - .concatMap(f -> Mono.just(f).publishOn(Schedulers.parallel()).map(i -> 1/i), implicitPrefetchValue()) + .concatMap(f -> Mono.just(f).publishOn(Schedulers.parallel()).map(i -> 1/i), testBasePrefetchValue()) .onErrorContinue(OnNextFailureStrategyTest::drop); StepVerifier.create(test) @@ -748,7 +749,7 @@ public void errorModeContinueInternalErrorMonoAsync() { @Test public void discardOnDrainMapperError() { StepVerifier.create(Flux.just(1, 2, 3) - .concatMap(i -> { throw new IllegalStateException("boom"); }, implicitPrefetchValue())) + .concatMap(i -> { throw new IllegalStateException("boom"); }, testBasePrefetchValue())) .expectErrorMessage("boom") .verifyThenAssertThat() .hasDiscardedExactly(1); @@ -757,7 +758,7 @@ public void discardOnDrainMapperError() { @Test public void discardDelayedOnDrainMapperError() { StepVerifier.create(Flux.just(1, 2, 3) - .concatMapDelayError(i -> { throw new IllegalStateException("boom"); }, implicitPrefetchValue())) + .concatMapDelayError(i -> { throw new IllegalStateException("boom"); }, testBasePrefetchValue())) .expectErrorMessage("boom") .verifyThenAssertThat() .hasDiscardedExactly(1, 2, 3); @@ -774,7 +775,7 @@ public void discardDelayedInnerOnDrainMapperError() { return Mono.just(i); }, false, - implicitPrefetchValue() + testBasePrefetchValue() )) .expectNext(1) .expectErrorMessage("boom") @@ -785,7 +786,7 @@ public void discardDelayedInnerOnDrainMapperError() { @Test public void publisherOfPublisherDelayEnd() { StepVerifier.create(Flux.concatDelayError(Flux.just(Flux.just(1, 2), - Flux.just(3, 4)), false, implicitPrefetchValue())) + Flux.just(3, 4)), false, testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyComplete(); } @@ -793,7 +794,7 @@ public void publisherOfPublisherDelayEnd() { @Test public void publisherOfPublisherDelayEnd2() { StepVerifier.create(Flux.concatDelayError(Flux.just(Flux.just(1, 2), - Flux.just(3, 4)), true, implicitPrefetchValue())) + Flux.just(3, 4)), true, testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyComplete(); } @@ -803,7 +804,7 @@ public void publisherOfPublisherDelayEndNot3() { StepVerifier.create(Flux.just(Flux.just(1, 2) .concatWith(Flux.error(new Exception("test"))), Flux.just(3, 4)) - .concatMapDelayError(f -> f, false, implicitPrefetchValue())) + .concatMapDelayError(f -> f, false, testBasePrefetchValue())) .expectNext(1, 2) .verifyErrorMessage("test"); } @@ -813,7 +814,7 @@ public void publisherOfPublisherDelayEndError() { StepVerifier.create(Flux.concatDelayError(Flux.just(Flux.just(1, 2) .concatWith(Flux.error(new Exception( "test"))), - Flux.just(3, 4)), false, implicitPrefetchValue())) + Flux.just(3, 4)), false, testBasePrefetchValue())) .expectNext(1, 2) .verifyErrorMessage("test"); } @@ -823,7 +824,7 @@ public void publisherOfPublisherDelayEndError2() { StepVerifier.create(Flux.concatDelayError(Flux.just(Flux.just(1, 2) .concatWith(Flux.error(new Exception( "test"))), - Flux.just(3, 4)), true, implicitPrefetchValue())) + Flux.just(3, 4)), true, testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyErrorMessage("test"); } @@ -833,7 +834,7 @@ public void publisherOfPublisherDelayEnd3() { StepVerifier.create(Flux.just(Flux.just(1, 2) .concatWith(Flux.error(new Exception("test"))), Flux.just(3, 4)) - .concatMapDelayError(f -> f, true, implicitPrefetchValue())) + .concatMapDelayError(f -> f, true, testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyErrorMessage("test"); } @@ -848,7 +849,7 @@ public void innerErrorsBoundary() { Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); //gh-1101: default changed from BOUNDARY to END - source.asFlux().concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), false, implicitPrefetchValue()) + source.asFlux().concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), false, testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -882,7 +883,7 @@ public void innerErrorsEnd() { Sinks.Many source1 = Sinks.unsafe().many().multicast().directBestEffort(); Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); - source.asFlux().concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), true, implicitPrefetchValue()) + source.asFlux().concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), true, testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -919,7 +920,7 @@ public void innerErrorsEnd() { @Test public void publisherOfPublisherDelay() { StepVerifier.create(Flux.concatDelayError(Flux.just(Flux.just(1, 2), - Flux.just(3, 4)), implicitPrefetchValue())) + Flux.just(3, 4)), testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyComplete(); } @@ -927,7 +928,7 @@ public void publisherOfPublisherDelay() { @Test public void publisherOfPublisherDelayError() { StepVerifier.create(Flux.concatDelayError(Flux.just(Flux.just(1, 2).concatWith(Flux.error(new Exception("test"))), - Flux.just(3, 4)), implicitPrefetchValue())) + Flux.just(3, 4)), testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyErrorMessage("test"); } @@ -940,7 +941,7 @@ public void concatMapDelayErrorWithFluxError() { Flux.just(1, 2), Flux.error(new Exception("test")), Flux.just(3, 4)) - .concatMapDelayError(f -> f, true, implicitPrefetchValue())) + .concatMapDelayError(f -> f, true, testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyErrorMessage("test"); } @@ -953,14 +954,14 @@ public void concatMapDelayErrorWithMonoError() { Flux.just(1, 2), Mono.error(new Exception("test")), Flux.just(3, 4)) - .concatMapDelayError(f -> f, true, implicitPrefetchValue())) + .concatMapDelayError(f -> f, true, testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyErrorMessage("test"); } @Test public void publisherOfPublisher() { - StepVerifier.create(Flux.concat(Flux.just(Flux.just(1, 2), Flux.just(3, 4)), implicitPrefetchValue())) + StepVerifier.create(Flux.concat(Flux.just(Flux.just(1, 2), Flux.just(3, 4)), testBasePrefetchValue())) .expectNext(1, 2, 3, 4) .verifyComplete(); } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxConcatMapNoPrefetchTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxConcatMapNoPrefetchTest.java index 98510569e6..26eaf0705d 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxConcatMapNoPrefetchTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxConcatMapNoPrefetchTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,24 +18,47 @@ import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Scannable; import reactor.test.StepVerifier; +import reactor.test.subscriber.AssertSubscriber; import static org.assertj.core.api.Assertions.assertThat; +import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST; -public class FluxConcatMapNoPrefetchTest extends AbstractFluxConcatMapTest { +class FluxConcatMapNoPrefetchTest extends AbstractFluxConcatMapTest { @Override - int implicitPrefetchValue() { + int testBasePrefetchValue() { return 0; } @Test - public void noRequestBeforeOnCompleteWithZeroPrefetch() { + void concatMapWithoutPrefetchArgument() { + assertThat(Flux.empty().concatMap(v -> Mono.empty())) + .isInstanceOf(FluxConcatMapNoPrefetch.class) + .satisfies(pub -> { + assertThat(Scannable.from(pub).scan(Scannable.Attr.PREFETCH)).as("scanned PREFETCH").isZero(); + assertThat(pub.getPrefetch()).as("getPrefetch()").isZero(); + }); + } + + @Test + void concatMapDelayErrorWithoutPrefetchArgument() { + assertThat(Flux.empty().concatMapDelayError(v -> Mono.empty())) + .isInstanceOf(FluxConcatMapNoPrefetch.class) + .satisfies(pub -> { + assertThat(Scannable.from(pub).scan(Scannable.Attr.PREFETCH)).as("scanned PREFETCH").isZero(); + assertThat(pub.getPrefetch()).as("getPrefetch()").isZero(); + }); + } + + @Test + void noRequestBeforeOnCompleteWithZeroPrefetch() { AtomicBoolean firstCompleted = new AtomicBoolean(false); Flux .generate(() -> 0, (i, sink) -> { @@ -76,7 +99,57 @@ public void noRequestBeforeOnCompleteWithZeroPrefetch() { } @Test - public void scanOperator(){ + void singleSubscriberOnly() { + AssertSubscriber ts = AssertSubscriber.create(); + + Sinks.Many source = Sinks.unsafe().many().multicast().directBestEffort(); + + Sinks.Many source1 = Sinks.unsafe().many().multicast().directBestEffort(); + Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); + + AtomicLong upstreamRequest = new AtomicLong(); + + source.asFlux() + .doOnRequest(l -> { + if (l == Long.MAX_VALUE) upstreamRequest.set(-2L); + else upstreamRequest.addAndGet(l); + }) + .concatMap(v -> v == 1 ? source1.asFlux() : source2.asFlux()) + .subscribe(ts); + + ts.assertNoValues() + .assertNoError() + .assertNotComplete(); + + assertThat(upstreamRequest).as("upstream before 1 value").hasValue(1); + source.tryEmitNext(1).orThrow(); + //FluxConcatMapNoPrefetch doesn't request more than 1 from upstream at a time + + assertThat(source1.currentSubscriberCount()).as("source1 has subscriber").isPositive(); + assertThat(source2.currentSubscriberCount()).as("source2 has subscriber").isZero(); + + source1.tryEmitNext(10).orThrow(); + //using an emit below would terminate the sink with an error + assertThat(source2.tryEmitNext(200)) + .as("early emit in source2") + .isEqualTo(Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER); + + source1.tryEmitComplete().orThrow(); + //now that source1 has completed, upstream will be requested of one more + assertThat(upstreamRequest).as("upstream after source1 completion").hasValue(2); + source.tryEmitNext(2).orThrow(); + source.emitComplete(FAIL_FAST); + + source2.tryEmitNext(20).orThrow(); + source2.tryEmitComplete().orThrow(); + + ts.assertValues(10, 20) + .assertNoError() + .assertComplete(); + } + + @Test + void scanOperator(){ Flux parent = Flux.just(1, 2); FluxConcatMapNoPrefetch test = new FluxConcatMapNoPrefetch<>(parent, i -> Flux.just(i.toString()) , FluxConcatMap.ErrorMode.END); @@ -86,7 +159,7 @@ public void scanOperator(){ } @Test - public void scanConcatMapNoPrefetchDelayError() { + void scanConcatMapNoPrefetchDelayError() { CoreSubscriber actual = new LambdaSubscriber<>(null, e -> {}, null, null); FluxConcatMapNoPrefetch.FluxConcatMapNoPrefetchSubscriber test = new FluxConcatMapNoPrefetch.FluxConcatMapNoPrefetchSubscriber<>(actual, Flux::just, FluxConcatMap.ErrorMode.END); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxConcatMapTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxConcatMapTest.java index 405fe79f2e..77e5087913 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxConcatMapTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxConcatMapTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ public class FluxConcatMapTest extends AbstractFluxConcatMapTest { @Override - int implicitPrefetchValue() { + int testBasePrefetchValue() { return Queues.XS_BUFFER_SIZE; } @@ -117,7 +117,7 @@ public void singleSubscriberOnly() { Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); source.asFlux() - .concatMap(v -> v == 1 ? source1.asFlux() : source2.asFlux()) + .concatMap(v -> v == 1 ? source1.asFlux() : source2.asFlux(), testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -157,7 +157,7 @@ public void singleSubscriberOnlyBoundary() { Sinks.Many source2 = Sinks.unsafe().many().multicast().directBestEffort(); source.asFlux() - .concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux()) + .concatMapDelayError(v -> v == 1 ? source1.asFlux() : source2.asFlux(), testBasePrefetchValue()) .subscribe(ts); ts.assertNoValues() @@ -373,7 +373,7 @@ public void discardOnError() { //also tests WeakScalar StepVerifier.create(Flux.just(1, 2, 3) .concatWith(Mono.error(new IllegalStateException("boom"))) - .concatMap(i -> Mono.just("value" + i)), + .concatMap(i -> Mono.just("value" + i), testBasePrefetchValue()), 0) .expectErrorMessage("boom") .verifyThenAssertThat() @@ -389,7 +389,7 @@ public void discardOnInnerErrorWithPrefetch() { } else { return Mono.defer(() -> Mono.just("value" + i)); } - })) + }, testBasePrefetchValue())) .expectNext("value1") .expectErrorMessage("boom: value2") .verifyThenAssertThat() @@ -405,7 +405,7 @@ public void discardOnInnerErrorCallableWithPrefetch() { } else { return Mono.just("value" + i); } - })) + }, testBasePrefetchValue())) .expectNext("value1") .expectErrorMessage("boom: value2") .verifyThenAssertThat() @@ -421,7 +421,7 @@ public void discardDelayedOnInnerErrorWithPrefetch() { } else { return Mono.defer(() -> Mono.just("value" + i)); } - })) + }, testBasePrefetchValue())) .expectNext("value1") .expectNext("value3") .expectErrorMessage("boom: value2") @@ -438,7 +438,7 @@ public void discardDelayedOnCallableInnerErrorWithPrefetch() { } else { return Mono.just("value" + i); } - })) + }, testBasePrefetchValue())) .expectNext("value1") .expectNext("value3") .expectErrorMessage("boom: value2") @@ -449,7 +449,7 @@ public void discardDelayedOnCallableInnerErrorWithPrefetch() { @Test public void discardOnCancel() { StepVerifier.create(Flux.just(1, 2, 3) - .concatMap(i -> Mono.just("value" + i), implicitPrefetchValue()), + .concatMap(i -> Mono.just("value" + i), testBasePrefetchValue()), 0) .thenCancel() .verifyThenAssertThat() @@ -482,7 +482,7 @@ public void discardDelayedOnNextQueueReject() { @Override public void discardDelayedOnDrainMapperError() { StepVerifier.create(Flux.just(1, 2, 3) - .concatMapDelayError(i -> { throw new IllegalStateException("boom"); })) + .concatMapDelayError(i -> { throw new IllegalStateException("boom"); }, testBasePrefetchValue())) .expectErrorMessage("boom") .verifyThenAssertThat() .hasDiscardedExactly(1); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDelayUntilTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDelayUntilTest.java index 36dd5ad254..34cec4f47c 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDelayUntilTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDelayUntilTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -210,7 +210,7 @@ public void immediateCancel() { @Test public void isAlias() { assertThat(Flux.range(1, 10).delayUntil(a -> Mono.empty())) - .isInstanceOf(FluxConcatMap.class); + .isInstanceOf(FluxConcatMapNoPrefetch.class); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxIntervalTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxIntervalTest.java index 38b32f3bc9..ab9eaa095f 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxIntervalTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxIntervalTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; import reactor.core.Scannable; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; @@ -186,15 +187,19 @@ public void scanIntervalRunnable() { } - @Test - public void tickOverflow() { - StepVerifier.withVirtualTime(() -> - Flux.interval(Duration.ofMillis(50)) - .delayUntil(i -> Mono.delay(Duration.ofMillis(250)))) - .thenAwait(Duration.ofMinutes(1)) - .expectNextCount(6) - .verifyErrorMessage("Could not emit tick 32 due to lack of requests (interval doesn't support small downstream requests that replenish slower than the ticks)"); - } + @Test + void tickOverflow() { + StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofMillis(50)), 0) + .expectSubscription() + .thenRequest(10) + .thenAwait(Duration.ofMillis(550)) + .expectNextCount(10) + .expectErrorSatisfies(e -> assertThat(e) + .matches(Exceptions::isOverflow) + .hasMessage("Could not emit tick 10 due to lack of requests (interval doesn't support small downstream requests that replenish slower than the ticks)") + ) + .verify(Duration.ofSeconds(1)); + } @Test public void shouldBeAbleToScheduleIntervalsWithLowGranularity() { diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowWhenTest.java index ee46bef4ac..e9e1eb171d 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowWhenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -306,7 +306,7 @@ Flux> scenario_windowWillSubdivideAnInputFluxOverlapTime() { return Flux.just(1, 2, 3, 4, 5, 6, 7, 8) .delayElements(Duration.ofMillis(99)) .window(Duration.ofMillis(300), Duration.ofMillis(200)) - .concatMap(Flux::buffer); + .concatMap(Flux::buffer, 1); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java b/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java index adf77331e2..d897c0b04d 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java +++ b/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,6 +81,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.fail; +import static reactor.core.Exceptions.unwrapMultipleExcludingTracebacks; import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST; public class FluxTests extends AbstractReactorTest { @@ -177,14 +178,12 @@ public void delayErrorConcatMapVsFlatMapTwoErrors() { Flux source = Flux.range(0, 5); - Flux concatMap = source.concatMapDelayError(mapFunction) - .doOnError(t -> concatSuppressed.addAll( - Arrays.asList(t.getSuppressed()))) + Flux concatMap = source.concatMapDelayError(mapFunction, 32) + .doOnError(t -> concatSuppressed.addAll(unwrapMultipleExcludingTracebacks(t))) .materialize() .map(Object::toString); Flux flatMap = source.flatMapDelayError(mapFunction, 2, 32) - .doOnError(t -> flatSuppressed.addAll( - Arrays.asList(t.getSuppressed()))) + .doOnError(t -> flatSuppressed.addAll(unwrapMultipleExcludingTracebacks(t))) .materialize() .map(Object::toString); From 96bb61fc342ef249fef0e74f5eec304992b2cb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 17 Mar 2022 10:39:50 +0100 Subject: [PATCH 010/312] Take(n) now behaves as take(n,true)/limitRequest (#2969) This commit changes the behavior of `take(long)` to limit the request made to upstream like the now deprecated operator `limitRequest`, as documented in the deprecation notice. Most usages of `take(long)` in the codebase have been switched to use `take(long, false)` instead to keep relying on the old behavior. All usages of `limitRequest(n)` have been turned into `take(n)`. Finally, the javadocs, marble diagrams and reference guide have been updated to reflect the new behavior. Fixes #2690. --- docs/asciidoc/apdx-operatorChoice.adoc | 3 +- docs/asciidoc/coreFeatures.adoc | 2 +- docs/asciidoc/debugging.adoc | 15 +- .../java/reactor/core/publisher/Flux.java | 42 ++--- .../java/reactor/core/publisher/Mono.java | 8 +- .../core/publisher/doc-files/marbles/take.svg | 77 --------- .../marbles/takeLimitRequestFalse.svg | 113 +++++-------- .../marbles/takeLimitRequestTrue.svg | 153 ++++++------------ .../reactor/core/publisher/ContextTests.java | 10 +- .../core/publisher/EmitterProcessorTest.java | 6 +- .../publisher/FluxBufferPredicateTest.java | 4 +- .../core/publisher/FluxBufferTest.java | 6 +- .../core/publisher/FluxBufferWhenTest.java | 10 +- .../core/publisher/FluxConcatArrayTest.java | 4 +- .../core/publisher/FluxCreateTest.java | 8 +- .../core/publisher/FluxDelaySequenceTest.java | 8 +- .../core/publisher/FluxDematerializeTest.java | 4 +- .../core/publisher/FluxDistinctTest.java | 4 +- .../FluxDistinctUntilChangedTest.java | 4 +- .../core/publisher/FluxDoFinallyTest.java | 6 +- .../core/publisher/FluxExpandTest.java | 6 +- .../core/publisher/FluxFilterWhenTest.java | 4 +- .../core/publisher/FluxGenerateTest.java | 4 +- .../core/publisher/FluxGroupByTest.java | 8 +- .../core/publisher/FluxIntervalTest.java | 2 +- .../core/publisher/FluxIterableTest.java | 4 +- .../core/publisher/FluxLimitRequestTest.java | 4 +- .../publisher/FluxMergeComparingTest.java | 4 +- .../core/publisher/FluxMergeOrderedTest.java | 4 +- .../FluxOnBackpressureBufferTest.java | 6 +- .../FluxOnBackpressureBufferTimeoutTest.java | 4 +- .../publisher/FluxPublishMulticastTest.java | 4 +- .../core/publisher/FluxPublishTest.java | 4 +- .../core/publisher/FluxRefCountGraceTest.java | 12 +- .../core/publisher/FluxRefCountTest.java | 4 +- .../core/publisher/FluxRepeatTest.java | 4 +- .../core/publisher/FluxRepeatWhenTest.java | 6 +- .../core/publisher/FluxReplayTest.java | 4 +- .../reactor/core/publisher/FluxRetryTest.java | 4 +- .../core/publisher/FluxRetryWhenTest.java | 6 +- .../core/publisher/FluxStreamTest.java | 4 +- .../core/publisher/FluxSwitchOnFirstTest.java | 6 +- .../reactor/core/publisher/FluxTakeTest.java | 8 +- .../core/publisher/FluxUsingWhenTest.java | 22 +-- .../publisher/FluxWindowPredicateTest.java | 12 +- .../core/publisher/MonoCollectListTest.java | 4 +- .../core/publisher/MonoCollectTest.java | 6 +- .../core/publisher/MonoExpandTest.java | 6 +- .../core/publisher/MonoRepeatTest.java | 4 +- .../publisher/MonoStreamCollectorTest.java | 6 +- .../core/publisher/ParallelFluxTest.java | 4 +- .../publisher/SerializedSubscriberTest.java | 4 +- .../publisher/scenarios/FluxSpecTests.java | 8 +- .../core/publisher/scenarios/FluxTests.java | 2 +- .../publisher/scenarios/PopularTagTests.java | 6 +- .../core/scheduler/RejectedExecutionTest.java | 11 +- .../test/java/reactor/guide/GuideTests.java | 17 +- .../publisher/FluxMetricsFuseableTest.java | 4 +- .../core/publisher/FluxMetricsTest.java | 6 +- .../test/DefaultContextExpectationsTest.java | 6 +- .../test/StepVerifierAssertionsTests.java | 12 +- .../java/reactor/test/StepVerifierTests.java | 26 +-- .../publisher/ColdTestPublisherTests.java | 8 +- .../publisher/DefaultTestPublisherTests.java | 10 +- 64 files changed, 313 insertions(+), 474 deletions(-) delete mode 100644 reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/take.svg diff --git a/docs/asciidoc/apdx-operatorChoice.adoc b/docs/asciidoc/apdx-operatorChoice.adoc index beffdb3b23..6195aef5e9 100644 --- a/docs/asciidoc/apdx-operatorChoice.adoc +++ b/docs/asciidoc/apdx-operatorChoice.adoc @@ -172,11 +172,10 @@ I want to deal with: * I want to keep only a subset of the sequence: ** by taking N elements: -*** at the beginning of the sequence: https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#take-long-boolean-[Flux#take(long, true)] +*** at the beginning of the sequence: https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#take-long-[Flux#take(long)] **** ...requesting an unbounded amount from upstream: https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#take-long-boolean-[Flux#take(long, false)] **** ...based on a duration: https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#take-java.time.Duration-[Flux#take(Duration)] **** ...only the first element, as a https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html[Mono]: https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#next--[Flux#next()] -**** ...using https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/org/reactivestreams/Subscription.html#request(long)[request(N)] rather than cancellation: https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#limitRequest-long-[Flux#limitRequest(long)] *** at the end of the sequence: https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#takeLast-int-[Flux#takeLast] *** until a criteria is met (inclusive): https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#takeUntil-java.util.function.Predicate-[Flux#takeUntil] (predicate-based), https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#takeUntilOther-org.reactivestreams.Publisher-[Flux#takeUntilOther] (companion publisher-based) *** while a criteria is met (exclusive): https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#takeWhile-java.util.function.Predicate-[Flux#takeWhile] diff --git a/docs/asciidoc/coreFeatures.adoc b/docs/asciidoc/coreFeatures.adoc index d047a006e7..dda3b969df 100644 --- a/docs/asciidoc/coreFeatures.adoc +++ b/docs/asciidoc/coreFeatures.adoc @@ -739,7 +739,7 @@ Flux.just("foo", "bar") <1> `doFinally` consumes a `SignalType` for the type of termination. <2> Similarly to `finally` blocks, we always record the timing. <3> Here we also increment statistics in case of cancellation only. -<4> `take(1)` cancels after one item is emitted. +<4> `take(1)` requests exactly 1 from upstream, and cancels after one item is emitted. ==== On the other hand, `using` handles the case where a `Flux` is derived from a diff --git a/docs/asciidoc/debugging.adoc b/docs/asciidoc/debugging.adoc index fc040cc7b9..1677729df0 100644 --- a/docs/asciidoc/debugging.adoc +++ b/docs/asciidoc/debugging.adoc @@ -591,7 +591,7 @@ This prints out the following (through the logger's console appender): ==== ---- 10:45:20.200 [main] INFO reactor.Flux.Range.1 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription) <1> -10:45:20.205 [main] INFO reactor.Flux.Range.1 - | request(unbounded) <2> +10:45:20.205 [main] INFO reactor.Flux.Range.1 - | request(3) <2> 10:45:20.205 [main] INFO reactor.Flux.Range.1 - | onNext(1) <3> 10:45:20.205 [main] INFO reactor.Flux.Range.1 - | onNext(2) 10:45:20.205 [main] INFO reactor.Flux.Range.1 - | onNext(3) @@ -611,13 +611,14 @@ characters, the actual event gets printed. Here, we get an `onSubscribe` call, a to the operator-specific implementation. Between square brackets, we get additional information, including whether the operator can be automatically optimized through synchronous or asynchronous fusion. -<2> On the second line, we can see that an unbounded request was propagated up from -downstream. +<2> On the second line, we can see that take limited the request to upstream to 3. <3> Then the range sends three values in a row. <4> On the last line, we see `cancel()`. ==== -The last line, (4), is the most interesting. We can see the `take` in action there. It -operates by cutting the sequence short after it has seen enough elements emitted. In -short, `take()` causes the source to `cancel()` once it has emitted the user-requested -amount. +The second (2) and last lines (4) are the most interesting. We can see the `take` in action there. +It leverages backpressure in order to ask the source for exactly the expected amount of elements. +After having received enough elements, it tells the source no more items will be needed by calling `cancel()`. +Note that if downstream had itself used backpressure, eg. by requesting only 1 element, +the `take` operator would have honored that (it _caps_ the request when propagating it from downstream +to upstream). diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 7105a4e56c..4c5c9f9a79 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -6001,7 +6001,7 @@ public final Mono last(T defaultValue) { * Typically used for scenarios where consumer(s) request a large amount of data * (eg. {@code Long.MAX_VALUE}) but the data source behaves better or can be optimized * with smaller requests (eg. database paging, etc...). All data is still processed, - * unlike with {@link #limitRequest(long)} which will cap the grand total request + * unlike with {@link #take(long)} which will cap the grand total request * amount. *

    * Equivalent to {@code flux.publishOn(Schedulers.immediate(), prefetchRate).subscribe() }. @@ -6015,7 +6015,7 @@ public final Mono last(T defaultValue) { * * @return a {@link Flux} limiting downstream's backpressure * @see #publishOn(Scheduler, int) - * @see #limitRequest(long) + * @see #take(long) */ public final Flux limitRate(int prefetchRate) { return onAssembly(this.publishOn(Schedulers.immediate(), prefetchRate)); @@ -6033,7 +6033,7 @@ public final Flux limitRate(int prefetchRate) { * Typically used for scenarios where consumer(s) request a large amount of data * (eg. {@code Long.MAX_VALUE}) but the data source behaves better or can be optimized * with smaller requests (eg. database paging, etc...). All data is still processed, - * unlike with {@link #limitRequest(long)} which will cap the grand total request + * unlike with {@link #take(long)} which will cap the grand total request * amount. *

    * Similar to {@code flux.publishOn(Schedulers.immediate(), prefetchRate).subscribe() }, @@ -6057,7 +6057,7 @@ public final Flux limitRate(int prefetchRate) { * @return a {@link Flux} limiting downstream's backpressure and customizing the * replenishment request amount * @see #publishOn(Scheduler, int) - * @see #limitRequest(long) + * @see #take(long) */ public final Flux limitRate(int highTide, int lowTide) { return onAssembly(this.publishOn(Schedulers.immediate(), true, highTide, lowTide)); @@ -8764,27 +8764,28 @@ public final Flux tag(String key, String value) { /** * Take only the first N values from this {@link Flux}, if available. - * If n is zero, the source is subscribed to but immediately cancelled, then the operator completes. + * If n is zero, the source isn't even subscribed to and the operator completes immediately upon subscription. *

    - * + * *

    - * Warning: The below behavior will change in 3.5.0 from that of - * {@link #take(long, boolean) take(n, false)} to that of {@link #take(long, boolean) take(n, true)}. - * See https://github.com/reactor/reactor-core/issues/2339 + * This ensures that the total amount requested upstream is capped at {@code n}, although smaller + * requests can be made if the downstream makes requests < n. In any case, this operator never lets + * the upstream produce more elements than the cap, and it can be used to more strictly adhere to backpressure. *

    - * Note that this operator doesn't propagate the backpressure requested amount. - * Rather, it makes an unbounded request and cancels once N elements have been emitted. - * As a result, the source could produce a lot of extraneous elements in the meantime. - * If that behavior is undesirable and you do not own the request from downstream - * (e.g. prefetching operators), consider using {@link #limitRequest(long)} instead. - * - * @param n the number of items to emit from this {@link Flux} + * This mode is typically useful for cases where a race between request and cancellation can lead + * the upstream to producing a lot of extraneous data, and such a production is undesirable (e.g. + * a source that would send the extraneous data over the network). + * It is equivalent to {@link #take(long, boolean)} with {@code limitRequest == true}, + * If there is a requirement for unbounded upstream request (eg. for performance reasons), + * use {@link #take(long, boolean)} with {@code limitRequest=false} instead. + * + * @param n the maximum number of items to request from upstream and emit from this {@link Flux} * * @return a {@link Flux} limited to size N * @see #take(long, boolean) */ public final Flux take(long n) { - return take(n, false); + return take(n, true); } /** @@ -8796,7 +8797,7 @@ public final Flux take(long n) { * at {@code n}. In that configuration, this operator never let the upstream produce more elements * than the cap, and it can be used to more strictly adhere to backpressure. * If n is zero, the source isn't even subscribed to and the operator completes immediately - * upon subscription. + * upon subscription (the behavior inherited from {@link #take(long)}). *

    * This mode is typically useful for cases where a race between request and cancellation can lead * the upstream to producing a lot of extraneous data, and such a production is undesirable (e.g. @@ -8806,8 +8807,7 @@ public final Flux take(long n) { *

    * If {@code limitRequest == false} this operator doesn't propagate the backpressure requested amount. * Rather, it makes an unbounded request and cancels once N elements have been emitted. - * If n is zero, the source is subscribed to but immediately cancelled, then the operator completes - * (the behavior inherited from {@link #take(long)}). + * If n is zero, the source is subscribed to but immediately cancelled, then the operator completes. *

    * In this mode, the source could produce a lot of extraneous elements despite cancellation. * If that behavior is undesirable and you do not own the request from downstream @@ -8868,7 +8868,7 @@ public final Flux take(Duration timespan, Scheduler timer) { return takeUntilOther(Mono.delay(timespan, timer)); } else { - return take(0); + return take(0, false); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 0806b855a9..4fa75c2674 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -3973,9 +3973,11 @@ public final Mono repeatWhenEmpty(int maxRepeat, Function, ? exten return repeatFactory.apply(o.index().map(Tuple2::getT1)); } else { - return repeatFactory.apply(o.index().map(Tuple2::getT1) - .take(maxRepeat) - .concatWith(Flux.error(() -> new IllegalStateException("Exceeded maximum number of repeats")))); + return repeatFactory.apply(o + .index() + .map(Tuple2::getT1) + .take(maxRepeat, false) + .concatWith(Flux.error(() -> new IllegalStateException("Exceeded maximum number of repeats")))); } }).next()); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/take.svg b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/take.svg deleted file mode 100644 index 64aab7c4df..0000000000 --- a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/take.svg +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - take ( 3 ) - - - - - - - - - - - - - - cancel() - - - - take (0 ) - - - - - - - - - cancel() - - - - - - - - - diff --git a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/takeLimitRequestFalse.svg b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/takeLimitRequestFalse.svg index 6515f7377a..7ac0cee914 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/takeLimitRequestFalse.svg +++ b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/takeLimitRequestFalse.svg @@ -1,77 +1,48 @@ - - - - - - + + + + - - - - + + - - - - + + - - - - + + - - - - - - - - - - - - - - - - - - take(3, false) - - - - - - - - - - - - - - cancel() - - - - take(0, false) - - - - - - - - - cancel() - - - - - - - - + + take(3, false) + + + + + + + + + + + + cancel() + + request(max) + + take(0, false) + + + + + + + cancel() + + + + + + + diff --git a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/takeLimitRequestTrue.svg b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/takeLimitRequestTrue.svg index 7849502699..6f599df1c6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/takeLimitRequestTrue.svg +++ b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/takeLimitRequestTrue.svg @@ -1,118 +1,63 @@ - - - - - - + + + + - - - - + + - - - - + + - - - - + + - - - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - take(3, true) + + + + + + + + + + + + + + + + take(3, true) - - - - - - request(23) + + + + + + request(23) - - - request(2) + + + request(2) - - - request(2) + + + request(2) - - - request(1) + + + request(1) - - - - cancel() + + + + cancel() diff --git a/reactor-core/src/test/java/reactor/core/publisher/ContextTests.java b/reactor-core/src/test/java/reactor/core/publisher/ContextTests.java index d269021105..0bf38257c9 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ContextTests.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ public void contextPassing() throws InterruptedException { return ctx; }) .log()) - .take(10) + .take(10, false) //ctx: test=baseSubscriber_take //return: test=baseSubscriber_take_range .contextWrite(ctx -> ctx.put("test", ctx.get("test") + "_range")) @@ -83,7 +83,7 @@ public void contextPassing2() throws InterruptedException { }) .log()) .map(d -> d) - .take(10) + .take(10, false) .contextWrite(ctx -> ctx.put("test", "foo")) .contextWrite(ctx -> ctx.put("test2", "bar")) .log() @@ -99,7 +99,7 @@ public void contextGet() throws InterruptedException { .log() .handle((d, c) -> c.next(c.currentContext().get("test") + "" + d)) .skip(3) - .take(3) + .take(3, false) .handle((d, c) -> c.next(c.currentContext().get("test2") + "" + d)) .contextWrite(ctx -> ctx.put("test", "foo")) .contextWrite(ctx -> ctx.put("test2", "bar")) @@ -141,7 +141,7 @@ public void contextGetHide() throws InterruptedException { .map(d -> d) .handle((d, c) -> c.next(c.currentContext().get("test") + "" + d)) .skip(3) - .take(3) + .take(3, false) .handle((d, c) -> c.next(c.currentContext().get("test2") + "" + d)) .contextWrite(ctx -> ctx.put("test", "foo")) .contextWrite(ctx -> ctx.put("test2", "bar")) diff --git a/reactor-core/src/test/java/reactor/core/publisher/EmitterProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/EmitterProcessorTest.java index 3ff6468911..566cd1418a 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/EmitterProcessorTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/EmitterProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -557,7 +557,7 @@ public void testHanging() { public void testNPE() { EmitterProcessor processor = EmitterProcessor.create(8); AssertSubscriber first = AssertSubscriber.create(1); - processor.log().take(1).subscribe(first); + processor.log().take(1, false).subscribe(first); AssertSubscriber second = AssertSubscriber.create(3); processor.log().subscribe(second); @@ -879,7 +879,7 @@ public void syncFusionFromInfiniteStream() { @Test public void syncFusionFromInfiniteStreamAndTake() { final Flux flux = Flux.fromStream(Stream.iterate(0, i -> i + 1)) - .take(10); + .take(10, false); final EmitterProcessor processor = EmitterProcessor.create(); flux.subscribe(processor); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferPredicateTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferPredicateTest.java index cf89b176ca..1fd6b87ddc 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferPredicateTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferPredicateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -359,7 +359,7 @@ public void untilChangedDisposesStateOnCancel() { .map(retainedDetector::tracked) .concatWith(Mono.error(new Throwable("unexpected"))) .bufferUntilChanged() - .take(50); + .take(50, false); StepVerifier.create(test) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTest.java index adb2389cb6..54617c2673 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -566,7 +566,7 @@ public void discardOnCancel() { @Test public void discardOnCancelSkip() { StepVerifier.create(Flux.just(1, 2, 3, 4, 5) - .limitRequest(2) + .take(2) .concatWith(Mono.never()) .buffer(3, 4)) .thenAwait(Duration.ofMillis(10)) @@ -578,7 +578,7 @@ public void discardOnCancelSkip() { @Test public void discardOnCancelOverlap() { StepVerifier.create(Flux.just(1, 2, 3, 4, 5, 6) - .limitRequest(2) + .take(2) .concatWith(Mono.never()) .buffer(4, 2)) .thenAwait(Duration.ofMillis(10)) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferWhenTest.java index a5af6f983e..030a8f622e 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferWhenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -580,7 +580,7 @@ public void openCloseTake() { StepVerifier.create(source.flux() .bufferWhen(open, o -> close) - .take(1), 2) + .take(1, false), 2) .then(() -> { open.next(1); close.complete(); @@ -602,7 +602,7 @@ public void openCloseLimit() { StepVerifier.create(source.flux() .bufferWhen(open, o -> close) - .limitRequest(1)) + .take(1)) .then(() -> { open.next(1); close.complete(); @@ -843,7 +843,7 @@ public void discardOnDrainEmittedAllWithErrors() { public void discardOnOpenError() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ZERO, Duration.ofMillis(100)) // 0, 1, 2 .map(Long::intValue) - .take(3) + .take(3, false) .bufferWhen(Flux.interval(Duration.ZERO, Duration.ofMillis(100)), u -> (u == 2) ? null : Mono.never())) .thenAwait(Duration.ofSeconds(2)) .expectErrorMessage("The bufferClose returned a null Publisher") @@ -855,7 +855,7 @@ public void discardOnOpenError() { public void discardOnBoundaryError() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ZERO, Duration.ofMillis(100)) // 0, 1, 2 .map(Long::intValue) - .take(3) + .take(3, false) .bufferWhen(Flux.interval(Duration.ZERO, Duration.ofMillis(100)), u -> (u == 2) ? Mono.error(new IllegalStateException("boom")) : Mono.never())) .thenAwait(Duration.ofSeconds(2)) .expectErrorMessage("boom") diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxConcatArrayTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxConcatArrayTest.java index f8a7360e9a..1e0a1aa89c 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxConcatArrayTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxConcatArrayTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -163,7 +163,7 @@ public void errorManyDelayed() { public void veryLongTake() { Flux.range(1, 1_000_000_000) .concatWith(Flux.empty()) - .take(10) + .take(10, false) .subscribeWith(AssertSubscriber.create()) .assertComplete() .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java index 39dd10ef8b..4367c09232 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1080,7 +1080,7 @@ private void testFluxCreateOnRequestMultipleThreads(OverflowStrategy overflowStr s.onDispose(() -> queue.close()); }, overflowStrategy); - StepVerifier.create(created.take(count).publishOn(Schedulers.parallel(), 1000)) + StepVerifier.create(created.take(count, false).publishOn(Schedulers.parallel(), 1000)) .expectNextCount(count) .expectComplete() .verify(); @@ -1271,7 +1271,7 @@ void contextTest() { .currentContext() .get(AtomicInteger.class) .incrementAndGet()))) - .take(10) + .take(10, false) .contextWrite(ctx -> ctx.put(AtomicInteger.class, new AtomicInteger()))) .expectNext(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) @@ -1284,7 +1284,7 @@ void contextTestPush() { .currentContext() .get(AtomicInteger.class) .incrementAndGet()))) - .take(10) + .take(10, false) .contextWrite(ctx -> ctx.put(AtomicInteger.class, new AtomicInteger()))) .expectNext(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDelaySequenceTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDelaySequenceTest.java index 9b567243c2..a0c35c874d 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDelaySequenceTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDelaySequenceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ public void delayFirstInterval() { Supplier>> test = () -> Flux.interval(Duration.ofMillis(50)) .delaySequence(Duration.ofMillis(500)) .elapsed() - .take(33); + .take(33, false); StepVerifier.withVirtualTime(test) .thenAwait(Duration.ofMillis(500 + 50)) @@ -73,7 +73,7 @@ public void delayFirstAsymmetricDelays() { return asymmetricDelays .delaySequence(Duration.ofMillis(500)) - .take(33) + .take(33, false) .elapsed(); }; @@ -109,7 +109,7 @@ public void delayElements() { Flux> test = Flux.interval(Duration.ofMillis(50)) .onBackpressureDrop() .delayElements(Duration.ofMillis(500)) - .take(33) + .take(33, false) .elapsed() .log(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDematerializeTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDematerializeTest.java index b5de955985..6cceee2578 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDematerializeTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDematerializeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -266,7 +266,7 @@ public void emissionTimingsAreGrouped() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofSeconds(1)) .map(i -> "tick" + i) - .take(5) + .take(5, false) .timestamp() .materialize() .>dematerialize() diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDistinctTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDistinctTest.java index 99716bda68..299d9110ce 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDistinctTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDistinctTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -692,7 +692,7 @@ public void distinctDefaultCancelDoesntRetainObjects() throws InterruptedExcepti .map(i -> retainedDetector.tracked(new DistinctDefaultCancel(i))) .concatWith(Mono.error(new IllegalStateException("boom"))) .distinct() - .take(50); + .take(50, false); StepVerifier.create(test) .expectNextCount(50) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDistinctUntilChangedTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDistinctUntilChangedTest.java index c59fd0da30..fb353255fa 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDistinctUntilChangedTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDistinctUntilChangedTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -362,7 +362,7 @@ public void distinctUntilChangedDefaultCancelDoesntRetainObjects() throws Interr .map(i -> retainedDetector.tracked(new DistinctDefaultCancel(i))) .concatWith(Mono.error(new IllegalStateException("boom"))) .distinctUntilChanged() - .take(50); + .take(50, false); StepVerifier.create(test) .expectNextCount(50) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDoFinallyTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDoFinallyTest.java index 844176ab38..1917208af9 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDoFinallyTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDoFinallyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,7 +92,7 @@ public void normalError() { @Test public void normalCancel() { - StepVerifier.create(Flux.range(1, 10).hide().doFinally(this).take(5)) + StepVerifier.create(Flux.range(1, 10).hide().doFinally(this).take(5, false)) .expectNoFusionSupport() .expectNext(1, 2, 3, 4, 5) .expectComplete() @@ -229,7 +229,7 @@ public void normalCancelConditional() { .hide() .doFinally(this) .filter(i -> true) - .take(5)) + .take(5, false)) .expectNoFusionSupport() .expectNext(1, 2, 3, 4, 5) .expectComplete() diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxExpandTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxExpandTest.java index 382089428c..7c2d82d05a 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxExpandTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxExpandTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -143,7 +143,7 @@ public void recursiveCountdownLoopDepth() { public void recursiveCountdownTake() { StepVerifier.create(Flux.just(10) .expand(countDown) - .take(5) + .take(5, false) ) .expectNext(10, 9, 8, 7, 6) .verifyComplete(); @@ -153,7 +153,7 @@ public void recursiveCountdownTake() { public void recursiveCountdownTakeDepth() { StepVerifier.create(Flux.just(10) .expandDeep(countDown) - .take(5) + .take(5, false) ) .expectNext(10, 9, 8, 7, 6) .verifyComplete(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxFilterWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxFilterWhenTest.java index 685ec57065..66f4cca9fa 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxFilterWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxFilterWhenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -183,7 +183,7 @@ public void predicateErrorFused() { public void take() { StepVerifier.create(Flux.range(1, 10) .filterWhen(v -> Mono.just(v % 2 == 0).hide()) - .take(1)) + .take(1, false)) .expectNext(2) .verifyComplete(); } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxGenerateTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxGenerateTest.java index 3912d39f2f..229e96ea3c 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxGenerateTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxGenerateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -437,7 +437,7 @@ public void contextTest() { StepVerifier.create(Flux.generate(s -> s.next(s.currentContext() .get(AtomicInteger.class) .incrementAndGet())) - .take(10) + .take(10, false) .contextWrite(ctx -> ctx.put(AtomicInteger.class, new AtomicInteger()))) .expectNext(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxGroupByTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxGroupByTest.java index fdfeb524d7..3931c3b951 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxGroupByTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxGroupByTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,7 @@ void performanceOfContinuouslyCancellingGroups() throws Exception { .publishOn(scheduler) .groupBy(Function.identity()) .flatMap(groupFlux -> groupFlux.take(Duration.ofSeconds(1L), scheduler) - .take(2) + .take(2, false) .collectList(), 16384) .map(Collection::size) .subscribe(downstream::addAndGet, System.err::println, latch::countDown); @@ -189,7 +189,7 @@ public void takeTwoGroupsOnly() { Flux.range(1, 10) .groupBy(k -> k % 3) - .take(2) + .take(2, false) .subscribe(ts); ts.assertValueCount(2) @@ -435,7 +435,7 @@ public void twoGroupsLongAsyncMergeHidden2() { final int total = 100_000; Flux.range(0, total) .groupBy(i -> (i / 2d) * 2d, 42) - .flatMap(it -> it.take(1) + .flatMap(it -> it.take(1, false) .hide(), 2) .publishOn(Schedulers.fromExecutorService(forkJoinPool), 2) .count() diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxIntervalTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxIntervalTest.java index ab9eaa095f..765c4ddcd9 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxIntervalTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxIntervalTest.java @@ -58,7 +58,7 @@ public void normal() { .add(System.currentTimeMillis()); Flux.interval(Duration.ofMillis(100), Duration.ofMillis(100), exec) - .take(5) + .take(5, false) .map(v -> System.currentTimeMillis()) .subscribe(ts); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxIterableTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxIterableTest.java index fbc9146ddb..916ba0f382 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxIterableTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxIterableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -299,7 +299,7 @@ public Integer next() { Flux.fromIterable(one) .publishOn(Schedulers.single()) - .take(10) + .take(10, false) .doOnDiscard(Integer.class, i -> discardCount.incrementAndGet()) .blockLast(Duration.ofSeconds(1)); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxLimitRequestTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxLimitRequestTest.java index 6f8eb98c31..9575ce7929 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxLimitRequestTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxLimitRequestTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -173,7 +173,7 @@ void takeCancelsOperatorAndSource() { .take(10, true) .doOnCancel(() -> operatorCancelled.set(true)) .doOnRequest(operatorRequested::add) - .take(3); + .take(3, false); StepVerifier.create(test) .expectNextCount(3) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxMergeComparingTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxMergeComparingTest.java index de6ca1341b..c5dd1fd63b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxMergeComparingTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxMergeComparingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2021-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -564,7 +564,7 @@ void backpressure3() { void take() { new FluxMergeComparing<>(2, Comparator.naturalOrder(), false, Flux.just(1, 3, 5, 7), Flux.just(2, 4, 6, 8)) - .take(5) + .take(5, false) .as(StepVerifier::create) .expectNext(1, 2, 3, 4, 5) .verifyComplete(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxMergeOrderedTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxMergeOrderedTest.java index b87b25ad0c..35a7d9f63f 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxMergeOrderedTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxMergeOrderedTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -522,7 +522,7 @@ void backpressure3() { void take() { new FluxMergeComparing<>(2, Comparator.naturalOrder(), true, Flux.just(1, 3, 5, 7), Flux.just(2, 4, 6, 8)) - .take(5) + .take(5, false) .as(StepVerifier::create) .expectNext(1, 2, 3, 4, 5) .verifyComplete(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxOnBackpressureBufferTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxOnBackpressureBufferTest.java index dd8759a064..1e1075a5ef 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxOnBackpressureBufferTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxOnBackpressureBufferTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -196,8 +196,8 @@ public void stepByStepRequesting() { AtomicInteger sourceCancelledAt = new AtomicInteger(-1); StepVerifier.withVirtualTime(() -> - Flux.interval(Duration.ofSeconds(1)) // lets emit 1 item per second; starting with zero - .take(8) + Flux.interval(Duration.ofSeconds(1)) + .take(8, false) .doOnNext(v -> sourceProduced.incrementAndGet()) .doOnCancel(() -> sourceCancelledAt.set(sourceProduced.get() - 1)) .onBackpressureBuffer(2, discardedItems::add) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxOnBackpressureBufferTimeoutTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxOnBackpressureBufferTimeoutTest.java index 5f95d5b734..1df335e08b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxOnBackpressureBufferTimeoutTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxOnBackpressureBufferTimeoutTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -182,7 +182,7 @@ public void timeoutLimit() { public void take() { StepVerifier.create(Flux.range(1, 5) .onBackpressureBuffer(Duration.ofMinutes(1), Integer.MAX_VALUE, v -> {}) - .take(2)) + .take(2, false)) .expectNext(1, 2) .verifyComplete(); } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxPublishMulticastTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxPublishMulticastTest.java index 87498991e9..ca2f6c975e 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxPublishMulticastTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxPublishMulticastTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -404,7 +404,7 @@ public void gh870() throws Exception { // .doOnCancel(() -> System.out.println("cancel 2")) .publish(Function.identity()) // .doOnCancel(() -> System.out.println("cancel 1")) - .take(5)) + .take(5, false)) .expectNextCount(5) .verifyComplete(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxPublishTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxPublishTest.java index e020930eff..3106b5f21c 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxPublishTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxPublishTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -673,7 +673,7 @@ public void syncFusionFromInfiniteStreamAndTake() { Flux.fromStream(Stream.iterate(0, i -> i + 1)) .publish() .autoConnect() - .take(10); + .take(10, false); StepVerifier.create(publish) .expectNextCount(10) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxRefCountGraceTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxRefCountGraceTest.java index 0685c889b2..a9097efa48 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxRefCountGraceTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxRefCountGraceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ void errorInterruptsGracePeriodPublish() { .refCount(1, Duration.ofSeconds(200)); return Flux.merge( - multiplex.take(1), + multiplex.take(1, false), Mono.delay(Duration.ofSeconds(5)) .flatMapMany(ignore -> multiplex .materialize().elapsed().map(Object::toString)) @@ -88,7 +88,7 @@ void errorInterruptsGracePeriodReplay() { .refCount(1, Duration.ofSeconds(200)); return Flux.merge( - multiplex.take(1), + multiplex.take(1, false), Mono.delay(Duration.ofSeconds(5)).flatMapMany(ignore -> multiplex .materialize().elapsed().map(Object::toString)) ); @@ -119,7 +119,7 @@ void completeInterruptsGracePeriodPublish() { .refCount(1, Duration.ofSeconds(200)); return Flux.merge( - multiplex.take(1), + multiplex.take(1, false), Mono.delay(Duration.ofSeconds(5)) .flatMapMany(ignore -> multiplex .materialize().elapsed().map(Object::toString)) @@ -152,7 +152,7 @@ void completeInterruptsGracePeriodReplay() { .refCount(1, Duration.ofSeconds(200)); return Flux.merge( - multiplex.take(1), + multiplex.take(1, false), Mono.delay(Duration.ofSeconds(5)).flatMapMany(ignore -> multiplex .materialize().elapsed().map(Object::toString)) ); @@ -181,7 +181,7 @@ public void avoidUnexpectedDoubleCancel() { test.subscribe(v -> {}, e -> unexpectedCancellation.set(true)); - StepVerifier.create(test.take(3)) + StepVerifier.create(test.take(3, false)) .expectNextCount(3) .verifyComplete(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxRefCountTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxRefCountTest.java index bef47a20e4..c051e81839 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxRefCountTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxRefCountTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ public void avoidUnexpectedDoubleCancel() { test.subscribe(v -> { }, e -> unexpectedCancellation.set(true)); - StepVerifier.create(test.take(3)) + StepVerifier.create(test.take(3, false)) .expectNextCount(3) .verifyComplete(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxRepeatTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxRepeatTest.java index a8406ee786..4aaa3e7445 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxRepeatTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxRepeatTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -120,7 +120,7 @@ public void repeatInfinite() { Flux.range(1, 2) .repeat() - .take(9) + .take(9, false) .subscribe(ts); ts.assertValues(1, 2, 1, 2, 1, 2, 1, 2, 1) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxRepeatWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxRepeatWhenTest.java index 507b808971..0716750c8a 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxRepeatWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxRepeatWhenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -160,7 +160,7 @@ public void lateOtherEmptyCancelsSource() { .doOnSubscribe(sub -> sourceSubscribed.set(true)) .doOnCancel(() -> sourceCancelled.set(true)); - Flux repeat = source.repeatWhen(other -> other.take(1)); + Flux repeat = source.repeatWhen(other -> other.take(1, false)); StepVerifier.create(repeat) .expectSubscription() @@ -377,7 +377,7 @@ Flux exponentialRepeatScenario2() { @Test public void scanOperator(){ Flux parent = Flux.just(1); - FluxRepeatWhen test = new FluxRepeatWhen<>(parent, c -> c.take(3)); + FluxRepeatWhen test = new FluxRepeatWhen<>(parent, c -> c.take(3, false)); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxReplayTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxReplayTest.java index d0fd196a50..89c716e733 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxReplayTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxReplayTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -434,7 +434,7 @@ public void multipleEarlySubscribersPropagateTheirLateRequests() { .doOnRequest(requests::add) .replay(7); - replay.take(13).subscribe(fiveThenEightSubscriber); + replay.take(13, false).subscribe(fiveThenEightSubscriber); replay.subscribe(sevenThenEightSubscriber); replay.connect(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxRetryTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxRetryTest.java index 701a566493..89590e0caa 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxRetryTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxRetryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,7 +103,7 @@ public void retryInfinite() { AssertSubscriber ts = AssertSubscriber.create(); source.retry() - .take(10) + .take(10, false) .subscribe(ts); ts.assertValues(1, 2, 3, 1, 2, 3, 1, 2, 3, 1) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxRetryWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxRetryWhenTest.java index e08e1ad85b..b7181178f0 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxRetryWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxRetryWhenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -339,7 +339,7 @@ public void lateOtherEmptyCancelsSourceAndCompletes() { .doOnSubscribe(sub -> sourceSubscribed.set(true)) .doOnCancel(() -> sourceCancelled.set(true)); - Flux retry = source.retryWhen(Retry.from(other -> other.take(1))); + Flux retry = source.retryWhen(Retry.from(other -> other.take(1, false))); StepVerifier.create(retry) .expectSubscription() @@ -1027,7 +1027,7 @@ public void gh1978() { .jitter(0d) .transientErrors(true) ) - .take(stopAfterCycles * elementPerCycle) + .take(stopAfterCycles * elementPerCycle, false) .elapsed() .map(Tuple2::getT1) .doOnNext(pause -> { if (pause > 500) pauses.add(pause / 1000); }) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxStreamTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxStreamTest.java index e023ffdefd..01e6aaac36 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxStreamTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxStreamTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -478,7 +478,7 @@ void infiniteStreamDoesntHangDiscardFused() { Flux.fromStream(stream) .publishOn(Schedulers.single()) - .take(10) + .take(10, false) .doOnDiscard(Integer.class, i -> {}) .blockLast(Duration.ofSeconds(1)); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxSwitchOnFirstTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxSwitchOnFirstTest.java index 0d916f8c40..db6cffc33b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxSwitchOnFirstTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxSwitchOnFirstTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -823,7 +823,7 @@ public void shouldBeAbleToBeCancelledProperly2() { .switchOnFirst((first, innerFlux) -> innerFlux .map(String::valueOf) - .take(1) + .take(1, false) ); publisher.next(1); @@ -848,7 +848,7 @@ public void shouldBeAbleToBeCancelledProperly3() { innerFlux .map(String::valueOf) ) - .take(1); + .take(1, false); publisher.next(1); publisher.next(2); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxTakeTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxTakeTest.java index 3c62462660..9daa938537 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxTakeTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxTakeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -712,7 +712,7 @@ public void onSubscribeRaceRequestingShouldBeConsistentForTakeFuseableTest() thr int take = 3000; RaceSubscriber actual = new RaceSubscriber<>(take); Flux.range(0, Integer.MAX_VALUE) - .take(take) + .take(take, false) .subscribe(actual); actual.await(5, TimeUnit.SECONDS); @@ -726,7 +726,7 @@ public void onSubscribeRaceRequestingShouldBeConsistentForTakeConditionalTest() int take = 3000; RaceSubscriber actual = new RaceSubscriber<>(take); Flux.range(0, Integer.MAX_VALUE) - .take(take) + .take(take, false) .filter(e -> true) .subscribe(actual); @@ -742,7 +742,7 @@ public void onSubscribeRaceRequestingShouldBeConsistentForTakeTest() throws Inte RaceSubscriber actual = new RaceSubscriber<>(take); Flux.range(0, Integer.MAX_VALUE) .hide() - .take(take) + .take(take, false) .subscribe(actual); actual.await(5, TimeUnit.SECONDS); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxUsingWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxUsingWhenTest.java index 26368ddb17..4dd9a11fcf 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxUsingWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxUsingWhenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -385,7 +385,7 @@ public void cancelWithHandler(Flux source) { TestResource::commit, TestResource::rollback, TestResource::cancel) - .take(2); + .take(2, false); StepVerifier.create(test) .expectNext("0", "1") @@ -412,7 +412,7 @@ public void cancelWithHandlerFailure(Flux source) { //immediate error to trigger the logging within the test .concatWith(Mono.error(new IllegalStateException("cancel error"))) ) - .take(2); + .take(2, false); StepVerifier.create(test) .expectNext("0", "1") @@ -443,7 +443,7 @@ public void cancelWithHandlerGenerationFailureLogs(Flux source) throws I TestResource::commit, TestResource::rollback, r -> null) - .take(2); + .take(2, false); StepVerifier.create(test) .expectNext("0", "1") @@ -475,7 +475,7 @@ public void cancelWithoutHandlerAppliesCommit(Flux source) { (tr, e) -> tr.rollback(new RuntimeException("placeholder rollback exception")), TestResource::commit ) - .take(2); + .take(2, false); StepVerifier.create(test) .expectNext("0", "1") @@ -508,7 +508,7 @@ public void cancelDefaultHandlerFailure(Flux source) { (r, e) -> r.rollback(new RuntimeException("placeholder ignored rollback exception")), completeOrCancel ) - .take(2); + .take(2, false); StepVerifier.create(test) .expectNext("0", "1") @@ -710,7 +710,7 @@ public void apiCancel(Flux transactionToCancel) { TestResource::rollback, TestResource::cancel); - StepVerifier.create(flux.take(1), 1) + StepVerifier.create(flux.take(1, false), 1) .expectNext("Transaction started") .verifyComplete(); @@ -737,7 +737,7 @@ public void apiCancelFailure(Flux transaction) { TestResource::rollback, TestResource::cancelError); - StepVerifier.create(flux.take(1), 1) + StepVerifier.create(flux.take(1, false), 1) .expectNext("Transaction started") .verifyComplete(); @@ -776,7 +776,7 @@ public void apiCancelGeneratingNullLogs(Flux transactionToCancel) { TestResource::rollback, TestResource::cancelNull); - StepVerifier.create(flux.take(1), 1) + StepVerifier.create(flux.take(1, false), 1) .expectNext("Transaction started") .verifyComplete(); @@ -963,7 +963,7 @@ public void contextPropagationOnCancel(Flux source) { TestResource::rollback, cancel -> cancelHandler) .contextWrite(Context.of(String.class, "contextual")) - .take(1) + .take(1, false) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -993,7 +993,7 @@ public void contextPropagationOnCancelWithNoHandler(Flux source) { TestResource::rollback, null) .contextWrite(Context.of(String.class, "contextual")) - .take(1) + .take(1, false) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowPredicateTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowPredicateTest.java index 8ede2f708e..e5d0f62aa2 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowPredicateTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowPredicateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -325,7 +325,7 @@ public void untilChangedDisposesStateOnCancel() { .map(retainedDetector::tracked) .concatWith(Mono.error(new Throwable("unexpected"))) .windowUntilChanged() - .take(50); + .take(50, false); StepVerifier.create(test) @@ -904,7 +904,7 @@ public void innerCancellationCancelsMainSequence() { StepVerifier.create(Flux.just("red", "green", "#", "black", "white") .log() .windowWhile(s -> !s.equals("#")) - .flatMap(w -> w.take(1))) + .flatMap(w -> w.take(1, false))) .expectNext("red") .thenCancel() .verify(); @@ -1069,7 +1069,7 @@ public void discardOnCancelWindowWhile() { .log("source", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) .windowWhile(i -> i > 0, 1) .concatMap(w -> w.log("win", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) - .take(1).contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardWindow::add))) + .take(1, false).contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardWindow::add))) .log("out", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) .contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardMain::add)) // stalls and times out without the fix due to insufficient request to upstream @@ -1094,7 +1094,7 @@ public void discardOnCancelWindowUntil() { .log("source", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) .windowUntil(i -> i == 0, false, 1) .concatMap(w -> w.log("win", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) - .take(1).contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardWindow::add))) + .take(1, false).contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardWindow::add))) .log("out", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) .contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardMain::add)) // stalls and times out without the fix due to insufficient request to upstream @@ -1119,7 +1119,7 @@ public void discardOnCancelWindowUntilCutBefore() { .log("source", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) .windowUntil(i -> i == 0, true, 1) .concatMap(w -> w.log("win", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) - .take(1).contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardWindow::add))) + .take(1, false).contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardWindow::add))) .log("out", Level.FINE, ON_NEXT, REQUEST, ON_COMPLETE, CANCEL) .contextWrite(Context.of(Hooks.KEY_ON_DISCARD, (Consumer) discardMain::add)) // stalls and times out without the fix due to insufficient request to upstream diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoCollectListTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoCollectListTest.java index 54b913aa9a..b0e38a7041 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoCollectListTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoCollectListTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -207,7 +207,7 @@ public void discardOnError() { @Test public void discardOnCancel() { Mono> test = Flux.interval(Duration.ofMillis(100)) - .take(10) + .take(10, false) .collectList(); StepVerifier.create(test) diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoCollectTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoCollectTest.java index 164209ccb3..de5fb3afbd 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoCollectTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoCollectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -256,7 +256,7 @@ public void discardListElementsOnError() { public void discardListElementsOnCancel() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofMillis(100)) - .take(10) + .take(10, false) .collect(ArrayList::new, List::add) ) .expectSubscription() @@ -300,7 +300,7 @@ public void discardWholeArrayOnCancel() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofMillis(100)) - .take(10) + .take(10, false) .collect(() -> new Object[4], (container, element) -> container[index.getAndIncrement()] = element) .doOnDiscard(Object.class, discarded::add)) .expectSubscription() diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoExpandTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoExpandTest.java index 68ac392563..cd831bfae4 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoExpandTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoExpandTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -139,7 +139,7 @@ public void recursiveCountdownLoopDepth() { public void recursiveCountdownTake() { StepVerifier.create(Mono.just(10) .expand(countDown) - .take(5) + .take(5, false) ) .expectNext(10, 9, 8, 7, 6) .verifyComplete(); @@ -149,7 +149,7 @@ public void recursiveCountdownTake() { public void recursiveCountdownTakeDepth() { StepVerifier.create(Mono.just(10) .expandDeep(countDown) - .take(5) + .take(5, false) ) .expectNext(10, 9, 8, 7, 6) .verifyComplete(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoRepeatTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoRepeatTest.java index 2733f3a3a2..62c5ea5bdd 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoRepeatTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoRepeatTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -129,7 +129,7 @@ public void repeatInfinite() { AtomicInteger i = new AtomicInteger(); Mono.fromCallable(i::incrementAndGet) .repeat() - .take(9) + .take(9, false) .subscribe(ts); ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9) diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoStreamCollectorTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoStreamCollectorTest.java index 7d2e8b1d93..7976bc3749 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoStreamCollectorTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoStreamCollectorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -211,7 +211,7 @@ public void discardIntermediateListElementsOnCancel() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofMillis(100)) - .take(10) + .take(10, false) .collect(collector) ) .expectSubscription() @@ -273,7 +273,7 @@ public void discardIntermediateMapOnCancel() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofMillis(100)) - .take(10) + .take(10, false) .collect(collector) .doOnDiscard(Object.class, discarded::add)) .expectSubscription() diff --git a/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxTest.java b/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxTest.java index 188c7c7df5..6e4b2a01cb 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -754,7 +754,7 @@ public void testPeekCancel() { .doOnError(e -> errorCount.increment()) .doOnTerminate(terminateCount::increment) .doAfterTerminate(afterTerminateCount::increment) - .sequential().take(4).subscribe(); + .sequential().take(4, false).subscribe(); assertThat(signals).as("signals").hasSize(4); //2x2 onNext (+ 2 non-represented cancels) assertThat(subscribeCount.longValue()).as("subscribe").isEqualTo(2); //1 per rail diff --git a/reactor-core/src/test/java/reactor/core/publisher/SerializedSubscriberTest.java b/reactor-core/src/test/java/reactor/core/publisher/SerializedSubscriberTest.java index 44dffefca7..07450824db 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/SerializedSubscriberTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SerializedSubscriberTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -115,7 +115,7 @@ void testLeakWithRetryWhenImmediatelyCancelled() throws InterruptedException { .doFinally(sig -> latch.countDown()) .publishOn(Schedulers.single()) .doFinally(sig -> latch.countDown()) - .retryWhen(Retry.from(p -> p.take(3))) + .retryWhen(Retry.from(p -> p.take(3, false))) .doFinally(sig -> latch.countDown()) .cancelOn(Schedulers.parallel()) .doOnDiscard(AtomicInteger.class, i -> { diff --git a/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxSpecTests.java b/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxSpecTests.java index 6674f9e8b6..2f722f1a7e 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxSpecTests.java +++ b/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -220,7 +220,7 @@ Flux scenario_rangeTimedSample() { return Flux.range(1, Integer.MAX_VALUE) .delayElements(Duration.ofMillis(100)) .sample(Duration.ofSeconds(4)) - .take(1); + .take(1, false); } @Test @@ -1047,7 +1047,7 @@ Mono scenario_fluxItemCanBeShiftedByTime() { return Flux.range(0, 10000) .delayElements(Duration.ofMillis(150)) .elapsed() - .take(10) + .take(10, false) .reduce(0L, (acc, next) -> acc > 0l ? ((next.getT1() + acc) / 2) : next.getT1()); @@ -1066,7 +1066,7 @@ Mono scenario_fluxItemCanBeShiftedByTime2() { return Flux.range(0, 10000) .delayElements(Duration.ofMillis(150)) .elapsed() - .take(10) + .take(10, false) .reduce(0L, (acc, next) -> acc > 0l ? ((next.getT1() + acc) / 2) : next.getT1()); diff --git a/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java b/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java index d897c0b04d..dbaaf5ee74 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java +++ b/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java @@ -1469,7 +1469,7 @@ public void testThrowWithoutOnErrorShowsUpInSchedulerHandler() { try { Flux.interval(Duration.ofMillis(100)) - .take(1) + .take(1, false) .publishOn(Schedulers.parallel()) .doOnCancel(latch::countDown) .subscribe(i -> { diff --git a/reactor-core/src/test/java/reactor/core/publisher/scenarios/PopularTagTests.java b/reactor-core/src/test/java/reactor/core/publisher/scenarios/PopularTagTests.java index 1e0aad7e60..7289b4a862 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/scenarios/PopularTagTests.java +++ b/reactor-core/src/test/java/reactor/core/publisher/scenarios/PopularTagTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,8 +67,8 @@ public void sampleTest() throws Exception { .flatMap(s -> s.groupBy(w -> w) .flatMap(w -> w.count().map(c -> Tuples.of(w.key(), c))) .collectSortedList((a, b) -> -a.getT2().compareTo(b.getT2())) - .flatMapMany(Flux::fromIterable) - .take(10) + .flatMapMany(Flux::fromIterable) + .take(10, false) .doAfterTerminate(() -> LOG.info("------------------------ window terminated" + "----------------------")) ) diff --git a/reactor-core/src/test/java/reactor/core/scheduler/RejectedExecutionTest.java b/reactor-core/src/test/java/reactor/core/scheduler/RejectedExecutionTest.java index b39a94fb40..cf741bac61 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/RejectedExecutionTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/RejectedExecutionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -111,7 +111,7 @@ public void tearDown() { */ @Test public void publishOn() throws Exception { - Flux flux = Flux.interval(Duration.ofMillis(2)).take(255) + Flux flux = Flux.interval(Duration.ofMillis(2)).take(255, false) .publishOn(scheduler) .doOnNext(i -> onNext(i)) .doOnError(e -> onError(e)); @@ -121,7 +121,7 @@ public void publishOn() throws Exception { @Test public void publishOnFilter() throws Exception { - Flux flux = Flux.interval(Duration.ofMillis(2)).take(255) + Flux flux = Flux.interval(Duration.ofMillis(2)).take(255, false) .publishOn(scheduler) .filter(t -> true) .doOnNext(i -> onNext(i)) @@ -156,7 +156,7 @@ public void publishOnFilter() throws Exception { */ @Test public void parallelRunOn() throws Exception { - ParallelFlux flux = Flux.interval(Duration.ofMillis(2)).take(255) + ParallelFlux flux = Flux.interval(Duration.ofMillis(2)).take(255, false) .parallel(1) .runOn(scheduler) .doOnNext(i -> onNext(i)) @@ -186,8 +186,7 @@ public void parallelRunOn() throws Exception { @Test public void subscribeOn() throws Exception { scheduler.tasksRemaining.set(1); //1 subscribe then request - Flux flux = Flux.interval(Duration.ofMillis(2)) - .take(255) + Flux flux = Flux.interval(Duration.ofMillis(2)).take(255, false) .subscribeOn(scheduler); CountDownLatch latch = new CountDownLatch(1); diff --git a/reactor-core/src/test/java/reactor/guide/GuideTests.java b/reactor-core/src/test/java/reactor/guide/GuideTests.java index eb9fc6c134..96ef0332cd 100644 --- a/reactor-core/src/test/java/reactor/guide/GuideTests.java +++ b/reactor-core/src/test/java/reactor/guide/GuideTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -581,13 +581,12 @@ public String toString() { public void errorHandlingDoFinally() { LongAdder statsCancel = new LongAdder(); // <1> - Flux flux = - Flux.just("foo", "bar") - .doFinally(type -> { - if (type == SignalType.CANCEL) // <2> - statsCancel.increment(); // <3> - }) - .take(1); // <4> + Flux flux = Flux.just("foo", "bar") + .doFinally(type -> { + if (type == SignalType.CANCEL) // <2> + statsCancel.increment(); // <3> + }) + .take(1); // <4> StepVerifier.create(flux) .expectNext("foo") @@ -660,7 +659,7 @@ public void errorHandlingRetryWhenApproximateRetry() { Flux.error(new IllegalArgumentException()) // <1> .doOnError(System.out::println) // <2> .retryWhen(Retry.from(companion -> // <3> - companion.take(3))); // <4> + companion.take(3, false))); // <4> StepVerifier.create(flux) .verifyComplete(); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxMetricsFuseableTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxMetricsFuseableTest.java index 26fe1e3a20..77daa93dc0 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxMetricsFuseableTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxMetricsFuseableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -285,7 +285,7 @@ public void onNextTimerCountsFuseable() { Flux source2 = Flux.range(1, 10); new FluxMetricsFuseable<>(source2) - .take(3) + .take(3, false) .blockLast(); assertThat(nextMeter.count()).isEqualTo(126L); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxMetricsTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxMetricsTest.java index bc1624794f..88e754bf84 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxMetricsTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxMetricsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -182,7 +182,7 @@ public void onNextTimerCounts() { .hide(); new FluxMetrics<>(source2) - .take(3) + .take(3, false) .blockLast(); assertThat(nextMeter.count()).isEqualTo(126L); @@ -366,7 +366,7 @@ public void subscribeToCancel() { .hide(); new FluxMetrics<>(source) - .take(1) + .take(1, false) .blockLast(); Timer stcCompleteTimer = registry.find(REACTOR_DEFAULT_NAME + METER_FLOW_DURATION) diff --git a/reactor-test/src/test/java/reactor/test/DefaultContextExpectationsTest.java b/reactor-test/src/test/java/reactor/test/DefaultContextExpectationsTest.java index 8720e18754..bee7bf196d 100644 --- a/reactor-test/src/test/java/reactor/test/DefaultContextExpectationsTest.java +++ b/reactor-test/src/test/java/reactor/test/DefaultContextExpectationsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,13 +74,13 @@ void tearDown() { @Test public void contextAccessibleLastInChain() { - assertContextExpectation(s -> s.take(3).contextWrite(Context.of("a", "b")), + assertContextExpectation(s -> s.take(3, false).contextWrite(Context.of("a", "b")), e -> e, 3); } @Test public void contextAccessibleFirstInChain() { - assertContextExpectation(s -> s.contextWrite(Context.of("a", "b")).take(3), + assertContextExpectation(s -> s.contextWrite(Context.of("a", "b")).take(3, false), e -> e, 3); } diff --git a/reactor-test/src/test/java/reactor/test/StepVerifierAssertionsTests.java b/reactor-test/src/test/java/reactor/test/StepVerifierAssertionsTests.java index 350887c685..37295a3b8f 100644 --- a/reactor-test/src/test/java/reactor/test/StepVerifierAssertionsTests.java +++ b/reactor-test/src/test/java/reactor/test/StepVerifierAssertionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ public void assertDroppedElementsAllPass() { s.onComplete(); s.onNext("bar"); s.onNext("baz"); - }).take(3)) + }).take(3, false)) .expectNext("foo") .expectComplete() .verifyThenAssertThat() @@ -55,7 +55,7 @@ public void assertNotDroppedElementsFailureOneDrop() { s.onNext("foo"); s.onComplete(); s.onNext("bar"); - }).take(2)) + }).take(2, false)) .expectNext("foo") .expectComplete() .verifyThenAssertThat() @@ -91,7 +91,7 @@ public void assertDroppedElementsFailureOneExtra() { s.onComplete(); s.onNext("bar"); s.onNext("baz"); - }).take(3)) + }).take(3, false)) .expectNext("foo") .expectComplete() .verifyThenAssertThat() @@ -112,7 +112,7 @@ public void assertDroppedElementsFailureOneMissing() { s.onComplete(); s.onNext("bar"); s.onNext("baz"); - }).take(3)) + }).take(3, false)) .expectNext("foo") .expectComplete() .verifyThenAssertThat() @@ -248,7 +248,7 @@ public void assertNotDroppedErrorsFailureOneDrop() { s.onNext("foo"); s.onComplete(); s.onError(new IllegalStateException("boom")); - }).take(2)) + }).take(2, false)) .expectNext("foo") .expectComplete() .verifyThenAssertThat() diff --git a/reactor-test/src/test/java/reactor/test/StepVerifierTests.java b/reactor-test/src/test/java/reactor/test/StepVerifierTests.java index 7bfde0f105..838fc05d19 100644 --- a/reactor-test/src/test/java/reactor/test/StepVerifierTests.java +++ b/reactor-test/src/test/java/reactor/test/StepVerifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -646,7 +646,7 @@ public void verifyDuration() { long interval = 200; Flux flux = Flux.interval(Duration.ofMillis(interval)) .map(l -> "foo") - .take(2); + .take(2, false); Duration duration = StepVerifier.create(flux) .thenAwait(Duration.ofSeconds(100)) @@ -662,7 +662,7 @@ public void verifyDuration() { public void verifyDurationTimeout() { Flux flux = Flux.interval(Duration.ofMillis(200)) .map(l -> "foo") - .take(2); + .take(2, false); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> StepVerifier.create(flux) @@ -1035,7 +1035,7 @@ public void verifyVirtualTimeNoEventError() { @Test public void verifyVirtualTimeNoEventInterval() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofSeconds(3)) - .take(2)) + .take(2, false)) .expectSubscription() .expectNoEvent(Duration.ofSeconds(3)) .expectNext(0L) @@ -1050,7 +1050,7 @@ public void verifyVirtualTimeNoEventInterval() { public void verifyVirtualTimeNoEventIntervalError() { Throwable thrown = catchThrowable(() -> StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofSeconds(3)) - .take(2)) + .take(2, false)) .expectSubscription() .expectNoEvent(Duration.ofSeconds(3)) .expectNext(0L) @@ -1997,7 +1997,7 @@ public void lowRequestCheckCanBeDisabled() { @Test public void takeAsyncFusedBackpressured() { Sinks.Many up = Sinks.many().unicast().onBackpressureBuffer(); - StepVerifier.create(up.asFlux().take(3), 0) + StepVerifier.create(up.asFlux().take(3, false), 0) .expectFusion() .then(() -> up.emitNext("test", FAIL_FAST)) .then(() -> up.emitNext("test", FAIL_FAST)) @@ -2012,7 +2012,7 @@ public void takeAsyncFusedBackpressured() { @Test public void cancelAsyncFusion() { Sinks.Many up = Sinks.many().unicast().onBackpressureBuffer(); - StepVerifier.create(up.asFlux().take(3), 0) + StepVerifier.create(up.asFlux().take(3, false), 0) .expectFusion() .then(() -> up.emitNext("test", FAIL_FAST)) .then(() -> up.emitNext("test", FAIL_FAST)) @@ -2042,7 +2042,7 @@ public void virtualTimeSchedulerUseExactlySupplied() { public void virtualTimeSchedulerVeryLong() { StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofMillis(1)) .map(tick -> new Date()) - .take(100000) + .take(100000, false) .collectList()) .thenAwait(Duration.ofHours(1000)) .consumeNextWith(list -> assertTrue(list.size() == 100000)) @@ -2155,7 +2155,7 @@ public void gh783() { Flux interval = Flux.interval(Duration.ofSeconds(1)); return interval.map( tick -> message); }) - .take(size) + .take(size, false) .collectList() ) .thenAwait(Duration.ofHours(1)) @@ -2175,7 +2175,7 @@ public void gh783_deferredAdvanceTime() { Flux interval = Flux.interval(Duration.ofSeconds(1)); return interval.map( tick -> message); }, 30,1) - .take(size) + .take(size, false) .collectList() ) .thenAwait(Duration.ofHours(2)) @@ -2200,7 +2200,7 @@ public void gh783_withInnerFlatmap() { .subscribeOn(parallel)) .subscribeOn(parallel); }, 1,30) - .take(size) + .take(size, false) .collectList() ) .thenAwait(Duration.ofMillis(1500 * (size + 10))) @@ -2211,7 +2211,7 @@ public void gh783_withInnerFlatmap() { @Test public void gh783_intervalFullyEmitted() { - StepVerifier.withVirtualTime(() -> Flux.just("foo").flatMap(message -> Flux.interval(Duration.ofMinutes(5)).take(12))) + StepVerifier.withVirtualTime(() -> Flux.just("foo").flatMap(message -> Flux.interval(Duration.ofMinutes(5)).take(12, false))) .expectSubscription() .expectNoEvent(Duration.ofMinutes(5)) .expectNext(0L) @@ -2225,7 +2225,7 @@ public void gh783_intervalFullyEmitted() { @Test public void gh783_firstSmallAdvance() { - StepVerifier.withVirtualTime(() -> Flux.just("foo").flatMap(message -> Flux.interval(Duration.ofMinutes(5)).take(12))) + StepVerifier.withVirtualTime(() -> Flux.just("foo").flatMap(message -> Flux.interval(Duration.ofMinutes(5)).take(12, false))) .expectSubscription() .expectNoEvent(Duration.ofMinutes(3)) .thenAwait(Duration.ofHours(1)) diff --git a/reactor-test/src/test/java/reactor/test/publisher/ColdTestPublisherTests.java b/reactor-test/src/test/java/reactor/test/publisher/ColdTestPublisherTests.java index c9550f86ac..bfa1d131e4 100644 --- a/reactor-test/src/test/java/reactor/test/publisher/ColdTestPublisherTests.java +++ b/reactor-test/src/test/java/reactor/test/publisher/ColdTestPublisherTests.java @@ -307,10 +307,10 @@ public void expectMinRequestedFailure() { public void expectMaxRequestedNormal() { TestPublisher publisher = TestPublisher.createCold(); - Flux.from(publisher).limitRequest(5).subscribe(); + Flux.from(publisher).take(5).subscribe(); publisher.assertMaxRequested(5); - Flux.from(publisher).limitRequest(10).subscribe(); + Flux.from(publisher).take(10).subscribe(); publisher.assertSubscribers(2); publisher.assertMaxRequested(10); } @@ -319,7 +319,7 @@ public void expectMaxRequestedNormal() { public void expectMaxRequestedWithUnbounded() { TestPublisher publisher = TestPublisher.createCold(); - Flux.from(publisher).limitRequest(5).subscribe(); + Flux.from(publisher).take(5).subscribe(); publisher.assertMaxRequested(5); Flux.from(publisher).subscribe(); @@ -331,7 +331,7 @@ public void expectMaxRequestedWithUnbounded() { public void expectMaxRequestedFailure() { TestPublisher publisher = TestPublisher.createCold(); - Flux.from(publisher).limitRequest(7).subscribe(); + Flux.from(publisher).take(7).subscribe(); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> publisher.assertMaxRequested(6)) diff --git a/reactor-test/src/test/java/reactor/test/publisher/DefaultTestPublisherTests.java b/reactor-test/src/test/java/reactor/test/publisher/DefaultTestPublisherTests.java index 44e82a969a..74231a7ba8 100644 --- a/reactor-test/src/test/java/reactor/test/publisher/DefaultTestPublisherTests.java +++ b/reactor-test/src/test/java/reactor/test/publisher/DefaultTestPublisherTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -333,10 +333,10 @@ public void expectMinRequestedFailure() { public void expectMaxRequestedNormal() { TestPublisher publisher = TestPublisher.create(); - Flux.from(publisher).limitRequest(5).subscribe(); + Flux.from(publisher).take(5).subscribe(); publisher.assertMaxRequested(5); - Flux.from(publisher).limitRequest(10).subscribe(); + Flux.from(publisher).take(10).subscribe(); publisher.assertSubscribers(2); publisher.assertMaxRequested(10); } @@ -345,7 +345,7 @@ public void expectMaxRequestedNormal() { public void expectMaxRequestedWithUnbounded() { TestPublisher publisher = TestPublisher.create(); - Flux.from(publisher).limitRequest(5).subscribe(); + Flux.from(publisher).take(5).subscribe(); publisher.assertMaxRequested(5); Flux.from(publisher).subscribe(); @@ -357,7 +357,7 @@ public void expectMaxRequestedWithUnbounded() { public void expectMaxRequestedFailure() { TestPublisher publisher = TestPublisher.create(); - Flux.from(publisher).limitRequest(7).subscribe(); + Flux.from(publisher).take(7).subscribe(); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> publisher.assertMaxRequested(6)) From ee9c15f4f9d8542a39a46b3a5d51c3ab26d3ac66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 22 Mar 2022 10:15:46 +0100 Subject: [PATCH 011/312] [release] Prepare and release 3.5.0-M1 --- README.md | 10 +++++----- gradle.properties | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5284ab6eb7..25f11db963 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.4.16" - testCompile "io.projectreactor:reactor-test:3.4.16" + compile "io.projectreactor:reactor-core:3.5.0-M1" + testCompile "io.projectreactor:reactor-test:3.5.0-M1" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.4.17-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.4.17-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.0-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.4.16" + // implementation "io.projectreactor:reactor-tools:3.5.0-M1" } ``` diff --git a/gradle.properties b/gradle.properties index d074d28df0..c33216bf2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=3.5.0-SNAPSHOT -bomVersion=2022.0.0-SNAPSHOT \ No newline at end of file +version=3.5.0-M1 +bomVersion=2022.0.0-M1 \ No newline at end of file From 748fc2b93b2e9c8534effe66a1f2962132d71cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 22 Mar 2022 10:47:08 +0100 Subject: [PATCH 012/312] [release] Next development version 3.5.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c33216bf2e..5e71b9b453 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=3.5.0-M1 +version=3.5.0-SNAPSHOT bomVersion=2022.0.0-M1 \ No newline at end of file From 490a1e5f993e0c2a83f3055e7c883a0ce4fd9e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 8 Apr 2022 16:43:09 +0200 Subject: [PATCH 013/312] Switch to Micrometer 2 (snapshots) and adapt where needed (#3006) - use the micrometer 2.0 SNAPSHOT bom - configure Renovate to entirely ignore micrometer - add micrometer-binders dependency (ExecutorService instrumentation has moved) - add micrometer-commons dependency in preparation of Tag(s) moving there - adapt SchedulerMetricsDecorator to these elements Note that `Metrics` considers that Micrometer is active by mere detection of the `MeterRegistry`, which might not be sufficient for schedulers notably now. --- .github/renovate.json | 2 +- gradle/libs.versions.toml | 6 +++++- reactor-core/build.gradle | 10 ++++++++-- .../core/scheduler/SchedulerMetricDecorator.java | 8 ++++---- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index bbb32c1ccb..a9ed55b2dd 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -60,7 +60,7 @@ "matchPackageNames": ["io.micrometer:micrometer-core"], "groupName": "Micrometer 1.3.0", "groupSlug": "micrometer", - "allowedVersions": "=1.3.0" + "enabled": false } ] } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 917fa381e1..edce29338b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ asciidoctor = "3.3.2" bytebuddy = "1.12.8" jmh = "1.35" junit = "5.8.2" +micrometer = "2.0.0-SNAPSHOT" reactiveStreams = "1.0.3" [libraries] @@ -26,7 +27,10 @@ jsr166backport = "io.projectreactor:jsr166:1.0.0.RELEASE" jsr305 = "com.google.code.findbugs:jsr305:3.0.1" junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } logback = "ch.qos.logback:logback-classic:1.2.11" -micrometer = "io.micrometer:micrometer-core:1.3.0" +micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } +micrometer-binders = { module = "io.micrometer:micrometer-binders" } +micrometer-commons = { module = "io.micrometer:micrometer-commons" } +micrometer-core = { module = "io.micrometer:micrometer-core" } mockito = "org.mockito:mockito-core:4.4.0" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } reactiveStreams-tck = { module = "org.reactivestreams:reactive-streams-tck", version.ref = "reactiveStreams" } diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 6f344a6346..8dbbf727a7 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -83,7 +83,10 @@ dependencies { testCompileOnly libs.slf4j // Optional Metrics - compileOnly libs.micrometer + compileOnly platform(libs.micrometer.bom) + compileOnly libs.micrometer.commons + compileOnly libs.micrometer.core + compileOnly libs.micrometer.binders // Optional BlockHound support compileOnly libs.blockhound @@ -112,7 +115,10 @@ dependencies { // withMicrometerTest is a test-set that validates what happens when micrometer *IS* // on the classpath. Needs sourceSets.test.output because tests there use helpers like AutoDisposingRule etc. - withMicrometerTestImplementation libs.micrometer + withMicrometerTestImplementation platform(libs.micrometer.bom) + withMicrometerTestImplementation libs.micrometer.binders + withMicrometerTestImplementation libs.micrometer.commons + withMicrometerTestImplementation libs.micrometer.core withMicrometerTestImplementation sourceSets.test.output jcstressImplementation(project(":reactor-test")) { diff --git a/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java b/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java index ca787be73e..489a9d7663 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,9 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; +import io.micrometer.binder.jvm.ExecutorServiceMetrics; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; +import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.search.Search; import reactor.core.Disposable; @@ -73,7 +73,7 @@ public synchronized ScheduledExecutorService apply(Scheduler scheduler, Schedule executorDifferentiator.computeIfAbsent(scheduler, key -> new AtomicInteger(0)) .getAndIncrement(); - Tags tags = Tags.of(TAG_SCHEDULER_ID, schedulerId); + Tag[] tags = new Tag[] { Tag.of(TAG_SCHEDULER_ID, schedulerId) }; /* Design note: we assume that a given Scheduler won't apply the decorator twice to the From c758a364f4920a646a20ff3fe6e7d8b82ae1a36c Mon Sep 17 00:00:00 2001 From: tmyksj <33417830+tmyksj@users.noreply.github.com> Date: Mon, 11 Apr 2022 20:13:40 +0900 Subject: [PATCH 014/312] Increase landscape width of refguide content for readability (#3009) Fixes #2784. --- docs/asciidoc/stylesheets/reactor.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/asciidoc/stylesheets/reactor.css b/docs/asciidoc/stylesheets/reactor.css index db787ac77d..1cde818103 100644 --- a/docs/asciidoc/stylesheets/reactor.css +++ b/docs/asciidoc/stylesheets/reactor.css @@ -822,7 +822,7 @@ p a > code:hover { margin-right: auto; margin-top: 0; margin-bottom: 0; - max-width: 62.5em; + max-width: max(80em, 60%); *zoom: 1; position: relative; padding-left: 4em; From cfa3ff2553d57634f19f95809b90a13a6654e147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 13 Apr 2022 11:34:59 +0200 Subject: [PATCH 015/312] Micrometer reverted split of binders (#3014) --- gradle/libs.versions.toml | 1 - reactor-core/build.gradle | 2 -- .../java/reactor/core/scheduler/SchedulerMetricDecorator.java | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69a63aa3f9..57c72d3d30 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,6 @@ jsr305 = "com.google.code.findbugs:jsr305:3.0.1" junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } -micrometer-binders = { module = "io.micrometer:micrometer-binders" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } mockito = "org.mockito:mockito-core:4.4.0" diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 8dbbf727a7..e31d21e900 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -86,7 +86,6 @@ dependencies { compileOnly platform(libs.micrometer.bom) compileOnly libs.micrometer.commons compileOnly libs.micrometer.core - compileOnly libs.micrometer.binders // Optional BlockHound support compileOnly libs.blockhound @@ -116,7 +115,6 @@ dependencies { // withMicrometerTest is a test-set that validates what happens when micrometer *IS* // on the classpath. Needs sourceSets.test.output because tests there use helpers like AutoDisposingRule etc. withMicrometerTestImplementation platform(libs.micrometer.bom) - withMicrometerTestImplementation libs.micrometer.binders withMicrometerTestImplementation libs.micrometer.commons withMicrometerTestImplementation libs.micrometer.core withMicrometerTestImplementation sourceSets.test.output diff --git a/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java b/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java index 489a9d7663..0728a3a2ba 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java @@ -24,9 +24,9 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; -import io.micrometer.binder.jvm.ExecutorServiceMetrics; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; import io.micrometer.core.instrument.search.Search; import reactor.core.Disposable; From f7dd504ce89af59ba688cfaea95bb2eb4961d85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 15 Apr 2022 16:50:11 +0200 Subject: [PATCH 016/312] Switch Micrometer snapshots to 1.10.0-SNAPSHOT (#3016) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57c72d3d30..e342f3e5f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ asciidoctor = "3.3.2" bytebuddy = "1.12.9" jmh = "1.35" junit = "5.8.2" -micrometer = "2.0.0-SNAPSHOT" +micrometer = "1.10.0-SNAPSHOT" reactiveStreams = "1.0.3" [libraries] From df6ea67ba84ca46d941824c346d94c92db148887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 19 Apr 2022 11:24:54 +0200 Subject: [PATCH 017/312] Add 'tap', a generic side-effect/observability operator (#3013) This commit adds a new operator named `tap`, which allows to tap into signals emitted by the previous step (onSubscribe, onNext, onComplete, onError) and to the downstream Subscription (cancel, request) in a stateful manner. A `SignalListenerFactory` can be provided in order to define and capture state, both at assembly time and subscription time. The factory is invoked at subscription time for each downstream `Subscriber`, creating a dedicated `SignalListener` from the common assembly time state and the context of the subscriber. All of the signals are notified to these `SignalListener` which themselves can maintain state between each signals, unlike individual doOnXxx operators. The `SignalListener` is also notified of malformed signals, has handlers that are called after termination (doAfterComplete, doAfterError) and at the very beginning/end of the sequence (doFirst, doFinally). Exceptions thrown from the `SignalListener` handlers will cause a termination of the sequence (if possible), and no more handlers will be invoked. Instead, the #handleListenerError uncaught hook is invoked. --- .../java/reactor/core/publisher/Flux.java | 94 +- .../java/reactor/core/publisher/FluxTap.java | 332 ++++++ .../core/publisher/FluxTapFuseable.java | 287 +++++ .../java/reactor/core/publisher/Mono.java | 94 +- .../java/reactor/core/publisher/MonoTap.java | 81 ++ .../core/publisher/MonoTapFuseable.java | 80 ++ .../observability/DefaultSignalListener.java | 105 ++ .../util/observability/SignalListener.java | 203 ++++ .../observability/SignalListenerFactory.java | 64 + .../reactor/core/publisher/FluxTapTest.java | 1031 +++++++++++++++++ 10 files changed, 2369 insertions(+), 2 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/publisher/FluxTap.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/MonoTap.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java create mode 100644 reactor-core/src/main/java/reactor/util/observability/DefaultSignalListener.java create mode 100644 reactor-core/src/main/java/reactor/util/observability/SignalListener.java create mode 100644 reactor-core/src/main/java/reactor/util/observability/SignalListenerFactory.java create mode 100644 reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 4c5c9f9a79..306a44ccef 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -57,9 +57,9 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.core.publisher.FluxOnAssembly.AssemblySnapshot; import reactor.core.publisher.FluxOnAssembly.CheckpointHeavySnapshot; import reactor.core.publisher.FluxOnAssembly.CheckpointLightSnapshot; -import reactor.core.publisher.FluxOnAssembly.AssemblySnapshot; import reactor.core.publisher.FluxSink.OverflowStrategy; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Scheduler.Worker; @@ -78,6 +78,8 @@ import reactor.util.function.Tuple7; import reactor.util.function.Tuple8; import reactor.util.function.Tuples; +import reactor.util.observability.SignalListener; +import reactor.util.observability.SignalListenerFactory; import reactor.util.retry.Retry; /** @@ -8939,6 +8941,96 @@ public final Flux takeWhile(Predicate continuePredicate) { return onAssembly(new FluxTakeWhile<>(this, continuePredicate)); } + /** + * Tap into Reactive Streams signals emitted or received by this {@link Flux} and notify a stateful per-{@link Subscriber} + * {@link SignalListener}. + *

    + * Any exception thrown by the {@link SignalListener} methods causes the subscription to be cancelled + * and the subscriber to be terminated with an {@link Subscriber#onError(Throwable) onError signal} of that + * exception. Note that {@link SignalListener#doFinally(SignalType)}, {@link SignalListener#doAfterComplete()} and + * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} + * the exception. + *

    + * This simplified variant assumes the state is purely initialized within the {@link Supplier}, + * as it is called for each incoming {@link Subscriber} without additional context. + * + * @param simpleListenerGenerator the {@link Supplier} to create a new {@link SignalListener} on each subscription + * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} + * @see #tap(Function) + * @see #tap(SignalListenerFactory) + */ + public final Flux tap(Supplier> simpleListenerGenerator) { + return tap(new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher ignored) { + return null; + } + + @Override + public SignalListener createListener(Publisher ignored1, ContextView ignored2, Void ignored3) { + return simpleListenerGenerator.get(); + } + }); + } + + /** + * Tap into Reactive Streams signals emitted or received by this {@link Flux} and notify a stateful per-{@link Subscriber} + * {@link SignalListener}. + *

    + * Any exception thrown by the {@link SignalListener} methods causes the subscription to be cancelled + * and the subscriber to be terminated with an {@link Subscriber#onError(Throwable) onError signal} of that + * exception. Note that {@link SignalListener#doFinally(SignalType)}, {@link SignalListener#doAfterComplete()} and + * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} + * the exception. + *

    + * This simplified variant allows the {@link SignalListener} to be constructed for each subscription + * with access to the incoming {@link Subscriber}'s {@link ContextView}. + * + * @param listenerGenerator the {@link Function} to create a new {@link SignalListener} on each subscription + * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} + * @see #tap(Supplier) + * @see #tap(SignalListenerFactory) + */ + public final Flux tap(Function> listenerGenerator) { + return tap(new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher ignored) { + return null; + } + + @Override + public SignalListener createListener(Publisher ignored1, ContextView listenerContext, Void ignored2) { + return listenerGenerator.apply(listenerContext); + } + }); + } + + /** + * Tap into Reactive Streams signals emitted or received by this {@link Flux} and notify a stateful per-{@link Subscriber} + * {@link SignalListener} created by the provided {@link SignalListenerFactory}. + *

    + * The factory will initialize a {@link SignalListenerFactory#initializePublisherState(Publisher) state object} for + * each {@link Flux} or {@link Mono} instance it is used with, and that state will be cached and exposed for each + * incoming {@link Subscriber} in order to generate the associated {@link SignalListenerFactory#createListener(Publisher, ContextView, Object) listener}. + *

    + * Any exception thrown by the {@link SignalListener} methods causes the subscription to be cancelled + * and the subscriber to be terminated with an {@link Subscriber#onError(Throwable) onError signal} of that + * exception. Note that {@link SignalListener#doFinally(SignalType)}, {@link SignalListener#doAfterComplete()} and + * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} + * the exception. + * + * @param listenerFactory the {@link SignalListenerFactory} to create a new {@link SignalListener} on each subscription + * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} + * @see #tap(Supplier) + * @see #tap(Function) + */ + public final Flux tap(SignalListenerFactory listenerFactory) { + if (this instanceof Fuseable) { + return onAssembly(new FluxTapFuseable<>(this, listenerFactory)); + } + return onAssembly(new FluxTap<>(this, listenerFactory)); + } + /** * Return a {@code Mono} that completes when this {@link Flux} completes. * This will actively ignore the sequence and only replay completion or error signals. diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java new file mode 100644 index 0000000000..3a57782853 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Fuseable.ConditionalSubscriber; +import reactor.util.annotation.Nullable; +import reactor.util.observability.SignalListener; +import reactor.util.observability.SignalListenerFactory; + +/** + * A generic per-Subscription side effect {@link Flux} that notifies a {@link SignalListener} of most events. + * + * @author Simon Baslé + */ +final class FluxTap extends InternalFluxOperator { + + final SignalListenerFactory tapFactory; + final STATE commonTapState; + + FluxTap(Flux source, SignalListenerFactory tapFactory) { + super(source); + this.tapFactory = tapFactory; + this.commonTapState = tapFactory.initializePublisherState(source); + } + + @Override + public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) throws Throwable { + //if the SequenceObserver cannot be created, all we can do is error the subscriber. + //after it is created, in case doFirst fails we can additionally try to invoke doFinally. + //note that if the later handler also fails, then that exception is thrown. + SignalListener signalListener; + try { + //TODO replace currentContext() with contextView() when available + signalListener = tapFactory.createListener(source, actual.currentContext().readOnly(), commonTapState); + } + catch (Throwable generatorError) { + Operators.error(actual, generatorError); + return null; + } + + try { + signalListener.doFirst(); + } + catch (Throwable listenerError) { + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return null; + } + + if (actual instanceof ConditionalSubscriber) { + //noinspection unchecked + return new TapConditionalSubscriber<>((ConditionalSubscriber) actual, signalListener); + } + return new TapSubscriber<>(actual, signalListener); + } + + @Nullable + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return super.scanUnsafe(key); + } + + //TODO support onErrorContinue around listener errors + static class TapSubscriber implements InnerOperator { + + final CoreSubscriber actual; + final SignalListener listener; + + boolean done; + Subscription s; + + TapSubscriber(CoreSubscriber actual, SignalListener signalListener) { + this.actual = actual; + this.listener = signalListener; + } + + @Override + public CoreSubscriber actual() { + return this.actual; + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return s; + if (key == Attr.TERMINATED) return done; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return InnerOperator.super.scanUnsafe(key); + } + + /** + * Cancel the prepared subscription, pass the listener error to {@link SignalListener#handleListenerError(Throwable)} + * and then terminate the downstream directly with same error (without invoking any other handler). + * + * @param listenerError the exception thrown from a handler method before the subscription was set + * @param toCancel the {@link Subscription} that was prepared but not sent downstream + */ + protected void handleListenerErrorPreSubscription(Throwable listenerError, Subscription toCancel) { + toCancel.cancel(); + listener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + } + + /** + * Cancel the active subscription, pass the listener error to {@link SignalListener#handleListenerError(Throwable)} + * and then terminate the downstream directly with same error (without invoking any other handler). + * + * @param listenerError the exception thrown from a handler method + */ + protected void handleListenerErrorAndTerminate(Throwable listenerError) { + s.cancel(); + listener.handleListenerError(listenerError); + actual.onError(listenerError); //TODO wrap ? hooks ? + } + + /** + * Cancel the active subscription, pass the listener error to {@link SignalListener#handleListenerError(Throwable)}, + * combine it with the original error and then terminate the downstream directly this combined exception + * (without invoking any other handler). + * + * @param listenerError the exception thrown from a handler method + * @param originalError the exception that was about to occur when handler was invoked + */ + protected void handleListenerErrorMultipleAndTerminate(Throwable listenerError, Throwable originalError) { + s.cancel(); + listener.handleListenerError(listenerError); + RuntimeException multiple = Exceptions.multiple(listenerError, originalError); + actual.onError(multiple); //TODO wrap ? hooks ? + } + + /** + * After the downstream is considered terminated (or cancelled), pass the listener error to + * {@link SignalListener#handleListenerError(Throwable)} then drop that error. + * + * @param listenerError the exception thrown from a handler method happening after sequence termination + */ + protected void handleListenerErrorPostTermination(Throwable listenerError) { + listener.handleListenerError(listenerError); + Operators.onErrorDropped(listenerError, actual.currentContext()); + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + + try { + listener.doOnSubscription(); + } + catch (Throwable observerError) { + handleListenerErrorPreSubscription(observerError, s); + return; + } + actual.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + try { + listener.doOnMalformedOnNext(t); + } + catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } + finally { + Operators.onNextDropped(t, currentContext()); + } + return; + } + try { + listener.doOnNext(t); + } + catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); + return; + } + actual.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (done) { + try { + listener.doOnMalformedOnError(t); + } + catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } + finally { + Operators.onErrorDropped(t, currentContext()); + } + return; + } + done = true; + + try { + listener.doOnError(t); + } + catch (Throwable observerError) { + //any error in the hooks interrupts other hooks, including doFinally + handleListenerErrorMultipleAndTerminate(observerError, t); + return; + } + + actual.onError(t); //RS: onError MUST terminate normally and not throw + + try { + listener.doAfterError(t); + listener.doFinally(SignalType.ON_ERROR); + } + catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } + } + + @Override + public void onComplete() { + if (done) { + try { + listener.doOnMalformedOnComplete(); + } + catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } + return; + } + done = true; + + try { + listener.doOnComplete(); + } + catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); + return; + } + + actual.onComplete(); //RS: onComplete MUST terminate normally and not throw + + try { + listener.doAfterComplete(); + listener.doFinally(SignalType.ON_COMPLETE); + } + catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + try { + listener.doOnRequest(n); + } + catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); + return; + } + s.request(n); + } + } + + @Override + public void cancel() { + try { + listener.doOnCancel(); + } + catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); + return; + } + + try { + s.cancel(); + } + finally { + try { + listener.doFinally(SignalType.CANCEL); + } + catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); //redundant s.cancel + } + } + } + } + + static final class TapConditionalSubscriber extends TapSubscriber implements ConditionalSubscriber { + + final ConditionalSubscriber actualConditional; + + public TapConditionalSubscriber(ConditionalSubscriber actual, SignalListener signalListener) { + super(actual, signalListener); + this.actualConditional = actual; + } + + @Override + public boolean tryOnNext(T t) { + if (actualConditional.tryOnNext(t)) { + try { + listener.doOnNext(t); + } + catch (Throwable listenerError) { + handleListenerErrorAndTerminate(listenerError); + } + return true; + } + return false; + } + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java new file mode 100644 index 0000000000..35ac96368a --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Fuseable; +import reactor.util.annotation.Nullable; +import reactor.util.observability.SignalListener; +import reactor.util.observability.SignalListenerFactory; + +/** + * A {@link reactor.core.Fuseable} generic per-Subscription side effect {@link Flux} that notifies a + * {@link SignalListener} of most events. + * + * @author Simon Baslé + */ +final class FluxTapFuseable extends InternalFluxOperator implements Fuseable { + + final SignalListenerFactory tapFactory; + final STATE commonTapState; + + FluxTapFuseable(Flux source, SignalListenerFactory tapFactory) { + super(source); + this.tapFactory = tapFactory; + this.commonTapState = tapFactory.initializePublisherState(source); + } + + @Override + public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) throws Throwable { + //if the SequenceObserver cannot be created, all we can do is error the subscriber. + //after it is created, in case doFirst fails we can additionally try to invoke doFinally. + //note that if the later handler also fails, then that exception is thrown. + SignalListener signalListener; + try { + //TODO replace currentContext() with contextView() when available + signalListener = tapFactory.createListener(source, actual.currentContext().readOnly(), commonTapState); + } + catch (Throwable generatorError) { + Operators.error(actual, generatorError); + return null; + } + + try { + signalListener.doFirst(); + } + catch (Throwable listenerError) { + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return null; + } + + if (actual instanceof ConditionalSubscriber) { + //noinspection unchecked + return new TapConditionalFuseableSubscriber<>((ConditionalSubscriber) actual, signalListener); + } + return new TapFuseableSubscriber<>(actual, signalListener); + } + + @Nullable + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return super.scanUnsafe(key); + } + + //TODO support onErrorContinue around listener errors + static class TapFuseableSubscriber extends FluxTap.TapSubscriber implements QueueSubscription { + + int mode; + QueueSubscription qs; + + TapFuseableSubscriber(CoreSubscriber actual, SignalListener signalListener) { + super(actual, signalListener); + } + + /** + * Cancel the active subscription, pass the listener error to {@link SignalListener#handleListenerError(Throwable)} + * then return that same exception wrapped via {@link Exceptions#propagate(Throwable)} if needed. + * It should be immediately thrown to terminate the downstream directly from {@link #poll()} (without invoking + * any other handler). + * + * @param listenerError the exception thrown from a handler method + */ + protected RuntimeException handleObserverErrorInPoll(Throwable listenerError) { + qs.cancel(); + listener.handleListenerError(listenerError); + return Exceptions.propagate(listenerError); + } + + /** + * Cancel the active subscription, pass the listener error to {@link SignalListener#handleListenerError(Throwable)}, + * combine it with the original error and then return the combined exception. The returned exception should be + * immediately thrown to terminate the downstream directly from {@link #poll()} (without invoking any other handler). + * + * @param listenerError the exception thrown from a handler method + * @param pollError the exception that was about to be thrown from poll when handler was invoked + */ + protected RuntimeException handleObserverErrorMultipleInPoll(Throwable listenerError, RuntimeException pollError) { + qs.cancel(); + listener.handleListenerError(listenerError); + return Exceptions.multiple(listenerError, pollError); + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + if (!(s instanceof QueueSubscription)) { + handleListenerErrorPreSubscription(new IllegalStateException("Fuseable subscriber but no QueueSubscription"), s); + return; + } + this.s = s; + //noinspection unchecked + this.qs = (QueueSubscription) s; + + try { + listener.doOnSubscription(); + } + catch (Throwable listenerError) { + handleListenerErrorPreSubscription(listenerError, s); + return; + } + actual.onSubscribe(this); //should trigger requestFusion + } + } + + @Override + public int requestFusion(int requestedMode) { + if (qs == null) { + this.mode = NONE; + return NONE; + } + this.mode = qs.requestFusion(requestedMode); + + try { + listener.doOnFusion(this.mode); + return mode; + } + catch (Throwable listenerError) { + if (mode == ASYNC || mode == NONE) { + handleListenerErrorAndTerminate(listenerError); + return NONE; + } + //for SYNC, no interruption + listener.handleListenerError(listenerError); + return mode; + } + } + + @SuppressWarnings("ConstantConditions") + @Override + public void onNext(T t) { + if (this.mode == ASYNC) { + actual.onNext(null); + return; //will observe onNext events through the lens of poll() + } + super.onNext(t); + } + + @Override + @Nullable + public T poll() { + if (qs == null) { + return null; + } + T v; + //try to poll. failure means doOnError. doOnError failure is combined with original + try { + v = qs.poll(); + } + catch (RuntimeException pollError) { + try { + listener.doOnError(pollError); + } + catch (Throwable listenerError) { + throw handleObserverErrorMultipleInPoll(listenerError, pollError); + } + + //the subscription can be considered cancelled at this point + //exceptionally we invoked doFinally _before_ the propagation (since it is throwing) + try { + listener.doFinally(SignalType.ON_ERROR); + } + catch (Throwable listenerError) { + throw handleObserverErrorMultipleInPoll(listenerError, pollError); + } + throw pollError; + } + + //SYNC fusion uses null as onComplete and throws as onError + //ASYNC fusion uses classic methods + if (v == null && (this.done || mode == SYNC)) { + try { + listener.doOnComplete(); + } + catch (Throwable listenerError) { + throw handleObserverErrorInPoll(listenerError); + } + + //exceptionally doFinally will be invoked before the downstream is notified of completion (return null) + try { + listener.doFinally(SignalType.ON_COMPLETE); + } + catch (Throwable listenerError) { + throw handleObserverErrorInPoll(listenerError); + } + + //notify the downstream of completion + return null; + } + + if (v != null) { + //this is an onNext event + try { + listener.doOnNext(v); + } + catch (Throwable listenerError) { + if (mode == SYNC) { + throw handleObserverErrorInPoll(listenerError); + } + handleListenerErrorAndTerminate(listenerError); + //TODO discard the element ? + return null; + } + } + return v; + } + + @Override + public int size() { + return qs == null ? 0 : qs.size(); + } + + @Override + public boolean isEmpty() { + return qs == null || qs.isEmpty(); + } + + @Override + public void clear() { + if (qs != null) { + qs.clear(); + } + } + } + + static final class TapConditionalFuseableSubscriber extends TapFuseableSubscriber implements ConditionalSubscriber { + + final ConditionalSubscriber actualConditional; + + public TapConditionalFuseableSubscriber(ConditionalSubscriber actual, SignalListener signalListener) { + super(actual, signalListener); + this.actualConditional = actual; + } + + @Override + public boolean tryOnNext(T t) { + if (actualConditional.tryOnNext(t)) { + try { + listener.doOnNext(t); + } + catch (Throwable listenerError) { + handleListenerErrorAndTerminate(listenerError); + } + return true; + } + return false; + } + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 4fa75c2674..2c8bbd2f21 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -52,9 +52,9 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.core.publisher.FluxOnAssembly.AssemblySnapshot; import reactor.core.publisher.FluxOnAssembly.CheckpointHeavySnapshot; import reactor.core.publisher.FluxOnAssembly.CheckpointLightSnapshot; -import reactor.core.publisher.FluxOnAssembly.AssemblySnapshot; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Scheduler.Worker; import reactor.core.scheduler.Schedulers; @@ -72,6 +72,8 @@ import reactor.util.function.Tuple7; import reactor.util.function.Tuple8; import reactor.util.function.Tuples; +import reactor.util.observability.SignalListener; +import reactor.util.observability.SignalListenerFactory; import reactor.util.retry.Retry; /** @@ -4475,6 +4477,96 @@ public final Mono takeUntilOther(Publisher other) { return onAssembly(new MonoTakeUntilOther<>(this, other)); } + /** + * Tap into Reactive Streams signals emitted or received by this {@link Mono} and notify a stateful per-{@link Subscriber} + * {@link SignalListener}. + *

    + * Any exception thrown by the {@link SignalListener} methods causes the subscription to be cancelled + * and the subscriber to be terminated with an {@link Subscriber#onError(Throwable) onError signal} of that + * exception. Note that {@link SignalListener#doFinally(SignalType)}, {@link SignalListener#doAfterComplete()} and + * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} + * the exception. + *

    + * This simplified variant assumes the state is purely initialized within the {@link Supplier}, + * as it is called for each incoming {@link Subscriber} without additional context. + * + * @param simpleListenerGenerator the {@link Supplier} to create a new {@link SignalListener} on each subscription + * @return a new {@link Mono} with side effects defined by generated {@link SignalListener} + * @see #tap(Function) + * @see #tap(SignalListenerFactory) + */ + public final Mono tap(Supplier> simpleListenerGenerator) { + return tap(new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher ignored) { + return null; + } + + @Override + public SignalListener createListener(Publisher ignored1, ContextView ignored2, Void ignored3) { + return simpleListenerGenerator.get(); + } + }); + } + + /** + * Tap into Reactive Streams signals emitted or received by this {@link Mono} and notify a stateful per-{@link Subscriber} + * {@link SignalListener}. + *

    + * Any exception thrown by the {@link SignalListener} methods causes the subscription to be cancelled + * and the subscriber to be terminated with an {@link Subscriber#onError(Throwable) onError signal} of that + * exception. Note that {@link SignalListener#doFinally(SignalType)}, {@link SignalListener#doAfterComplete()} and + * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} + * the exception. + *

    + * This simplified variant allows the {@link SignalListener} to be constructed for each subscription + * with access to the incoming {@link Subscriber}'s {@link ContextView}. + * + * @param listenerGenerator the {@link Function} to create a new {@link SignalListener} on each subscription + * @return a new {@link Mono} with side effects defined by generated {@link SignalListener} + * @see #tap(Supplier) + * @see #tap(SignalListenerFactory) + */ + public final Mono tap(Function> listenerGenerator) { + return tap(new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher ignored) { + return null; + } + + @Override + public SignalListener createListener(Publisher ignored1, ContextView listenerContext, Void ignored2) { + return listenerGenerator.apply(listenerContext); + } + }); + } + + /** + * Tap into Reactive Streams signals emitted or received by this {@link Mono} and notify a stateful per-{@link Subscriber} + * {@link SignalListener} created by the provided {@link SignalListenerFactory}. + *

    + * The factory will initialize a {@link SignalListenerFactory#initializePublisherState(Publisher) state object} for + * each {@link Flux} or {@link Mono} instance it is used with, and that state will be cached and exposed for each + * incoming {@link Subscriber} in order to generate the associated {@link SignalListenerFactory#createListener(Publisher, ContextView, Object) listener}. + *

    + * Any exception thrown by the {@link SignalListener} methods causes the subscription to be cancelled + * and the subscriber to be terminated with an {@link Subscriber#onError(Throwable) onError signal} of that + * exception. Note that {@link SignalListener#doFinally(SignalType)}, {@link SignalListener#doAfterComplete()} and + * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} + * the exception. + * + * @param listenerFactory the {@link SignalListenerFactory} to create a new {@link SignalListener} on each subscription + * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} + * @see #tap(Supplier) + * @see #tap(Function) + */ + public final Mono tap(SignalListenerFactory listenerFactory) { + if (this instanceof Fuseable) { + return onAssembly(new MonoTapFuseable<>(this, listenerFactory)); + } + return onAssembly(new MonoTap<>(this, listenerFactory)); + } + /** * Return a {@code Mono} which only replays complete and error signals * from this {@link Mono}. diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java new file mode 100644 index 0000000000..a924da75f5 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.publisher.FluxTap.TapSubscriber; +import reactor.util.annotation.Nullable; +import reactor.util.observability.SignalListener; +import reactor.util.observability.SignalListenerFactory; + +/** + * A generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. + * + * @author Simon Baslé + */ +final class MonoTap extends InternalMonoOperator { + + final SignalListenerFactory tapFactory; + final STATE commonTapState; + + MonoTap(Mono source, SignalListenerFactory tapFactory) { + super(source); + this.tapFactory = tapFactory; + this.commonTapState = tapFactory.initializePublisherState(source); + } + + @Override + public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) throws Throwable { + //if the SequenceObserver cannot be created, all we can do is error the subscriber. + //after it is created, in case doFirst fails we can additionally try to invoke doFinally. + //note that if the later handler also fails, then that exception is thrown. + SignalListener signalListener; + try { + //TODO replace currentContext() with contextView() when available + signalListener = tapFactory.createListener(source, actual.currentContext().readOnly(), commonTapState); + } + catch (Throwable generatorError) { + Operators.error(actual, generatorError); + return null; + } + + try { + signalListener.doFirst(); + } + catch (Throwable listenerError) { + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return null; + } + + if (actual instanceof Fuseable.ConditionalSubscriber) { + //noinspection unchecked + return new FluxTap.TapConditionalSubscriber<>((Fuseable.ConditionalSubscriber) actual, signalListener); + } + return new TapSubscriber<>(actual, signalListener); + } + + @Nullable + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PREFETCH) return -1; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return super.scanUnsafe(key); + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java new file mode 100644 index 0000000000..2fe0099672 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.util.annotation.Nullable; +import reactor.util.observability.SignalListener; +import reactor.util.observability.SignalListenerFactory; + +/** + * A {@link Fuseable} generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. + * + * @author Simon Baslé + */ +final class MonoTapFuseable extends InternalMonoOperator implements Fuseable { + + final SignalListenerFactory tapFactory; + final STATE commonTapState; + + MonoTapFuseable(Mono source, SignalListenerFactory tapFactory) { + super(source); + this.tapFactory = tapFactory; + this.commonTapState = tapFactory.initializePublisherState(source); + } + + @Override + public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) throws Throwable { + //if the SequenceObserver cannot be created, all we can do is error the subscriber. + //after it is created, in case doFirst fails we can additionally try to invoke doFinally. + //note that if the later handler also fails, then that exception is thrown. + SignalListener signalListener; + try { + //TODO replace currentContext() with contextView() when available + signalListener = tapFactory.createListener(source, actual.currentContext().readOnly(), commonTapState); + } + catch (Throwable generatorError) { + Operators.error(actual, generatorError); + return null; + } + + try { + signalListener.doFirst(); + } + catch (Throwable listenerError) { + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return null; + } + + if (actual instanceof ConditionalSubscriber) { + //noinspection unchecked + return new FluxTapFuseable.TapConditionalFuseableSubscriber<>((ConditionalSubscriber) actual, signalListener); + } + return new FluxTapFuseable.TapFuseableSubscriber<>(actual, signalListener); + } + + @Nullable + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PREFETCH) return -1; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return super.scanUnsafe(key); + } +} diff --git a/reactor-core/src/main/java/reactor/util/observability/DefaultSignalListener.java b/reactor-core/src/main/java/reactor/util/observability/DefaultSignalListener.java new file mode 100644 index 0000000000..a9f3c1c051 --- /dev/null +++ b/reactor-core/src/main/java/reactor/util/observability/DefaultSignalListener.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.util.observability; + +import reactor.core.Fuseable; +import reactor.core.publisher.SignalType; + +/** + * A default implementation of a {@link SignalListener} with all the handlers no-op. + * + * @author Simon Baslé + */ +public abstract class DefaultSignalListener implements SignalListener { + + /** + * This listener actually captures the negotiated fusion + * mode (if any) and exposes this information to child classes via #getFusionMode(). + */ + int fusionMode = Fuseable.NONE; + + @Override + public void doFirst() throws Throwable { + } + + @Override + public void doFinally(SignalType terminationType) throws Throwable { + } + + @Override + public void doOnSubscription() throws Throwable { + } + + @Override + public void doOnFusion(int negotiatedFusion) throws Throwable { + this.fusionMode = negotiatedFusion; + } + + /** + * Return the fusion mode negotiated with the source: {@link Fuseable#SYNC} and {@link Fuseable#ASYNC}) as relevant + * if some fusion was negotiated. {@link Fuseable#NONE} if fusion was never requested, or if it couldn't be negotiated. + * + * @return the negotiated fusion mode, if any + */ + protected int getFusionMode() { + return fusionMode; + } + + @Override + public void doOnRequest(long requested) throws Throwable { + } + + @Override + public void doOnCancel() throws Throwable { + } + + @Override + public void doOnNext(T value) throws Throwable { + } + + @Override + public void doOnComplete() throws Throwable { + } + + @Override + public void doOnError(Throwable error) throws Throwable { + } + + @Override + public void doAfterComplete() throws Throwable { + } + + @Override + public void doAfterError(Throwable error) throws Throwable { + } + + @Override + public void doOnMalformedOnNext(T value) throws Throwable { + } + + @Override + public void doOnMalformedOnComplete() throws Throwable { + } + + @Override + public void doOnMalformedOnError(Throwable error) throws Throwable { + } + + @Override + public void handleListenerError(Throwable listenerError) { + } +} diff --git a/reactor-core/src/main/java/reactor/util/observability/SignalListener.java b/reactor-core/src/main/java/reactor/util/observability/SignalListener.java new file mode 100644 index 0000000000..4ab7ded5a1 --- /dev/null +++ b/reactor-core/src/main/java/reactor/util/observability/SignalListener.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.util.observability; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.core.publisher.SignalType; +import reactor.util.context.Context; + +/** + * A listener which combines various handlers to be triggered per the corresponding {@link Flux} or {@link Mono} signals. + * This is similar to the "side effect" operators in {@link Flux} and {@link Mono}, but in a single listener class. + * {@link SignalListener} are created by a {@link SignalListenerFactory}, which is tied to a particular {@link Publisher}. + * Each time a new {@link Subscriber} subscribes to that {@link Publisher}, the factory creates an associated {@link SignalListener}. + *

    + * Both publisher-to-subscriber events and subscription events are handled. Methods are closer to the side-effect doOnXxx operators + * than to {@link Subscriber} and {@link Subscription} methods, in order to avoid misconstruing this for an actual Reactive Streams + * implementation. The actual downstream {@link Subscriber} and upstream {@link Subscription} are intentionally not exposed + * to avoid any influence on the observed sequence. + * + * @author Simon Baslé + */ +public interface SignalListener { + + /** + * Handle the very beginning of the {@link Subscriber}-{@link Publisher} interaction. + * This handler is invoked right before subscribing to the parent {@link Publisher}, as a downstream + * {@link Subscriber} has called {@link Publisher#subscribe(Subscriber)}. + *

    + * Once the {@link Publisher} has acknowledged with a {@link Subscription}, the {@link #doOnSubscription()} + * handler will be invoked before that {@link Subscription} is passed down. + * + * @see #doOnSubscription() + */ + void doFirst() throws Throwable; + + /** + * Handle terminal signals after the signals have been propagated, as the final step. + * Only {@link SignalType#ON_COMPLETE}, {@link SignalType#ON_ERROR} or {@link SignalType#CANCEL} can be passed. + * This handler is invoked AFTER the terminal signal has been propagated, and if relevant AFTER the {@link #doAfterComplete()} + * or {@link #doAfterError(Throwable)} events. If any doOnXxx handler throws, this handler is NOT invoked (see {@link #handleListenerError(Throwable)} + * instead). + * + * @see #handleListenerError(Throwable) + */ + void doFinally(SignalType terminationType) throws Throwable; + + /** + * Handle the fact that the upstream {@link Publisher} acknowledged {@link Subscription}. + * The {@link Subscription} is intentionally not exposed in order to avoid manipulation by the observer. + *

    + * While {@link #doFirst} is invoked right as the downstream {@link Subscriber} is registered, + * this method is invoked as the upstream answers back with a {@link Subscription} (and before that + * same {@link Subscription} is passed downstream). + * + * @see #doFirst() + */ + void doOnSubscription() throws Throwable; + + /** + * Handle the negotiation of fusion between two {@link reactor.core.Fuseable} operators. As the downstream operator + * requests fusion, the upstream answers back with the compatible level of fusion it can handle. This {@code negotiatedFusion} + * code is passed to this handler right before it is propagated downstream. + * + * @param negotiatedFusion the final fusion mode negotiated by the upstream operator in response to a fusion request + * from downstream + */ + void doOnFusion(int negotiatedFusion) throws Throwable; + + /** + * Handle a new request made by the downstream, exposing the demand. + *

    + * This is invoked before the request is propagated upstream. + * + * @param requested the downstream demand + */ + void doOnRequest(long requested) throws Throwable; + + /** + * Handle the downstream cancelling its currently observed {@link Subscription}. + *

    + * This handler is invoked before propagating the cancellation upstream, while {@link #doFinally(SignalType)} + * is invoked right after the cancellation has been propagated upstream. + * + * @see #doFinally(SignalType) + */ + void doOnCancel() throws Throwable; + + /** + * Handle a new value emission from the source. + *

    + * This handler is invoked before propagating the value downstream. + * + * @param value the emitted value + */ + void doOnNext(T value) throws Throwable; + + /** + * Handle graceful onComplete sequence termination. + *

    + * This handler is invoked before propagating the completion downstream, while both + * {@link #doAfterComplete()} and {@link #doFinally(SignalType)} are invoked after. + * + * @see #doAfterComplete() + * @see #doFinally(SignalType) + */ + void doOnComplete() throws Throwable; + + /** + * Handle onError sequence termination. + *

    + * This handler is invoked before propagating the error downstream, while both + * {@link #doAfterError(Throwable)} and {@link #doFinally(SignalType)} are invoked after. + * + * @param error the exception that terminated the sequence + * @see #doAfterError(Throwable) + * @see #doFinally(SignalType) + */ + void doOnError(Throwable error) throws Throwable; + + /** + * Handle graceful onComplete sequence termination, after onComplete has been propagated downstream. + *

    + * This handler is invoked after propagating the completion downstream, similar to {@link #doFinally(SignalType)} + * and unlike {@link #doOnComplete()}. + */ + void doAfterComplete() throws Throwable; + + /** + * Handle onError sequence termination after onError has been propagated downstream. + *

    + * This handler is invoked after propagating the error downstream, similar to {@link #doFinally(SignalType)} + * and unlike {@link #doOnError(Throwable)}. + * + * @param error the exception that terminated the sequence + */ + void doAfterError(Throwable error) throws Throwable; + + /** + * Handle malformed {@link Subscriber#onNext(Object)}, which are onNext happening after the sequence has already terminated + * via {@link Subscriber#onComplete()} or {@link Subscriber#onError(Throwable)}. + * Note that after this handler is invoked, the value is automatically {@link Operators#onNextDropped(Object, Context) dropped}. + *

    + * If this handler fails with an exception, that exception is {@link Operators#onErrorDropped(Throwable, Context) dropped} before the + * value is also dropped. + * + * @param value the value for which an emission was attempted (which will be automatically dropped afterwards) + */ + void doOnMalformedOnNext(T value) throws Throwable; + + /** + * Handle malformed {@link Subscriber#onError(Throwable)}, which means the sequence has already terminated + * via {@link Subscriber#onComplete()} or {@link Subscriber#onError(Throwable)}. + * Note that after this handler is invoked, the exception is automatically {@link Operators#onErrorDropped(Throwable, Context) dropped}. + *

    + * If this handler fails with an exception, that exception is {@link Operators#onErrorDropped(Throwable, Context) dropped} before the + * original onError exception is also dropped. + * + * @param error the extraneous exception (which will be automatically dropped afterwards) + */ + void doOnMalformedOnError(Throwable error) throws Throwable; + + /** + * Handle malformed {@link Subscriber#onComplete()}, which means the sequence has already terminated + * via {@link Subscriber#onComplete()} or {@link Subscriber#onError(Throwable)}. + *

    + * If this handler fails with an exception, that exception is {@link Operators#onErrorDropped(Throwable, Context) dropped}. + */ + void doOnMalformedOnComplete() throws Throwable; + + /** + * A special handler for exceptions thrown from all the other handlers. + * This method MUST return normally, i.e. it MUST NOT throw. + * When a {@link SignalListener} handler fails, callers are expected to first invoke this method then to propagate + * the {@code listenerError} downstream if that is possible, terminating the original sequence with the listenerError. + *

    + * Typically, this special handler is intended for a last chance at processing the error despite the fact that + * {@link #doFinally(SignalType)} is not triggered on handler errors. For example, recording the error in a + * metrics backend or cleaning up state that would otherwise be cleaned up by {@link #doFinally(SignalType)}. + * + * @param listenerError the exception thrown from a {@link SignalListener} handler method + */ + void handleListenerError(Throwable listenerError); +} diff --git a/reactor-core/src/main/java/reactor/util/observability/SignalListenerFactory.java b/reactor-core/src/main/java/reactor/util/observability/SignalListenerFactory.java new file mode 100644 index 0000000000..e259c1626a --- /dev/null +++ b/reactor-core/src/main/java/reactor/util/observability/SignalListenerFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.util.observability; + +import org.reactivestreams.Publisher; + +import reactor.util.context.ContextView; + +/** + * A factory for per-subscription {@link SignalListener}, exposing the ability to generate common state at publisher level + * from the source {@link Publisher}. + *

    + * Examples of such state include: + *

      + *
    • is the publisher a Mono? (unlocking Mono-specific behavior in the {@link SignalListener}
    • + *
    • resolution of NAME and TAGS on the source
    • + *
    • preparation of a common configuration, like a registry for metrics
    • + *
    + * + * @param the type of data emitted by the observed source {@link Publisher} + * @param the type of the publisher-level state that will be shared between all {@link SignalListener} created by this factory + * @author Simon Baslé + */ +public interface SignalListenerFactory { + + /** + * Create the {@code STATE} object for the given {@link Publisher}. This assumes this factory will only be used on + * that particular source, allowing all {@link SignalListener} created by this factory to inherit the common state. + * + * @param source the source {@link Publisher} this factory is attached to. + * @return the common state + */ + STATE initializePublisherState(Publisher source); + + /** + * Create a new {@link SignalListener} each time a new {@link org.reactivestreams.Subscriber} subscribes to the + * {@code source} {@link Publisher}. + *

    + * The {@code source} {@link Publisher} is the same as the one that triggered common state creation at assembly time in + * {@link #initializePublisherState(Publisher)}). Said common state is passed to this method as well, and so is the + * {@link ContextView} for the newly registered {@link reactor.core.CoreSubscriber}. + * + * @param source the source {@link Publisher} that is being subscribed to + * @param listenerContext the {@link ContextView} associated with the new subscriber + * @param publisherContext the common state initialized in {@link #initializePublisherState(Publisher)} + * @return a stateful {@link SignalListener} observing signals to and from the new subscriber + */ + SignalListener createListener(Publisher source, ContextView listenerContext, STATE publisherContext); + +} diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java new file mode 100644 index 0000000000..cc6f5b1d28 --- /dev/null +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java @@ -0,0 +1,1031 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.Fuseable.ConditionalSubscriber; +import reactor.core.Scannable; +import reactor.core.Scannable.Attr; +import reactor.core.Scannable.Attr.RunStyle; +import reactor.test.ParameterizedTestWithName; +import reactor.test.publisher.TestPublisher; +import reactor.test.subscriber.TestSubscriber; +import reactor.util.context.ContextView; +import reactor.util.observability.SignalListener; +import reactor.util.observability.SignalListenerFactory; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Simon Baslé + */ +class FluxTapTest { + + private static class TestSignalListener implements SignalListener { + + /** + * The String representation of the events, or doOnXxx methods. + */ + public final Deque events = new ConcurrentLinkedDeque<>(); + + /** + * The errors passed to the {@link #handleListenerError(Throwable)} hook. Unused by default. + */ + public final Deque listenerErrors = new ConcurrentLinkedDeque<>(); + + @Override + public void doFirst() throws Throwable { + events.offer("doFirst"); + } + + @Override + public void doFinally(SignalType terminationType) throws Throwable { + events.offer("doFinally:" + terminationType.name()); + } + + @Override + public void doOnSubscription() throws Throwable { + events.offer("doOnSubscription"); + } + + @Override + public void doOnFusion(int negotiatedFusion) throws Throwable { + events.offer("doOnFusion:" + Fuseable.fusionModeName(negotiatedFusion)); + } + + @Override + public void doOnRequest(long requested) throws Throwable { + events.offer("doOnRequest:" + (requested == Long.MAX_VALUE ? "unbounded" : requested)); + } + + @Override + public void doOnCancel() throws Throwable { + events.offer("doOnCancel"); + } + + @Override + public void doOnNext(T value) throws Throwable { + events.offer("doOnNext:" + value); + } + + @Override + public void doOnComplete() throws Throwable { + events.offer("doOnComplete"); + } + + @Override + public void doOnError(Throwable error) throws Throwable { + events.offer("doOnError:" + error); + } + + @Override + public void doAfterComplete() throws Throwable { + events.offer("doAfterComplete"); + } + + @Override + public void doAfterError(Throwable error) throws Throwable { + events.offer("doAfterError:" + error); + } + + @Override + public void doOnMalformedOnNext(T value) throws Throwable { + events.offer("doOnMalformedOnNext:" + value); + } + + @Override + public void doOnMalformedOnError(Throwable error) throws Throwable { + events.offer("doOnMalformedOnError:" + error); + } + + @Override + public void doOnMalformedOnComplete() throws Throwable { + events.offer("doOnMalformedOnComplete"); + } + + @Override + public void handleListenerError(Throwable listenerError) { + listenerErrors.offer(listenerError); + } + } + + private static final TestSingletonFactory factoryOf(TestSignalListener listener) { + return new TestSingletonFactory<>(listener); + } + + private static TestSingletonFactory ignoredFactory() { + return new TestSingletonFactory<>(new TestSignalListener()); + } + + private static final class TestSingletonFactory implements SignalListenerFactory { + + final TestSignalListener singleton; + + private TestSingletonFactory(TestSignalListener singleton) { + this.singleton = singleton; + } + + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, ContextView listenerContext, + Void publisherContext) { + return singleton; + } + } + + @Test + void scenarioTerminatingOnComplete() { + TestSignalListener testSignalListener = new TestSignalListener<>(); + + Flux fullFlux = Flux.just(1, 2, 3).hide(); + + fullFlux.tap(() -> testSignalListener) + .subscribeWith(TestSubscriber.create()); + + assertThat(testSignalListener.listenerErrors).as("listener errors").isEmpty(); + + assertThat(testSignalListener.events) + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:unbounded", + "doOnNext:1", + "doOnNext:2", + "doOnNext:3", + "doOnComplete", + "doAfterComplete", + "doFinally:ON_COMPLETE" + ); + } + + @Test + void scenarioTerminatingOnError() { + TestSignalListener testSignalListener = new TestSignalListener<>(); + RuntimeException expectedError = new RuntimeException("expected"); + + Flux fullFlux = Flux.just(1, 2, 3).concatWith(Mono.error(expectedError)).hide(); + + fullFlux.tap(() -> testSignalListener) + .subscribeWith(TestSubscriber.create()); + + assertThat(testSignalListener.listenerErrors).as("listener errors").isEmpty(); + + assertThat(testSignalListener.events) + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:unbounded", + "doOnNext:1", + "doOnNext:2", + "doOnNext:3", + "doOnError:" + expectedError, + "doAfterError:" + expectedError, + "doFinally:ON_ERROR" + ); + } + + @Test + void multipleRequests() { + TestSignalListener testSignalListener = new TestSignalListener<>(); + TestSubscriber testSubscriber = TestSubscriber.builder().initialRequest(0L).build(); + + Flux fullFlux = Flux.just(1, 2, 3).hide(); + + fullFlux.tap(() -> testSignalListener) + .subscribeWith(testSubscriber); + + assertThat(testSignalListener.listenerErrors).as("listener errors").isEmpty(); + + assertThat(testSignalListener.events) + .as("before first request") + .containsExactly( + "doFirst", + "doOnSubscription" + ); + + testSubscriber.request(1); + assertThat(testSignalListener.events) + .as("first request") + .hasSize(4) + .endsWith( + "doOnRequest:1", + "doOnNext:1" + ); + + testSubscriber.request(1); + assertThat(testSignalListener.events) + .as("second request") + .hasSize(6) + .endsWith( + "doOnRequest:1", + "doOnNext:2" + ); + + testSubscriber.request(10); + assertThat(testSignalListener.events) + .as("final request") + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:1", + "doOnNext:1", + "doOnRequest:1", + "doOnNext:2", + "doOnRequest:10", + "doOnNext:3", + "doOnComplete", + "doAfterComplete", + "doFinally:ON_COMPLETE" + ); + } + + @Test + void withCancellation() { + TestSignalListener testSignalListener = new TestSignalListener<>(); + TestSubscriber testSubscriber = TestSubscriber.builder().initialRequest(0L).build(); + + Flux fullFlux = Flux.just(1, 2, 3).hide(); + + fullFlux.tap(() -> testSignalListener) + .subscribeWith(testSubscriber); + + assertThat(testSignalListener.listenerErrors).as("listener errors").isEmpty(); + + testSubscriber.request(2); + + assertThat(testSignalListener.events) + .as("partial request") + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:2", + "doOnNext:1", + "doOnNext:2" + ); + + testSubscriber.cancel(); + + assertThat(testSignalListener.events) + .as("cancelled") + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:2", + "doOnNext:1", + "doOnNext:2", + "doOnCancel", + "doFinally:CANCEL" + ); + } + + @ParameterizedTestWithName + @ValueSource(strings = {"ON_COMPLETE", "ON_ERROR"}) + void malformedOnNext(SignalType termination) { + AtomicReference dropped = new AtomicReference<>(); + Hooks.onNextDropped(dropped::set); + + TestSignalListener testSignalListener = new TestSignalListener<>(); + TestPublisher testPublisher = TestPublisher.createNoncompliant(TestPublisher.Violation.CLEANUP_ON_TERMINATE); + TestSubscriber ignored = TestSubscriber.create(); + + testPublisher.flux().hide() + .tap(() -> testSignalListener) + .subscribeWith(ignored); + + testPublisher.next(1, 2, 3); + + if (termination == SignalType.ON_COMPLETE) { + testPublisher.complete(); + testPublisher.next(-1); + + assertThat(testSignalListener.events) + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:unbounded", + "doOnNext:1", + "doOnNext:2", + "doOnNext:3", + "doOnComplete", + "doAfterComplete", + "doFinally:ON_COMPLETE", + "doOnMalformedOnNext:-1" + ); + } + else { + Throwable errorTermination = new RuntimeException("onError termination"); + testPublisher.error(errorTermination); + testPublisher.next(-1); + + + assertThat(testSignalListener.events) + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:unbounded", + "doOnNext:1", + "doOnNext:2", + "doOnNext:3", + "doOnError:" + errorTermination, + "doAfterError:" + errorTermination, + "doFinally:ON_ERROR", + "doOnMalformedOnNext:-1" + ); + } + + assertThat(testSignalListener.listenerErrors).as("listener errors").isEmpty(); + assertThat(dropped).as("dropped onNext").hasValue(-1); + } + + @ParameterizedTestWithName + @ValueSource(strings = {"ON_COMPLETE", "ON_ERROR"}) + void malformedOnComplete(SignalType termination) { + TestSignalListener testSignalListener = new TestSignalListener<>(); + TestPublisher testPublisher = TestPublisher.createNoncompliant(TestPublisher.Violation.CLEANUP_ON_TERMINATE); + TestSubscriber ignored = TestSubscriber.create(); + + testPublisher.flux().hide() + .tap(() -> testSignalListener) + .subscribeWith(ignored); + + testPublisher.next(1, 2, 3); + + if (termination == SignalType.ON_COMPLETE) { + testPublisher.complete(); + testPublisher.complete(); + + assertThat(testSignalListener.events) + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:unbounded", + "doOnNext:1", + "doOnNext:2", + "doOnNext:3", + "doOnComplete", + "doAfterComplete", + "doFinally:ON_COMPLETE", + "doOnMalformedOnComplete" + ); + } + else { + Exception errorTermination = new RuntimeException("onError termination"); + testPublisher.error(errorTermination); + testPublisher.complete(); + + assertThat(testSignalListener.events) + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:unbounded", + "doOnNext:1", + "doOnNext:2", + "doOnNext:3", + "doOnError:" + errorTermination, + "doAfterError:" + errorTermination, + "doFinally:ON_ERROR", + "doOnMalformedOnComplete" + ); + } + + assertThat(testSignalListener.listenerErrors).as("listener errors").isEmpty(); + } + + @ParameterizedTestWithName + @ValueSource(strings = {"ON_COMPLETE", "ON_ERROR"}) + void malformedOnError(SignalType termination) { + AtomicReference dropped = new AtomicReference<>(); + Hooks.onErrorDropped(dropped::set); + + TestSignalListener testSignalListener = new TestSignalListener<>(); + TestPublisher testPublisher = TestPublisher.createNoncompliant(TestPublisher.Violation.CLEANUP_ON_TERMINATE); + TestSubscriber ignored = TestSubscriber.create(); + Throwable malformedError = new RuntimeException("expected malformed onError"); + + testPublisher.flux().hide() + .tap(() -> testSignalListener) + .subscribeWith(ignored); + + testPublisher.next(1, 2, 3); + + if (termination == SignalType.ON_COMPLETE) { + testPublisher.complete(); + testPublisher.error(malformedError); + + assertThat(testSignalListener.events) + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:unbounded", + "doOnNext:1", + "doOnNext:2", + "doOnNext:3", + "doOnComplete", + "doAfterComplete", + "doFinally:ON_COMPLETE", + "doOnMalformedOnError:" + malformedError + ); + } + else { + Exception errorTermination = new RuntimeException("onError termination"); + testPublisher.error(errorTermination); + testPublisher.error(malformedError); + + assertThat(testSignalListener.events) + .containsExactly( + "doFirst", + "doOnSubscription", + "doOnRequest:unbounded", + "doOnNext:1", + "doOnNext:2", + "doOnNext:3", + "doOnError:" + errorTermination, + "doAfterError:" + errorTermination, + "doFinally:ON_ERROR", + "doOnMalformedOnError:" + malformedError + ); + } + + assertThat(testSignalListener.listenerErrors).as("listener errors").isEmpty(); + assertThat(dropped).as("malformed error was dropped").hasValue(malformedError); + } + + + @Test + void throwingCreateListener() { + TestSubscriber testSubscriber = TestSubscriber.create(); + FluxTap test = new FluxTap<>(Flux.just(1), + new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + throw new IllegalStateException("expected"); + } + }); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(testSubscriber.expectTerminalError()) + .as("downstream error") + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected"); + } + + @Test + void doFirstListenerError() { + Throwable listenerError = new IllegalStateException("expected from doFirst"); + + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener listener = new TestSignalListener() { + @Override + public void doFirst() throws Throwable { + throw listenerError; + } + }; + + FluxTap test = new FluxTap<>(Flux.just(1), factoryOf(listener)); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(listener.listenerErrors) + .as("listenerErrors") + .containsExactly(listenerError); + + assertThat(listener.events) + .as("events") + .isEmpty(); + } + + @Nested + class FluxTapFuseableTest { + + @Test + void implementationSmokeTest() { + Flux fuseableSource = Flux.just(1); + //the TestSubscriber "requireFusion" configuration below is intentionally inverted + //so that an exception describing the actual Subscription is thrown when calling block() + TestSubscriber testSubscriberForFuseable = TestSubscriber.builder().requireNotFuseable().build(); + Flux fuseable = fuseableSource.tap(TestSignalListener::new); + + assertThat(fuseableSource).as("smoke test fuseableSource").isInstanceOf(Fuseable.class); + assertThat(fuseable).as("fuseable").isInstanceOf(FluxTapFuseable.class); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> fuseable.subscribeWith(testSubscriberForFuseable).block()) + .as("TapFuseableSubscriber") + .withMessageContaining("got reactor.core.publisher.FluxTapFuseable$TapFuseableSubscriber"); + } + + @Test + void throwingCreateListener() { + TestSubscriber testSubscriber = TestSubscriber.create(); + FluxTapFuseable test = new FluxTapFuseable<>(Flux.just(1), + new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + throw new IllegalStateException("expected"); + } + }); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(testSubscriber.expectTerminalError()) + .as("downstream error") + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected"); + } + + //doFirst is invoked from each publisher's subscribeOrReturn + @Test + void doFirst() { + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener listener = new TestSignalListener<>(); + + FluxTapFuseable test = new FluxTapFuseable<>(Flux.just(1), factoryOf(listener)); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(listener.listenerErrors).as("listenerErrors").isEmpty(); + assertThat(listener.events) + .as("events") + .containsExactly( + "doFirst" + ); + } + + @Test + void doFirstListenerError() { + Throwable listenerError = new IllegalStateException("expected from doFirst"); + + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener listener = new TestSignalListener() { + @Override + public void doFirst() throws Throwable { + throw listenerError; + } + }; + + FluxTapFuseable test = new FluxTapFuseable<>(Flux.just(1), factoryOf(listener)); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(listener.listenerErrors) + .as("listenerErrors") + .containsExactly(listenerError); + + assertThat(listener.events) + .as("events") + .isEmpty(); + } + + //TODO test clear/size/isEmpty + + //TODO test poll + + //TODO test ASYNC fusion onNext + + //TODO test SYNC fusion onNext + } + + @Nested + class FluxTapConditionalTest { + + @Test + void implementationSmokeTest() { + Flux normalSource = Flux.just(1).hide(); + //the TestSubscriber "requireFusion" configuration below is intentionally inverted + //so that an exception describing the actual Subscription is thrown when calling block() + TestSubscriber conditionalTestSubscriber = TestSubscriber.builder().requireFusion(2).buildConditional(i -> true); + Flux normal = normalSource.tap(TestSignalListener::new); + + assertThat(normalSource).as("smoke test normal source").isNotInstanceOf(Fuseable.class); + assertThat(normal).as("normal").isInstanceOf(FluxTap.class); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> normal.subscribeWith(conditionalTestSubscriber).block()) + .as("TapConditionalSubscriber") + .withMessageContaining("got reactor.core.publisher.FluxTap$TapConditionalSubscriber"); + } + + //TODO test tryOnNext + } + + @Nested + class FluxTapConditionalFuseableTest { + + //TODO test tryOnNext + + @Test + void implementationSmokeTest() { + Flux fuseableSource = Flux.just(1); + //the TestSubscriber "requireFusion" configuration below is intentionally inverted + //so that an exception describing the actual Subscription is thrown when calling block() + TestSubscriber conditionalTestSubscriberForFuseable = TestSubscriber.builder().requireNotFuseable().buildConditional(i -> true); + Flux fuseable = fuseableSource.tap(TestSignalListener::new); + + assertThat(fuseableSource).as("smoke test fuseableSource").isInstanceOf(Fuseable.class); + assertThat(fuseable).as("fuseable").isInstanceOf(FluxTapFuseable.class); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> fuseable.subscribeWith(conditionalTestSubscriberForFuseable).block()) + .as("TapConditionalFuseableSubscriber") + .withMessageContaining("got reactor.core.publisher.FluxTapFuseable$TapConditionalFuseableSubscriber"); + } + } + + @Nested + class MonoTapTest { + + @Test + void subscriberImplementationsFromFluxTap() { + Mono normalSource = Mono.just(1).hide(); + + assertThat(normalSource).as("smoke test normalSource").isNotInstanceOf(Fuseable.class); + + Mono normal = normalSource.tap(TestSignalListener::new); + + assertThat(normal).as("normal").isInstanceOf(MonoTap.class); + + //the TestSubscriber "requireFusion" configuration below are intentionally inverted + //so that an exception describing the actual Subscription is thrown when calling block() + TestSubscriber testSubscriberForNormal = TestSubscriber.builder().requireFusion(2).build(); + TestSubscriber testSubscriberForNormalConditional = TestSubscriber.builder().requireFusion(2).buildConditional(i -> true); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> normal.subscribeWith(testSubscriberForNormal).block()) + .as("TapSubscriber") + .withMessageContaining("got reactor.core.publisher.FluxTap$TapSubscriber"); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> normal.subscribeWith(testSubscriberForNormalConditional).block()) + .as("TapConditionalSubscriber") + .withMessageContaining("got reactor.core.publisher.FluxTap$TapConditionalSubscriber"); + } + + @Test + void throwingCreateListener() { + TestSubscriber testSubscriber = TestSubscriber.create(); + MonoTap test = new MonoTap<>(Mono.just(1), + new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + throw new IllegalStateException("expected"); + } + }); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(testSubscriber.expectTerminalError()) + .as("downstream error") + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected"); + } + + //doFirst is invoked from each publisher's subscribeOrReturn + @Test + void doFirst() { + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener listener = new TestSignalListener<>(); + + MonoTap test = new MonoTap<>(Mono.just(1), factoryOf(listener)); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(listener.listenerErrors).as("listenerErrors").isEmpty(); + assertThat(listener.events) + .as("events") + .containsExactly( + "doFirst" + ); + } + + @Test + void doFirstListenerError() { + Throwable listenerError = new IllegalStateException("expected from doFirst"); + + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener listener = new TestSignalListener() { + @Override + public void doFirst() throws Throwable { + throw listenerError; + } + }; + + MonoTap test = new MonoTap<>(Mono.just(1), factoryOf(listener)); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(listener.listenerErrors) + .as("listenerErrors") + .containsExactly(listenerError); + + assertThat(listener.events) + .as("events") + .isEmpty(); + } + } + + @Nested + class MonoTapFuseableTest { + + @Test + void subscriberImplementationsFromFluxTapFuseable() { + Mono fuseableSource = Mono.just(1); + + assertThat(fuseableSource).as("smoke test fuseableSource").isInstanceOf(Fuseable.class); + + Mono fuseable = fuseableSource.tap(TestSignalListener::new); + + assertThat(fuseable).as("fuseable").isInstanceOf(MonoTapFuseable.class); + + //the TestSubscriber "requireFusion" configuration below are intentionally inverted + //so that an exception describing the actual Subscription is thrown when calling block() + TestSubscriber testSubscriberForFuseable = TestSubscriber.builder().requireNotFuseable().build(); + TestSubscriber testSubscriberForFuseableConditional = TestSubscriber.builder().requireNotFuseable().buildConditional(i -> true); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> fuseable.subscribeWith(testSubscriberForFuseable).block()) + .as("TapFuseableSubscriber") + .withMessageContaining("got reactor.core.publisher.FluxTapFuseable$TapFuseableSubscriber"); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> fuseable.subscribeWith(testSubscriberForFuseableConditional).block()) + .as("TapFuseableConditionalSubscriber") + .withMessageContaining("got reactor.core.publisher.FluxTapFuseable$TapConditionalFuseableSubscriber"); + } + + @Test + void throwingCreateListener() { + TestSubscriber testSubscriber = TestSubscriber.create(); + MonoTapFuseable test = new MonoTapFuseable<>(Mono.just(1), + new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + throw new IllegalStateException("expected"); + } + }); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(testSubscriber.expectTerminalError()) + .as("downstream error") + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected"); + } + + //doFirst is invoked from each publisher's subscribeOrReturn + @Test + void doFirst() { + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener listener = new TestSignalListener<>(); + + MonoTapFuseable test = new MonoTapFuseable<>(Mono.just(1), factoryOf(listener)); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(listener.listenerErrors).as("listenerErrors").isEmpty(); + assertThat(listener.events) + .as("events") + .containsExactly( + "doFirst" + ); + } + + @Test + void doFirstListenerError() { + Throwable listenerError = new IllegalStateException("expected from doFirst"); + + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener listener = new TestSignalListener() { + @Override + public void doFirst() throws Throwable { + throw listenerError; + } + }; + + MonoTapFuseable test = new MonoTapFuseable<>(Mono.just(1), factoryOf(listener)); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(listener.listenerErrors) + .as("listenerErrors") + .containsExactly(listenerError); + + assertThat(listener.events) + .as("events") + .isEmpty(); + } + } + + @Nested + class TapScannableTest { + + @Test + void scanFluxTap() { + Flux source = Flux.just(1); + FluxTap testPublisher = new FluxTap<>(source, ignoredFactory()); + + Scannable test = Scannable.from(testPublisher); + assertThat(test).isSameAs(testPublisher) + .matches(Scannable::isScanAvailable, "isScanAvailable"); + + assertThat(test.scan(Attr.PREFETCH)).as("PREFETCH").isEqualTo(-1); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isEqualTo(RunStyle.SYNC); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(source); + } + + @Test + void scanFluxTapFuseable() { + Flux source = Flux.just(1); + FluxTapFuseable testPublisher = new FluxTapFuseable<>(source, ignoredFactory()); + + Scannable test = Scannable.from(testPublisher); + assertThat(test).isSameAs(testPublisher) + .matches(Scannable::isScanAvailable, "isScanAvailable"); + + assertThat(test.scan(Attr.PREFETCH)).as("PREFETCH").isEqualTo(-1); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isEqualTo(RunStyle.SYNC); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(source); + } + + @Test + void scanMonoListen() { + Mono source = Mono.just(1); + MonoTap testPublisher = new MonoTap<>(source, ignoredFactory()); + + Scannable test = Scannable.from(testPublisher); + assertThat(test).isSameAs(testPublisher) + .matches(Scannable::isScanAvailable, "isScanAvailable"); + + assertThat(test.scan(Attr.PREFETCH)).as("PREFETCH").isEqualTo(-1); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isEqualTo(RunStyle.SYNC); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(source); + } + + @Test + void scanMonoListenFuseable() { + Mono source = Mono.just(1); + MonoTapFuseable testPublisher = new MonoTapFuseable<>(source, ignoredFactory()); + + Scannable test = Scannable.from(testPublisher); + assertThat(test).isSameAs(testPublisher) + .matches(Scannable::isScanAvailable, "isScanAvailable"); + + assertThat(test.scan(Attr.PREFETCH)).as("PREFETCH").isEqualTo(-1); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isEqualTo(RunStyle.SYNC); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(source); + } + + @Test + void scanListenSubscriber() { + CoreSubscriber actual = Operators.drainSubscriber(); + Subscription subscription = Operators.emptySubscription(); + + FluxTap.TapSubscriber subscriber = new FluxTap.TapSubscriber<>( + actual, new TestSignalListener<>()); + + subscriber.onSubscribe(subscription); + + Scannable test = Scannable.from(subscriber); + assertThat(test.isScanAvailable()).as("isScanAvailable").isTrue(); + assertThat(test).isSameAs(subscriber); + + assertThat(test.scanUnsafe(Attr.ACTUAL)).as("ACTUAL").isSameAs(actual); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(subscription); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isSameAs(RunStyle.SYNC); + + subscriber.onComplete(); + assertThat(test.scan(Attr.TERMINATED)).as("TERMINATED").isTrue(); + } + + @Test + void scanListenConditionalSubscriber() { + ConditionalSubscriber actual = Operators.toConditionalSubscriber(Operators.drainSubscriber()); + Subscription subscription = Operators.emptySubscription(); + + FluxTap.TapConditionalSubscriber subscriber = new FluxTap.TapConditionalSubscriber<>( + actual, new TestSignalListener<>()); + + subscriber.onSubscribe(subscription); + + Scannable test = Scannable.from(subscriber); + assertThat(test.isScanAvailable()).as("isScanAvailable").isTrue(); + assertThat(test).isSameAs(subscriber); + + assertThat(test.scanUnsafe(Attr.ACTUAL)).as("ACTUAL").isSameAs(actual); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(subscription); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isSameAs(RunStyle.SYNC); + + subscriber.onComplete(); + assertThat(test.scan(Attr.TERMINATED)).as("TERMINATED").isTrue(); + } + + @Test + void scanListenFuseableSubscriber() { + CoreSubscriber actual = Operators.drainSubscriber(); + Subscription subscription = Operators.emptySubscription(); + + FluxTapFuseable.TapFuseableSubscriber subscriber = new FluxTapFuseable.TapFuseableSubscriber<>( + actual, new TestSignalListener<>()); + + subscriber.onSubscribe(subscription); + + Scannable test = Scannable.from(subscriber); + assertThat(test.isScanAvailable()).as("isScanAvailable").isTrue(); + assertThat(test).isSameAs(subscriber); + + assertThat(test.scanUnsafe(Attr.ACTUAL)).as("ACTUAL").isSameAs(actual); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(subscription); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isSameAs(RunStyle.SYNC); + + subscriber.onComplete(); + assertThat(test.scan(Attr.TERMINATED)).as("TERMINATED").isTrue(); + } + + @Test + void scanListenConditionalFuseableSubscriber() { + ConditionalSubscriber actual = Operators.toConditionalSubscriber(Operators.drainSubscriber()); + Subscription subscription = Operators.emptySubscription(); + + FluxTapFuseable.TapConditionalFuseableSubscriber + subscriber = new FluxTapFuseable.TapConditionalFuseableSubscriber<>( + actual, new TestSignalListener<>()); + + subscriber.onSubscribe(subscription); + + Scannable test = Scannable.from(subscriber); + assertThat(test.isScanAvailable()).as("isScanAvailable").isTrue(); + assertThat(test).isSameAs(subscriber); + + assertThat(test.scanUnsafe(Attr.ACTUAL)).as("ACTUAL").isSameAs(actual); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(subscription); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isSameAs(RunStyle.SYNC); + + subscriber.onComplete(); + assertThat(test.scan(Attr.TERMINATED)).as("TERMINATED").isTrue(); + } + } +} \ No newline at end of file From cbd3913a0299f2f37a7683165416d99d4559cda9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 20 Apr 2022 09:43:19 +0200 Subject: [PATCH 018/312] Introduce new module reactor-core-micrometer (#3015) The new module introduces an alternative more explicit way of bringing metrics in reactor-core, both for Schedulers and for Flux and Mono. The entry point is the `Micrometer` class. The following metrics-related features in core are superseded: - global configuration previously done in core's `Metrics` - scheduler `ExecutorService` instrumentation previously done in `Scheduler#enableMetrics` - Flux/Mono `metrics()` operators replaced by `tap` + the factory from the new `Micrometer.metrics()` method As a consequence, these metrics-related classes and methods in core are deprecated, to be removed in 3.6.0 at the earliest. Note that most logic needed to be duplicated or reproduced in the new module. The new module lives in the core repository and shares the same groupId as reactor-core (io.projectreactor). It is dedicated to Micrometer instrumentation for reactor-core. The new module brings an explicit dependency to Micrometer 1.10+, which means that the old `Metrics.isInstrumentationAvailable` will always be `true` when the module is on the classpath. Extra care should be taken to avoid activating metrics via both the core features and their module equivalents. --- build.gradle | 36 +- gradle.properties | 1 + reactor-core-micrometer/build.gradle | 85 ++++ .../observability/micrometer/Micrometer.java | 133 ++++++ .../micrometer/MicrometerListener.java | 316 ++++++++++++++ .../MicrometerListenerConfiguration.java | 132 ++++++ .../micrometer/MicrometerListenerFactory.java | 62 +++ .../MicrometerSchedulerMetricsDecorator.java | 223 ++++++++++ .../micrometer/package-info.java | 23 + .../MicrometerListenerConfigurationTest.java | 239 +++++++++++ .../MicrometerListenerFactoryTest.java | 121 ++++++ .../micrometer/MicrometerListenerTest.java | 405 ++++++++++++++++++ .../micrometer/MicrometerTest.java | 95 ++++ .../src/test/resources/logback.xml | 32 ++ .../observability/DefaultSignalListener.java | 2 +- .../observability/SignalListener.java | 2 +- .../observability/SignalListenerFactory.java | 2 +- .../java/reactor/core/publisher/Flux.java | 17 +- .../reactor/core/publisher/FluxMetrics.java | 3 +- .../core/publisher/FluxMetricsFuseable.java | 3 +- .../java/reactor/core/publisher/FluxTap.java | 4 +- .../core/publisher/FluxTapFuseable.java | 4 +- .../java/reactor/core/publisher/Mono.java | 17 +- .../reactor/core/publisher/MonoMetrics.java | 24 +- .../core/publisher/MonoMetricsFuseable.java | 10 +- .../java/reactor/core/publisher/MonoTap.java | 4 +- .../core/publisher/MonoTapFuseable.java | 4 +- .../scheduler/SchedulerMetricDecorator.java | 1 + .../reactor/core/scheduler/Schedulers.java | 5 + .../src/main/java/reactor/util/Metrics.java | 30 +- .../reactor/core/publisher/FluxTapTest.java | 4 +- .../reactor/util/MetricsNoMicrometerTest.java | 5 +- .../java/reactor/util/MetricsTest.java | 3 +- settings.gradle | 2 +- 34 files changed, 1979 insertions(+), 70 deletions(-) create mode 100644 reactor-core-micrometer/build.gradle create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListener.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerFactory.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerSchedulerMetricsDecorator.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/package-info.java create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerFactoryTest.java create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerTest.java create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java create mode 100644 reactor-core-micrometer/src/test/resources/logback.xml rename reactor-core/src/main/java/reactor/{util => core}/observability/DefaultSignalListener.java (98%) rename reactor-core/src/main/java/reactor/{util => core}/observability/SignalListener.java (99%) rename reactor-core/src/main/java/reactor/{util => core}/observability/SignalListenerFactory.java (98%) diff --git a/build.gradle b/build.gradle index 686f36837b..cae8e649e9 100644 --- a/build.gradle +++ b/build.gradle @@ -49,30 +49,36 @@ repositories { //needed at root for asciidoctor and nohttp-checkstyle mavenCentral() } -ext { - jdk = JavaVersion.current().majorVersion - jdkJavadoc = "https://docs.oracle.com/javase/$jdk/docs/api/" - if (JavaVersion.current().isJava11Compatible()) { - jdkJavadoc = "https://docs.oracle.com/en/java/javase/$jdk/docs/api/" - } - println "JDK Javadoc link for this build is ${rootProject.jdkJavadoc}" - - versionNumber = VersionNumber.parse(version.toString()) +def osgiVersion(String v) { + def versionNumber = VersionNumber.parse(v) + def result if (versionNumber.qualifier == null || versionNumber.qualifier.size() == 0) { - osgiVersion = "${version}.RELEASE" - println "$version is a release, will use $osgiVersion for bnd" + result = "${v}.RELEASE" + println "$v is a release, will use $result for bnd" } else if (versionNumber.qualifier.equalsIgnoreCase("SNAPSHOT")) { def sdf = new SimpleDateFormat("yyyyMMddHHmm"); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); def buildTimestamp = sdf.format(new Date()) - osgiVersion = "${versionNumber.major}.${versionNumber.minor}.${versionNumber.micro}.BUILD-$buildTimestamp" - println "$version is a snapshot, will use $osgiVersion for bnd" + result = "${versionNumber.major}.${versionNumber.minor}.${versionNumber.micro}.BUILD-$buildTimestamp" + println "$v is a snapshot, will use $result for bnd" } else { - osgiVersion = "${versionNumber.major}.${versionNumber.minor}.${versionNumber.micro}.${versionNumber.qualifier}" - println "$version is neither release nor snapshot, will use $osgiVersion for bnd" + result = "${versionNumber.major}.${versionNumber.minor}.${versionNumber.micro}.${versionNumber.qualifier}" + println "$v is neither release nor snapshot, will use $result for bnd" + } + return result +} + +ext { + jdk = JavaVersion.current().majorVersion + jdkJavadoc = "https://docs.oracle.com/javase/$jdk/docs/api/" + if (JavaVersion.current().isJava11Compatible()) { + jdkJavadoc = "https://docs.oracle.com/en/java/javase/$jdk/docs/api/" } + println "JDK Javadoc link for this build is ${rootProject.jdkJavadoc}" + + osgiVersion = osgiVersion(version.toString()) /* * Note that all dependencies and their versions are now defined in diff --git a/gradle.properties b/gradle.properties index f84baad527..35fcc5d7a7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ version=3.5.0-SNAPSHOT bomVersion=2022.0.0-M1 +metricsMicrometerVersion=1.0.0-SNAPSHOT \ No newline at end of file diff --git a/reactor-core-micrometer/build.gradle b/reactor-core-micrometer/build.gradle new file mode 100644 index 0000000000..363ab47888 --- /dev/null +++ b/reactor-core-micrometer/build.gradle @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'biz.aQute.bnd.builder' +apply plugin: 'java-library' + +description = 'Reactor-Core Micrometer Metrics support' + +version = "$metricsMicrometerVersion" +group = "io.projectreactor" + +ext { + def osgiVersion = osgiVersion("$metricsMicrometerVersion") + + bndOptions = [ + "Export-Package": [ + "!*internal*", + "reactor.core.observability.micrometer*;version=$osgiVersion;-noimport:=true" + ].join(","), + "Import-Package": [ + "!javax.annotation", + "*" + ].join(","), + "Bundle-Name" : "reactor-core-micrometer", + "Bundle-SymbolicName" : "io.projectreactor.reactor-core-micrometer", + "Bundle-Version" : "$osgiVersion" + ] +} + +dependencies { + api project(":reactor-core") + compileOnly libs.jsr305 + + implementation platform(libs.micrometer.bom) + api libs.micrometer.core + + testImplementation platform(libs.junit.bom) + testImplementation "org.junit.jupiter:junit-jupiter-api" + testImplementation "org.junit.platform:junit-platform-launcher" + testImplementation "org.junit.jupiter:junit-jupiter-params" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" + + testImplementation platform(libs.micrometer.bom) + testImplementation libs.micrometer.core + + testImplementation(project(":reactor-test")) { + exclude module: 'reactor-core' + } + // Needs sourceSets.test.output because tests there use helpers like AutoDisposingRule etc. + testImplementation project(":reactor-core").sourceSets.test.output + + testRuntimeOnly libs.logback + testImplementation libs.assertj + testImplementation libs.mockito +} + +tasks.withType(Test).all { + useJUnitPlatform() +} + +// javadoc is configured in gradle/javadoc.gradle + +jar { + manifest { + attributes 'Implementation-Title': 'reactor-core-micrometer', + 'Implementation-Version': project.version, + 'Automatic-Module-Name': 'reactor.core.micrometer' + } + bnd(bndOptions) +} + +//TODO once 1.0.0 is released, introduce JAPICMP checks \ No newline at end of file diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java new file mode 100644 index 0000000000..f50a96e11e --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; + +import reactor.core.observability.SignalListener; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.core.observability.SignalListenerFactory; + +public final class Micrometer { + + private static final String SCHEDULERS_DECORATOR_KEY = "reactor.core.observability.micrometer.schedulerDecorator"; + private static MeterRegistry registry = Metrics.globalRegistry; + + /** + * The default "name" to use as a prefix for meter IDs if the instrumented sequence doesn't + * define a {@link reactor.core.publisher.Flux#name(String) name}. + */ + public static final String DEFAULT_METER_PREFIX = "reactor"; + + /** + * Set the registry to use in reactor for metrics related purposes. + * @return the previously configured registry. + */ + public static MeterRegistry useRegistry(MeterRegistry newRegistry) { + MeterRegistry previous = registry; + registry = newRegistry; + return previous; + } + + /** + * Get the registry used in reactor for metrics related purposes. + */ + public static MeterRegistry getRegistry() { + return registry; + } + + /** + * A {@link SignalListener} factory that will ultimately produce Micrometer metrics + * to the configured default {@link #getRegistry() registry}. + * To be used with either the {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} or + * {@link reactor.core.publisher.Mono#tap(SignalListenerFactory)} operator. + *

    + * When used in a {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} operator, meter names use + * the {@link reactor.core.publisher.Flux#name(String)} set upstream of the tap as id prefix if applicable + * or default to {@link #DEFAULT_METER_PREFIX}. Similarly, upstream tags are gathered and added + * to the default set of tags for meters. + *

    + * Note that some monitoring systems like Prometheus require to have the exact same set of + * tags for each meter bearing the same name. + * + * @param the type of onNext in the target publisher + * @return a {@link SignalListenerFactory} to record metrics + */ + public static SignalListenerFactory metrics() { + return new MicrometerListenerFactory<>(); + } + + /** + * A {@link SignalListener} factory that will ultimately produce Micrometer metrics + * to the provided {@link MeterRegistry} using the provided {@link Clock} for timings. + * To be used with either the {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} or + * {@link reactor.core.publisher.Mono#tap(SignalListenerFactory)} operator. + *

    + * When used in a {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} operator, meter names use + * the {@link reactor.core.publisher.Flux#name(String)} set upstream of the tap as id prefix if applicable + * or default to {@link #DEFAULT_METER_PREFIX}. Similarly, upstream tags are gathered and added + * to the default set of tags for meters. + *

    + * Note that some monitoring systems like Prometheus require to have the exact same set of + * tags for each meter bearing the same name. + * + * @param the type of onNext in the target publisher + * @return a {@link SignalListenerFactory} to record metrics + */ + public static SignalListenerFactory metrics(MeterRegistry registry, Clock clock) { + return new MicrometerListenerFactory() { + @Override + protected Clock useClock() { + return clock; + } + + @Override + protected MeterRegistry useRegistry() { + return registry; + } + }; + } + + /** + * Set-up a decorator that will instrument any {@link ExecutorService} that backs a reactor-core {@link Scheduler} + * (or scheduler implementations which use {@link Schedulers#decorateExecutorService(Scheduler, ScheduledExecutorService)}). + *

    + * The {@link MeterRegistry} to use can be configured via {@link #useRegistry(MeterRegistry)} + * prior to using this method, the default being {@link io.micrometer.core.instrument.Metrics#globalRegistry}. + * + * @implNote Note that this is added as a decorator via Schedulers when enabling metrics for schedulers, + * which doesn't change the Factory. + */ + public static void enableSchedulersMetricsDecorator() { + Schedulers.addExecutorServiceDecorator(SCHEDULERS_DECORATOR_KEY, + new MicrometerSchedulerMetricsDecorator(getRegistry())); + } + + /** + * If {@link #enableSchedulersMetricsDecorator()} has been previously called, removes the decorator. + * No-op if {@link #enableSchedulersMetricsDecorator()} hasn't been called. + */ + public static void disableSchedulersMetricsDecorator() { + Schedulers.removeExecutorServiceDecorator(SCHEDULERS_DECORATOR_KEY); + } +} \ No newline at end of file diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListener.java new file mode 100644 index 0000000000..576a9a1082 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListener.java @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.SignalType; +import reactor.util.annotation.Nullable; +import reactor.core.observability.SignalListener; + +/** + * A {@link SignalListener} that activates metrics gathering using Micrometer 1.x. + * + * @author Simon Baslé + */ +final class MicrometerListener implements SignalListener { + + final MicrometerListenerConfiguration configuration; + @Nullable + final DistributionSummary requestedCounter; + @Nullable + final Timer onNextIntervalTimer; + + Timer.Sample subscribeToTerminateSample; + long lastNextEventNanos = -1L; + boolean valued; + + MicrometerListener(MicrometerListenerConfiguration configuration) { + this.configuration = configuration; + + this.valued = false; + if (configuration.isMono) { + //for Mono we don't record onNextInterval (since there is at most 1 onNext). + //Note that we still need a mean to distinguish between empty Mono and valued Mono for recordOnCompleteEmpty + onNextIntervalTimer = null; + //we also don't count the number of request calls (there should be only one) + requestedCounter = null; + } + else { + this.onNextIntervalTimer = Timer.builder(configuration.sequenceName + METER_ON_NEXT_DELAY) + .tags(configuration.commonTags) + .description( + "Measures delays between onNext signals (or between onSubscribe and first onNext)") + .register(configuration.registry); + + if (!Micrometer.DEFAULT_METER_PREFIX.equals(configuration.sequenceName)) { + this.requestedCounter = DistributionSummary.builder(configuration.sequenceName + METER_REQUESTED) + .tags(configuration.commonTags) + .description( + "Counts the amount requested to a named Flux by all subscribers, until at least one requests an unbounded amount") + .register(configuration.registry); + } + else { + requestedCounter = null; + } + } + } + + @Override + public void doOnCancel() { + //we don't record the time between last onNext and cancel, + // because it would skew the onNext count by one + recordCancel(configuration.sequenceName, configuration.commonTags, configuration.registry, subscribeToTerminateSample); + } + + @Override + public void doOnComplete() { + //we don't record the time between last onNext and onComplete, + // because it would skew the onNext count by one. + // We differentiate between empty completion and value completion, however, via tags. + if (!valued) { + recordOnCompleteEmpty(configuration.sequenceName, configuration.commonTags, configuration.registry, subscribeToTerminateSample); + } + else if (!configuration.isMono) { + //recordOnComplete is done directly in onNext for the Mono(valued) case + recordOnComplete(configuration.sequenceName, configuration.commonTags, configuration.registry, subscribeToTerminateSample); + } + } + + @Override + public void doOnMalformedOnComplete() { + recordMalformed(configuration.sequenceName, configuration.commonTags, configuration.registry); + } + + @Override + public void doOnError(Throwable e) { + //we don't record the time between last onNext and onError, + // because it would skew the onNext count by one + recordOnError(configuration.sequenceName, configuration.commonTags, configuration.registry, subscribeToTerminateSample, e); + } + + @Override + public void doOnMalformedOnError(Throwable e) { + recordMalformed(configuration.sequenceName, configuration.commonTags, configuration.registry); + } + + @Override + public void doOnNext(T t) { + valued = true; + if (onNextIntervalTimer == null) { //NB: interval timer is only null if isMono + //record valued completion directly + recordOnComplete(configuration.sequenceName, configuration.commonTags, configuration.registry, subscribeToTerminateSample); + return; + } + //record the delay since previous onNext/onSubscribe. This also records the count. + long last = this.lastNextEventNanos; + this.lastNextEventNanos = configuration.clock.monotonicTime(); + this.onNextIntervalTimer.record(lastNextEventNanos - last, TimeUnit.NANOSECONDS); + } + + @Override + public void doOnMalformedOnNext(T value) { + recordMalformed(configuration.sequenceName, configuration.commonTags, configuration.registry); + } + + @Override + public void doOnSubscription() { + recordOnSubscribe(configuration.sequenceName, configuration.commonTags, configuration.registry); + this.subscribeToTerminateSample = Timer.start(configuration.clock); + this.lastNextEventNanos = configuration.clock.monotonicTime(); + } + + @Override + public void doOnRequest(long l) { + if (requestedCounter != null) { + requestedCounter.record(l); + } + } + + //unused hooks + + @Override + public void doFirst() { + // NO-OP + } + + @Override + public void doOnFusion(int negotiatedFusion) throws Throwable { + // NO-OP + //TODO metrics counting fused (with ASYNC/SYNC tags) could be implemented to supplement METER_SUBSCRIBED + } + + @Override + public void doFinally(SignalType terminationType) { + // NO-OP + } + + @Override + public void doAfterComplete() { + // NO-OP + } + + @Override + public void doAfterError(Throwable error) { + // NO-OP + } + + @Override + public void handleListenerError(Throwable listenerError) { + // NO-OP + } + + /** + * Meter that counts the number of events received from a malformed source (ie an onNext after an onComplete). + */ + static final String METER_MALFORMED = ".malformed.source"; + /** + * Meter that counts the number of subscriptions to a sequence. + */ + static final String METER_SUBSCRIBED = ".subscribed"; + /** + * Meter that times the duration elapsed between a subscription and the termination or cancellation of the sequence. + * A status tag is added to specify what event caused the timer to end (completed, completedEmpty, error, cancelled). + */ + static final String METER_FLOW_DURATION = ".flow.duration"; + /** + * Meter that times the delays between each onNext (or between the first onNext and the onSubscribe event). + */ + static final String METER_ON_NEXT_DELAY = ".onNext.delay"; + /** + * Meter that tracks the request amount, in {@link Flux#name(String) named} sequences only. + */ + static final String METER_REQUESTED = ".requested"; + /** + * Tag used by {@link #METER_FLOW_DURATION} when "status" is {@link #TAG_ON_ERROR}, to store the + * exception that occurred. + */ + static final String TAG_KEY_EXCEPTION = "exception"; + /** + * Tag bearing the sequence's name, as given by the {@link Flux#name(String)} operator. + */ + static final Tags DEFAULT_TAGS_FLUX = Tags.of("type", "Flux"); + static final Tags DEFAULT_TAGS_MONO = Tags.of("type", "Mono"); + + // === Operator === + static final Tag TAG_ON_ERROR = Tag.of("status", "error"); + static final Tags TAG_ON_COMPLETE = Tags.of("status", "completed", TAG_KEY_EXCEPTION, ""); + static final Tags TAG_ON_COMPLETE_EMPTY = Tags.of("status", "completedEmpty", TAG_KEY_EXCEPTION, ""); + static final Tags TAG_CANCEL = Tags.of("status", "cancelled", TAG_KEY_EXCEPTION, ""); + + /* + * This method calls the registry, which can be costly. However the cancel signal is only expected + * once per Subscriber. So the net effect should be that the registry is only called once, which + * is equivalent to registering the meter as a final field, with the added benefit of paying that + * cost only in case of cancellation. + */ + static void recordCancel(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { + Timer timer = Timer.builder(name + METER_FLOW_DURATION) + .tags(commonTags.and(TAG_CANCEL)) + .description( + "Times the duration elapsed between a subscription and the cancellation of the sequence") + .register(registry); + + flowDuration.stop(timer); + } + + /* + * This method calls the registry, which can be costly. However a malformed signal is generally + * not expected, or at most once per Subscriber. So the net effect should be that the registry + * is only called once, which is equivalent to registering the meter as a final field, + * with the added benefit of paying that cost only in case of onNext/onError after termination. + */ + static void recordMalformed(String name, Tags commonTags, MeterRegistry registry) { + registry.counter(name + METER_MALFORMED, commonTags) + .increment(); + } + + /* + * This method calls the registry, which can be costly. However the onError signal is expected + * at most once per Subscriber. So the net effect should be that the registry is only called once, + * which is equivalent to registering the meter as a final field, with the added benefit of paying + * that cost only in case of error. + */ + static void recordOnError(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration, + Throwable e) { + Timer timer = Timer.builder(name + METER_FLOW_DURATION) + .tags(commonTags.and(TAG_ON_ERROR)) + .tag(TAG_KEY_EXCEPTION, + e.getClass() + .getName()) + .description( + "Times the duration elapsed between a subscription and the onError termination of the sequence, with the exception name as a tag.") + .register(registry); + + flowDuration.stop(timer); + } + + /* + * This method calls the registry, which can be costly. However the onComplete signal is expected + * at most once per Subscriber. So the net effect should be that the registry is only called once, + * which is equivalent to registering the meter as a final field, with the added benefit of paying + * that cost only in case of completion (which is not always occurring). + */ + static void recordOnComplete(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { + Timer timer = Timer.builder(name + METER_FLOW_DURATION) + .tags(commonTags.and(TAG_ON_COMPLETE)) + .description( + "Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements") + .register(registry); + + flowDuration.stop(timer); + } + + /* + * This method calls the registry, which can be costly. However the onComplete signal is expected + * at most once per Subscriber. So the net effect should be that the registry is only called once, + * which is equivalent to registering the meter as a final field, with the added benefit of paying + * that cost only in case of completion (which is not always occurring). + */ + static void recordOnCompleteEmpty(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { + Timer timer = Timer.builder(name + METER_FLOW_DURATION) + .tags(commonTags.and(TAG_ON_COMPLETE_EMPTY)) + .description( + "Times the duration elapsed between a subscription and the onComplete termination of a sequence that didn't emit any element") + .register(registry); + + flowDuration.stop(timer); + } + + /* + * This method calls the registry, which can be costly. However the onSubscribe signal is expected + * at most once per Subscriber. So the net effect should be that the registry is only called once, + * which is equivalent to registering the meter as a final field, with the added benefit of paying + * that cost only in case of subscription. + */ + static void recordOnSubscribe(String name, Tags commonTags, MeterRegistry registry) { + Counter.builder(name + METER_SUBSCRIBED) + .tags(commonTags) + .description("Counts how many Reactor sequences have been subscribed to") + .register(registry) + .increment(); + } + +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java new file mode 100644 index 0000000000..3a27b41571 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.util.LinkedList; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.reactivestreams.Publisher; + +import reactor.core.Scannable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; +import reactor.util.function.Tuple2; + +/** + * A companion configuration object for {@link MicrometerListener} that serves as the state created by + * {@link MicrometerListenerFactory}. + * + * @author Simon Baslé + */ +final class MicrometerListenerConfiguration { + + private static final Logger LOGGER = Loggers.getLogger(MicrometerListenerConfiguration.class); + + static MicrometerListenerConfiguration fromFlux(Flux source, MeterRegistry meterRegistry, Clock clock) { + Tags defaultTags = MicrometerListener.DEFAULT_TAGS_FLUX; + final String name = resolveName(source, LOGGER); + final Tags tags = resolveTags(source, defaultTags); + + return new MicrometerListenerConfiguration(name, tags, meterRegistry, clock, false); + } + + static MicrometerListenerConfiguration fromMono(Mono source, MeterRegistry meterRegistry, Clock clock) { + Tags defaultTags = MicrometerListener.DEFAULT_TAGS_MONO; + final String name = resolveName(source, LOGGER); + final Tags tags = resolveTags(source, defaultTags); + + return new MicrometerListenerConfiguration(name, tags, meterRegistry, clock, true); + } + + /** + * Extract the name from the upstream, and detect if there was an actual name (ie. distinct from {@link + * Scannable#stepName()}) set by the user. + * + * @param source the upstream + * + * @return a name + */ + static String resolveName(Publisher source, Logger logger) { + Scannable scannable = Scannable.from(source); + if (!scannable.isScanAvailable()) { + logger.warn("Attempting to activate metrics but the upstream is not Scannable. You might want to use `name()` (and optionally `tags()`) right before this listener"); + return Micrometer.DEFAULT_METER_PREFIX; + } + + String nameOrDefault = scannable.name(); + if (scannable.stepName() + .equals(nameOrDefault)) { + return Micrometer.DEFAULT_METER_PREFIX; + } + else { + return nameOrDefault; + } + } + + /** + * Extract the tags from the upstream + * + * @param source the upstream + * + * @return a {@link Tags} of {@link io.micrometer.core.instrument.Tag} + */ + static Tags resolveTags(Publisher source, Tags tags) { + Scannable scannable = Scannable.from(source); + + if (scannable.isScanAvailable()) { + LinkedList> scannableTags = new LinkedList<>(); + scannable.tags().forEach(scannableTags::push); + return scannableTags.stream() + //Note the combiner below is for parallel streams, which won't be used + //For the identity, `commonTags` should be ok (even if reduce uses it multiple times) + //since it deduplicates + .reduce(tags, TAG_ACCUMULATOR, TAG_COMBINER); + } + + return tags; + } + + final Clock clock; + final Tags commonTags; + final boolean isMono; + final String sequenceName; + + //Note: meters and tag names are normalized by micrometer on the basis that the word + // separator is the dot, not camelCase... + final MeterRegistry registry; + + MicrometerListenerConfiguration(String sequenceName, Tags tags, MeterRegistry registryCandidate, Clock clock, + boolean isMono) { + this.clock = clock; + this.commonTags = tags; + this.isMono = isMono; + this.sequenceName = sequenceName; + this.registry = registryCandidate; + } + + static final BiFunction, Tags> TAG_ACCUMULATOR = + (prev, tuple) -> prev.and(Tag.of(tuple.getT1(), tuple.getT2())); + + static final BinaryOperator TAG_COMBINER = Tags::and; +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerFactory.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerFactory.java new file mode 100644 index 0000000000..808c803ea1 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; +import org.reactivestreams.Publisher; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; + +/** + * A {@link SignalListenerFactory} for {@link MicrometerListener}. + * + * @author Simon Baslé + */ +class MicrometerListenerFactory implements SignalListenerFactory { + + protected Clock useClock() { + return Clock.SYSTEM; + } + + protected MeterRegistry useRegistry() { + return Micrometer.getRegistry(); + } + + @Override + public MicrometerListenerConfiguration initializePublisherState(Publisher source) { + if (source instanceof Mono) { + return MicrometerListenerConfiguration.fromMono((Mono) source, useRegistry(), useClock()); + } + else if (source instanceof Flux) { + return MicrometerListenerConfiguration.fromFlux((Flux) source, useRegistry(), useClock()); + } + else { + throw new IllegalArgumentException("MicrometerListenerFactory must only be used via the tap operator / with a Flux or Mono"); + } + } + + @Override + public SignalListener createListener(Publisher source, ContextView listenerContext, + MicrometerListenerConfiguration publisherContext) { + return new MicrometerListener<>(publisherContext); + } +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerSchedulerMetricsDecorator.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerSchedulerMetricsDecorator.java new file mode 100644 index 0000000000..2cad2a1317 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerSchedulerMetricsDecorator.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; +import io.micrometer.core.instrument.search.Search; + +import reactor.core.Disposable; +import reactor.core.Scannable; +import reactor.core.scheduler.Scheduler; + +/** + * @author Simon Baslé + */ +final class MicrometerSchedulerMetricsDecorator implements BiFunction, + Disposable { + + //TODO expose keys and tags publicly? + static final String TAG_SCHEDULER_ID = "reactor.scheduler.id"; + + final WeakHashMap seenSchedulers = new WeakHashMap<>(); + final Map schedulerDifferentiator = new HashMap<>(); + final WeakHashMap executorDifferentiator = new WeakHashMap<>(); + final MeterRegistry registry; + + MicrometerSchedulerMetricsDecorator(MeterRegistry registry) { + this.registry = registry; + } + + @Override + public synchronized ScheduledExecutorService apply(Scheduler scheduler, ScheduledExecutorService service) { + //this is equivalent to `toString`, a detailed name like `parallel("foo", 3)` + String schedulerName = Scannable + .from(scheduler) + .scanOrDefault(Scannable.Attr.NAME, scheduler.getClass().getName()); + + //we hope that each NAME is unique enough, but we'll differentiate by Scheduler + String schedulerId = + seenSchedulers.computeIfAbsent(scheduler, s -> { + int schedulerDifferentiator = this.schedulerDifferentiator + .computeIfAbsent(schedulerName, k -> new AtomicInteger(0)) + .getAndIncrement(); + + return (schedulerDifferentiator == 0) ? schedulerName + : schedulerName + "#" + schedulerDifferentiator; + }); + + //we now want an executorId unique to a given scheduler + String executorId = schedulerId + "-" + + executorDifferentiator.computeIfAbsent(scheduler, key -> new AtomicInteger(0)) + .getAndIncrement(); + + Tag[] tags = new Tag[] { Tag.of(TAG_SCHEDULER_ID, schedulerId) }; + + /* + Design note: we assume that a given Scheduler won't apply the decorator twice to the + same ExecutorService. Even though, it would simply create an extraneous meter for + that ExecutorService, which we think is not that bad (compared to paying the price + upfront of also tracking executors instances to deduplicate). The main goal is to + detect Scheduler instances that have already started decorating their executors, + in order to avoid consider two calls in a row as duplicates (yet still being able + to distinguish between two instances with the same name and configuration). + */ + + + return new MetricsRemovingScheduledExecutorService(service, this.registry, executorId, tags); + } + + @Override + public void dispose() { + Search.in(registry) + .tagKeys(TAG_SCHEDULER_ID) + .meters() + .forEach(registry::remove); + + //note default isDisposed (returning false) is good enough, since the cleared + //collections can always be reused even though they probably won't + this.seenSchedulers.clear(); + this.schedulerDifferentiator.clear(); + this.executorDifferentiator.clear(); + } + + static class MetricsRemovingScheduledExecutorService implements ScheduledExecutorService { + + final ScheduledExecutorService scheduledExecutorService; + final MeterRegistry registry; + final String executorId; + + MetricsRemovingScheduledExecutorService(ScheduledExecutorService service, MeterRegistry registry, String executorId, Tag[] tags) { + this.scheduledExecutorService = ExecutorServiceMetrics.monitor(registry, service, executorId, tags); + this.registry = registry; + this.executorId = executorId; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return scheduledExecutorService.awaitTermination(timeout, unit); + } + + @Override + public void execute(Runnable command) { + scheduledExecutorService.execute(command); + } + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + return scheduledExecutorService.invokeAll(tasks); + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException { + return scheduledExecutorService.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, + ExecutionException { + return scheduledExecutorService.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return scheduledExecutorService.invokeAny(tasks, timeout, unit); + } + + @Override + public boolean isShutdown() { + return scheduledExecutorService.isShutdown(); + } + + @Override + public boolean isTerminated() { + return scheduledExecutorService.isTerminated(); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + return scheduledExecutorService.schedule(command, delay, unit); + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + return scheduledExecutorService.schedule(callable, delay, unit); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + return scheduledExecutorService.scheduleAtFixedRate(command, initialDelay, period, unit); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { + return scheduledExecutorService.scheduleWithFixedDelay(command, initialDelay, delay, unit); + } + + @Override + public List shutdownNow() { + removeMetrics(); + return scheduledExecutorService.shutdownNow(); + } + + @Override + public void shutdown() { + removeMetrics(); + scheduledExecutorService.shutdown(); + } + + @Override + public Future submit(Callable task) { + return scheduledExecutorService.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return scheduledExecutorService.submit(task, result); + } + + @Override + public Future submit(Runnable task) { + return scheduledExecutorService.submit(task); + } + + void removeMetrics() { + Search.in(registry) + .tag("name", executorId) + .meters() + .forEach(registry::remove); + } + } +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/package-info.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/package-info.java new file mode 100644 index 0000000000..e8f2239988 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support of Micrometer 1.10+ instrumentation on reactor-core classes. + */ +@NonNullApi +package reactor.core.observability.micrometer; + +import reactor.util.annotation.NonNullApi; \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java new file mode 100644 index 0000000000..b096d60858 --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.CsvSource; +import org.reactivestreams.Publisher; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.test.ParameterizedTestWithName; +import reactor.test.util.TestLogger; +import reactor.util.annotation.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Simon Baslé + */ +class MicrometerListenerConfigurationTest { + + @ParameterizedTestWithName + @CsvSource(value = { + ",", + "someName,", + ",someTag", + "someName,someTag" + }) + void fromFlux(@Nullable String name, @Nullable String tag) { + MeterRegistry expectedRegistry = new SimpleMeterRegistry(); + Clock expectedClock = Clock.SYSTEM; + + Flux flux = Flux.just(1, 2, 3); + + if (name != null) { + flux = flux.name(name); + } + if (tag != null) { + flux = flux.tag("tag", tag); + } + + MicrometerListenerConfiguration configuration = MicrometerListenerConfiguration.fromFlux(flux, expectedRegistry, expectedClock); + + assertThat(configuration.clock).as("clock").isSameAs(expectedClock); + assertThat(configuration.registry).as("registry").isSameAs(expectedRegistry); + assertThat(configuration.isMono).as("isMono").isFalse(); + + assertThat(configuration.sequenceName) + .as("sequenceName") + .isEqualTo(name == null ? Micrometer.DEFAULT_METER_PREFIX : name); + + if (tag == null) { + assertThat(configuration.commonTags.stream().map(t -> t.getKey() + "=" + t.getValue())) + .as("commonTags without additional tag") + .containsExactly("type=Flux"); + } + else { + assertThat(configuration.commonTags.stream().map(t -> t.getKey() + "=" + t.getValue())) + .as("commonTags") + .containsExactlyInAnyOrder("type=Flux", "tag="+tag); + } + } + + @ParameterizedTestWithName + @CsvSource(value = { + ",", + "someName,", + ",someTag", + "someName,someTag" + }) + void fromMono(@Nullable String name, @Nullable String tag) { + MeterRegistry expectedRegistry = new SimpleMeterRegistry(); + Clock expectedClock = Clock.SYSTEM; + + Mono mono = Mono.just(1); + + if (name != null) { + mono = mono.name(name); + } + if (tag != null) { + mono = mono.tag("tag", tag); + } + + MicrometerListenerConfiguration configuration = MicrometerListenerConfiguration.fromMono(mono, expectedRegistry, expectedClock); + + assertThat(configuration.clock).as("clock").isSameAs(expectedClock); + assertThat(configuration.registry).as("registry").isSameAs(expectedRegistry); + assertThat(configuration.isMono).as("isMono").isTrue(); + + assertThat(configuration.sequenceName) + .as("sequenceName") + .isEqualTo(name == null ? Micrometer.DEFAULT_METER_PREFIX : name); + + if (tag == null) { + assertThat(configuration.commonTags.stream().map(t -> t.getKey() + "=" + t.getValue())) + .as("commonTags without additional tag") + .containsExactly("type=Mono"); + } + else { + assertThat(configuration.commonTags.stream().map(t -> t.getKey() + "=" + t.getValue())) + .as("commonTags") + .containsExactlyInAnyOrder("type=Mono", "tag="+tag); + } + } + + @Test + void resolveName_notSet() { + TestLogger logger = new TestLogger(false); + Flux flux = Flux.just(1); + + String resolvedName = MicrometerListenerConfiguration.resolveName(flux, logger); + + assertThat(resolvedName).isEqualTo(Micrometer.DEFAULT_METER_PREFIX); + assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); + } + + @Test + void resolveName_setRightAbove() { + TestLogger logger = new TestLogger(false); + Flux flux = Flux.just(1).name("someName"); + + String resolvedName = MicrometerListenerConfiguration.resolveName(flux, logger); + + assertThat(resolvedName).isEqualTo("someName"); + assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); + } + + @Test + void resolveName_setHigherAbove() { + TestLogger logger = new TestLogger(false); + Flux flux = Flux.just(1).name("someName").filter(i -> i % 2 == 0).map(i -> i + 10); + + String resolvedName = MicrometerListenerConfiguration.resolveName(flux, logger); + + assertThat(resolvedName).isEqualTo("someName"); + assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); + } + + @Test + void resolveName_notScannable() { + TestLogger testLogger = new TestLogger(false); + Publisher publisher = Operators::complete; + + String resolvedName = MicrometerListenerConfiguration.resolveName(publisher, testLogger); + + assertThat(resolvedName).as("resolved name").isEqualTo(Micrometer.DEFAULT_METER_PREFIX); + assertThat(testLogger.getErrContent()).contains("Attempting to activate metrics but the upstream is not Scannable. You might want to use `name()` (and optionally `tags()`) right before this listener"); + } + + @Test + void resolveTags_notSet() { + Tags defaultTags = Tags.of("common1", "commonValue1"); + Flux flux = Flux.just(1); + + Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + + assertThat(resolvedTags.stream().map(Object::toString)) + .containsExactly("tag(common1=commonValue1)"); + } + + @Test + void resolveTags_setRightAbove() { + Tags defaultTags = Tags.of("common1", "commonValue1"); + Flux flux = Flux + .just(1) + .tag("k1", "v1"); + + Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + + assertThat(resolvedTags.stream().map(Object::toString)).containsExactlyInAnyOrder( + "tag(common1=commonValue1)", + "tag(k1=v1)" + ); + } + + @Test + void resolveTags_setHigherAbove() { + Tags defaultTags = Tags.of("common1", "commonValue1"); + Flux flux = Flux + .just(1) + .tag("k1", "v1") + .filter(i -> i % 2 == 0) + .map(i -> i + 10); + + Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + + assertThat(resolvedTags.stream().map(Object::toString)).containsExactlyInAnyOrder( + "tag(common1=commonValue1)", + "tag(k1=v1)" + ); + } + + @Test + void resolveTags_multipleScatteredTagsSetAbove() { + Tags defaultTags = Tags.of("common1", "commonValue1"); + Flux flux = Flux.just(1) + .tag("k1", "v1") + .filter(i -> i % 2 == 0) + .tag("k2", "v2") + .map(i -> i + 10); + + Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + + assertThat(resolvedTags.stream().map(Object::toString)).containsExactlyInAnyOrder( + "tag(common1=commonValue1)", + "tag(k1=v1)", + "tag(k2=v2)" + ); + } + + @Test + void resolveTags_notScannable() { + Tags defaultTags = Tags.of("common1", "commonValue1"); + Publisher publisher = Operators::complete; + + Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(publisher, defaultTags); + + assertThat(resolvedTags.stream().map(Object::toString)).containsExactly("tag(common1=commonValue1)"); + } +} \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerFactoryTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerFactoryTest.java new file mode 100644 index 0000000000..f2680069f3 --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerFactoryTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.util.context.Context; +import reactor.core.observability.SignalListener; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Simon Baslé + */ +class MicrometerListenerFactoryTest { + + @Test + void useClockDefaultsToSystemClock() { + MicrometerListenerFactory factory = new MicrometerListenerFactory<>(); + + assertThat(factory.useClock()).isSameAs(Clock.SYSTEM); + } + + @Test + void useRegistryDefaultsToCommonRegistry() { + SimpleMeterRegistry commonRegistry = new SimpleMeterRegistry(); + MeterRegistry defaultCommon = Micrometer.useRegistry(commonRegistry); + try { + MicrometerListenerFactory factory = new MicrometerListenerFactory<>(); + + assertThat(factory.useRegistry()).isSameAs(Micrometer.getRegistry()) + .isSameAs(commonRegistry); + } + finally { + Micrometer.useRegistry(defaultCommon); + } + } + + @Test + void configurationFromMono() { + MicrometerListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Mono.just(1)); + + assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); + assertThat(configuration.clock).as("clock").isSameAs(CUSTOM_CLOCK); + assertThat(configuration.isMono).as("isMono").isTrue(); + assertThat(configuration.commonTags).map(Object::toString).containsExactly("tag(type=Mono)"); + } + + @Test + void configurationFromFlux() { + MicrometerListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Flux.just(1, 2)); + + assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); + assertThat(configuration.clock).as("clock").isSameAs(CUSTOM_CLOCK); + assertThat(configuration.isMono).as("isMono").isFalse(); + assertThat(configuration.commonTags).map(Object::toString).containsExactly("tag(type=Flux)"); + } + + @Test + void configurationFromGenericPublisherIsRejected() { + assertThatIllegalArgumentException() + .isThrownBy(() -> CUSTOM_FACTORY.initializePublisherState(Operators::complete)) + .withMessage("MicrometerListenerFactory must only be used via the tap operator / with a Flux or Mono"); + } + + @Test + void createListenerOfTypeMicrometer() { + Publisher source = Mono.just(1); + MicrometerListenerConfiguration conf = CUSTOM_FACTORY.initializePublisherState(source); + SignalListener signalListener = CUSTOM_FACTORY.createListener(source, Context.empty(), conf); + + assertThat(signalListener).isInstanceOf(MicrometerListener.class); + assertThat(((MicrometerListener) signalListener).configuration).as("configuration").isSameAs(conf); + } + + protected static final Clock CUSTOM_CLOCK = new Clock() { + @Override + public long wallTime() { + return 0; + } + + @Override + public long monotonicTime() { + return 0; + } + }; + protected static final SimpleMeterRegistry CUSTOM_REGISTRY = new SimpleMeterRegistry(); + protected static final MicrometerListenerFactory CUSTOM_FACTORY = new MicrometerListenerFactory() { + @Override + protected Clock useClock() { + return CUSTOM_CLOCK; + } + + @Override + protected MeterRegistry useRegistry() { + return CUSTOM_REGISTRY; + } + }; +} \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerTest.java new file mode 100644 index 0000000000..a5d6812039 --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerTest.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Simon Baslé + */ +class MicrometerListenerTest { + + SimpleMeterRegistry registry; + AtomicLong virtualClockTime; + Clock virtualClock; + MicrometerListenerConfiguration configuration; + + @BeforeEach + void initRegistry() { + registry = new SimpleMeterRegistry(); + virtualClockTime = new AtomicLong(); + virtualClock = new Clock() { + @Override + public long wallTime() { + return virtualClockTime.get(); + } + + @Override + public long monotonicTime() { + return virtualClockTime.get(); + } + }; + configuration = new MicrometerListenerConfiguration( + "testName", + Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + virtualClock, + false); + } + + @Test + void initialStateFluxWithDefaultName() { + configuration = new MicrometerListenerConfiguration( + Micrometer.DEFAULT_METER_PREFIX, + Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + virtualClock, + false); + + MicrometerListener listener = new MicrometerListener<>(configuration); + + assertThat(listener.valued).as("valued").isFalse(); + assertThat(listener.requestedCounter).as("requestedCounter disabled").isNull(); + assertThat(listener.onNextIntervalTimer).as("onNextIntervalTimer").isNotNull(); + + assertThat(registry.getMetersAsString().split("\n")) + .as("registered meters: onNextIntervalTimer") + .containsExactly( + Micrometer.DEFAULT_METER_PREFIX + ".onNext.delay(TIMER)[testTag1='testTagValue1', testTag2='testTagValue2']; count=0.0, total_time=0.0 seconds, max=0.0 seconds" + ); + + assertThat(registry.remove(listener.onNextIntervalTimer)) + .as("registry contains onNextIntervalTimer") + .isSameAs(listener.onNextIntervalTimer); + } + + @Test + void initialStateFluxWithCustomName() { + configuration = new MicrometerListenerConfiguration( + "testName", + Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + virtualClock, + false); + + MicrometerListener listener = new MicrometerListener<>(configuration); + + assertThat(listener.valued).as("valued").isFalse(); + assertThat(listener.requestedCounter).as("requestedCounter").isNotNull(); + assertThat(listener.onNextIntervalTimer).as("onNextIntervalTimer").isNotNull(); + + assertThat(registry.getMetersAsString().split("\n")) + .as("registered meters: onNextIntervalTimer, requestedCounter") + .containsExactly( + "testName.onNext.delay(TIMER)[testTag1='testTagValue1', testTag2='testTagValue2']; count=0.0, total_time=0.0 seconds, max=0.0 seconds", + "testName.requested(DISTRIBUTION_SUMMARY)[testTag1='testTagValue1', testTag2='testTagValue2']; count=0.0, total=0.0, max=0.0" + ); + + assertThat(registry.remove(listener.onNextIntervalTimer)) + .as("registry contains onNextIntervalTimer") + .isSameAs(listener.onNextIntervalTimer); + + assertThat(registry.remove(listener.requestedCounter)) + .as("registry contains requestedCounter") + .isSameAs(listener.requestedCounter); + } + + @Test + void initialStateMono() { + configuration = new MicrometerListenerConfiguration( + Micrometer.DEFAULT_METER_PREFIX, + Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + virtualClock, + true); + + MicrometerListener listener = new MicrometerListener<>(configuration); + + assertThat(listener.valued).as("valued").isFalse(); + assertThat(listener.requestedCounter).as("requestedCounter disabled").isNull(); + assertThat(listener.onNextIntervalTimer).as("onNextIntervalTimer disabled").isNull(); + + assertThat(registry.getMetersAsString()) + .as("no registered meters") + .isEmpty(); + } + + @Test + void timerSampleInitializedInSubscription() { + MicrometerListener listener = new MicrometerListener<>(configuration); + + assertThat(listener.subscribeToTerminateSample) + .as("subscribeToTerminateSample pre subscription") + .isNull(); + assertThat(listener.lastNextEventNanos) + .as("lastNextEventNanos") + .isEqualTo(-1L); + assertThat(registry.getMeters()) + .as("meters pre subscription") + .hasSize(2); + + virtualClockTime.incrementAndGet(); + listener.doOnSubscription(); + + assertThat(listener.subscribeToTerminateSample) + .as("subscribeToTerminateSample") + .isNotNull(); + assertThat(listener.lastNextEventNanos) + .as("lastNextEventNanos") + .isEqualTo(1L); + assertThat(registry.getMeters()) + .as("meters post subscription") + .hasSize(3); + assertThat(registry.find("testName" + MicrometerListener.METER_SUBSCRIBED).counter()) + .as("meter .subscribed") + .isNotNull() + .satisfies(meter -> assertThat(meter.count()).isEqualTo(1d)); + } + + @Test + void doOnCancelTimesFlowDurationMeter() { + MicrometerListener listener = new MicrometerListener<>(configuration); + listener.doOnSubscription(); + assertThat(registry.getMeters()).hasSize(3); + + virtualClockTime.set(100); + listener.doOnCancel(); + + Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + .timer(); + + assertThat(registry.getMeters()).hasSize(4); + assertThat(timer.getId().toString()) + .as(".flow.duration with exception and status tags") + .isEqualTo("MeterId{name='testName.flow.duration', tags=[tag(exception=),tag(status=cancelled),tag(testTag1=testTagValue1),tag(testTag2=testTagValue2)]}"); + assertThat(timer.totalTime(TimeUnit.NANOSECONDS)) + .as("measured time") + .isEqualTo(100); + } + + @Test + void doOnCompleteTimesFlowDurationMeter_completeEmpty() { + MicrometerListener listener = new MicrometerListener<>(configuration); + listener.doOnSubscription(); + assertThat(registry.getMeters()).hasSize(3); + + virtualClockTime.set(100); + listener.doOnComplete(); + + Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + .timer(); + + assertThat(registry.getMeters()).hasSize(4); + assertThat(timer.getId().toString()) + .as(".flow.duration with exception and status tags") + .isEqualTo("MeterId{name='testName.flow.duration', tags=[tag(exception=),tag(status=completedEmpty),tag(testTag1=testTagValue1),tag(testTag2=testTagValue2)]}"); + assertThat(timer.totalTime(TimeUnit.NANOSECONDS)) + .as("measured time") + .isEqualTo(100); + } + + @Test + void doOnCompleteTimesFlowDurationMeter_completeValued() { + MicrometerListener listener = new MicrometerListener<>(configuration); + listener.doOnSubscription(); + assertThat(registry.getMeters()).hasSize(3); + + virtualClockTime.set(100); + listener.valued = true; + listener.doOnComplete(); + + Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + .timer(); + + assertThat(registry.getMeters()).hasSize(4); + assertThat(timer.getId().toString()) + .as(".flow.duration with exception and status tags") + .isEqualTo("MeterId{name='testName.flow.duration', tags=[tag(exception=),tag(status=completed),tag(testTag1=testTagValue1),tag(testTag2=testTagValue2)]}"); + assertThat(timer.totalTime(TimeUnit.NANOSECONDS)) + .as("measured time") + .isEqualTo(100); + } + + @Test + void doOnErrorTimesFlowDurationMeter() { + MicrometerListener listener = new MicrometerListener<>(configuration); + listener.doOnSubscription(); + assertThat(registry.getMeters()).hasSize(3); + + virtualClockTime.set(100); + listener.doOnError(new IllegalStateException("expected")); + + Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + .timer(); + + assertThat(registry.getMeters()).hasSize(4); + assertThat(timer.getId().toString()) + .as(".flow.duration with exception and status tags") + .isEqualTo("MeterId{name='testName.flow.duration', tags=[tag(exception=java.lang.IllegalStateException),tag(status=error),tag(testTag1=testTagValue1),tag(testTag2=testTagValue2)]}"); + assertThat(timer.totalTime(TimeUnit.NANOSECONDS)) + .as("measured time") + .isEqualTo(100); + } + + @Test + void doOnNextRecordsInterval() { + MicrometerListener listener = new MicrometerListener<>(configuration); + listener.doOnSubscription(); + + virtualClockTime.set(100); + listener.doOnNext(1); + + assertThat(listener.valued).as("valued").isTrue(); + assertThat(listener.lastNextEventNanos).as("lastEventNanos recorded").isEqualTo(100); + + assertThat(registry.find("testName" + MicrometerListener.METER_FLOW_DURATION).meters()) + .as("no flow.duration meter yet") + .isEmpty(); + + assertThat(listener.onNextIntervalTimer.totalTime(TimeUnit.NANOSECONDS)) + .as("interval timed") + .isEqualTo(100); + assertThat(listener.onNextIntervalTimer.count()) + .as("onNext count") + .isOne(); + } + + + @Test + void doOnNextRecordsInterval_defaultName() { + configuration = new MicrometerListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), + registry, virtualClock, false); + MicrometerListener listener = new MicrometerListener<>(configuration); + listener.doOnSubscription(); + + virtualClockTime.set(100); + listener.doOnNext(1); + + assertThat(listener.valued).as("valued").isTrue(); + assertThat(listener.lastNextEventNanos).as("lastEventNanos recorded").isEqualTo(100); + + assertThat(registry.find(Micrometer.DEFAULT_METER_PREFIX + MicrometerListener.METER_FLOW_DURATION).meters()) + .as("no flow.duration meter yet") + .isEmpty(); + + assertThat(listener.onNextIntervalTimer.totalTime(TimeUnit.NANOSECONDS)) + .as("interval timed") + .isEqualTo(100); + assertThat(listener.onNextIntervalTimer.count()) + .as("onNext count") + .isOne(); + } + + @Test + void doOnNext_monoRecordsCompletionOnly() { + configuration = new MicrometerListenerConfiguration("testName", Tags.empty(), + registry, virtualClock, true); + MicrometerListener listener = new MicrometerListener<>(configuration); + + listener.doOnSubscription(); + + virtualClockTime.set(100); + listener.doOnNext(1); + + assertThat(listener.valued).as("valued").isTrue(); + assertThat(listener.lastNextEventNanos).as("no lastEventNanos recorded").isZero(); + + Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + .timer(); + + assertThat(timer.getId().toString()) + .as(".flow.duration with exception and status tags") + .isEqualTo("MeterId{name='testName.flow.duration', tags=[tag(exception=),tag(status=completed)]}"); + assertThat(timer.totalTime(TimeUnit.NANOSECONDS)) + .as("measured time") + .isEqualTo(100); + } + + @Test + void doOnNextMultipleRecords() { + MicrometerListener listener = new MicrometerListener<>(configuration); + listener.doOnSubscription(); + + virtualClockTime.set(100); + listener.doOnNext(1); + virtualClockTime.set(300); + listener.doOnNext(2); + + assertThat(listener.onNextIntervalTimer.totalTime(TimeUnit.NANOSECONDS)) + .as("total time") + .isEqualTo(300); + assertThat(listener.onNextIntervalTimer.count()) + .as("onNext count") + .isEqualTo(2); + assertThat(listener.onNextIntervalTimer.max(TimeUnit.NANOSECONDS)) + .as("onNext max interval") + .isEqualTo(200); + } + + @Test + void doOnRequestRecordsTotalDemand() { + MicrometerListener listener = new MicrometerListener<>(configuration); + listener.doOnRequest(100L); + + assertThat(listener.requestedCounter.count()).as("1 request calls").isEqualTo(1); + assertThat(listener.requestedCounter.totalAmount()).as("total after first request").isEqualTo(100); + + listener.doOnRequest(200L); + + assertThat(listener.requestedCounter.count()).as("2 request calls").isEqualTo(2); + assertThat(listener.requestedCounter.totalAmount()).as("total after second request").isEqualTo(300); + assertThat(listener.requestedCounter.max()).as("max is second request").isEqualTo(200); + } + + @Test + void doOnRequestMonoIgnoresRequest() { + configuration = new MicrometerListenerConfiguration("testName", Tags.empty(), registry, virtualClock, true); + MicrometerListener listener = new MicrometerListener<>(configuration); + assertThatCode(() -> listener.doOnRequest(100L)).doesNotThrowAnyException(); + assertThat(listener.requestedCounter).isNull(); + } + + @Test + void doOnRequestDefaultNameIgnoresRequest() { + configuration = new MicrometerListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), registry, virtualClock, false); + MicrometerListener listener = new MicrometerListener<>(configuration); + assertThatCode(() -> listener.doOnRequest(100L)).doesNotThrowAnyException(); + assertThat(listener.requestedCounter).isNull(); + } + + @Test + void malformedCounterCapturesNextCompleteError() { + MicrometerListener listener = new MicrometerListener<>(configuration); + + Counter malformedCounter = registry.find("testName" + MicrometerListener.METER_MALFORMED).counter(); + assertThat(malformedCounter).as("counter not registered").isNull(); + + listener.doOnMalformedOnNext(123); + + malformedCounter = registry.find("testName" + MicrometerListener.METER_MALFORMED).counter(); + assertThat(malformedCounter).as("lazy counter registration").isNotNull(); + assertThat(malformedCounter.count()).as("onNext malformed").isOne(); + + listener.doOnMalformedOnComplete(); + assertThat(malformedCounter.count()).as("onComplete malformed").isEqualTo(2); + + listener.doOnMalformedOnError(new IllegalStateException("expected, ignored")); + assertThat(malformedCounter.count()).as("onError malformed").isEqualTo(3); + } +} \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java new file mode 100644 index 0000000000..dd27963a5e --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Simon Baslé + */ +class MicrometerTest { + + private MeterRegistry defaultRegistry; + + @BeforeEach + void init() { + defaultRegistry = Micrometer.getRegistry(); + } + + @AfterEach + void restore() { + Micrometer.useRegistry(defaultRegistry); + } + + @Test + void defaultRegistryCanBeChanged() { + MeterRegistry registry = Micrometer.getRegistry(); + try { + assertThat(registry).as("default common registry").isEqualTo(Metrics.globalRegistry); + + MeterRegistry replacement = new SimpleMeterRegistry(); + MeterRegistry old = Micrometer.useRegistry(replacement); + + assertThat(old).as("useRegistry return value").isSameAs(registry); + assertThat(Micrometer.getRegistry()).as("getRegistry post useRegistry").isSameAs(replacement); + } + finally { + Micrometer.useRegistry(registry); + } + } + + @Test + void metricsUsesCommonRegistry() { + SimpleMeterRegistry customCommonRegistry = new SimpleMeterRegistry(); + Micrometer.useRegistry(customCommonRegistry); + MicrometerListenerFactory factory = (MicrometerListenerFactory) Micrometer.metrics(); + + assertThat(factory.useClock()).as("clock").isSameAs(Clock.SYSTEM); + assertThat(factory.useRegistry()).as("registry").isSameAs(customCommonRegistry); + } + + @Test + void metricsUsesSpecifiedClockAndRegistry() { + SimpleMeterRegistry customCommonRegistry = new SimpleMeterRegistry(); + Micrometer.useRegistry(customCommonRegistry); + SimpleMeterRegistry customLocalRegistry = new SimpleMeterRegistry(); + Clock customLocalClock = new Clock() { + @Override + public long wallTime() { + return 0; + } + + @Override + public long monotonicTime() { + return 0; + } + }; + + MicrometerListenerFactory factory = (MicrometerListenerFactory) Micrometer.metrics(customLocalRegistry, customLocalClock); + + assertThat(factory.useClock()).as("clock").isSameAs(customLocalClock).isNotSameAs(Clock.SYSTEM); + assertThat(factory.useRegistry()).as("registry").isSameAs(customLocalRegistry).isNotSameAs(customCommonRegistry); + } +} \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/resources/logback.xml b/reactor-core-micrometer/src/test/resources/logback.xml new file mode 100644 index 0000000000..de5d4a1a63 --- /dev/null +++ b/reactor-core-micrometer/src/test/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/util/observability/DefaultSignalListener.java b/reactor-core/src/main/java/reactor/core/observability/DefaultSignalListener.java similarity index 98% rename from reactor-core/src/main/java/reactor/util/observability/DefaultSignalListener.java rename to reactor-core/src/main/java/reactor/core/observability/DefaultSignalListener.java index a9f3c1c051..58f61e0a1e 100644 --- a/reactor-core/src/main/java/reactor/util/observability/DefaultSignalListener.java +++ b/reactor-core/src/main/java/reactor/core/observability/DefaultSignalListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package reactor.util.observability; +package reactor.core.observability; import reactor.core.Fuseable; import reactor.core.publisher.SignalType; diff --git a/reactor-core/src/main/java/reactor/util/observability/SignalListener.java b/reactor-core/src/main/java/reactor/core/observability/SignalListener.java similarity index 99% rename from reactor-core/src/main/java/reactor/util/observability/SignalListener.java rename to reactor-core/src/main/java/reactor/core/observability/SignalListener.java index 4ab7ded5a1..8df017d1be 100644 --- a/reactor-core/src/main/java/reactor/util/observability/SignalListener.java +++ b/reactor-core/src/main/java/reactor/core/observability/SignalListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package reactor.util.observability; +package reactor.core.observability; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; diff --git a/reactor-core/src/main/java/reactor/util/observability/SignalListenerFactory.java b/reactor-core/src/main/java/reactor/core/observability/SignalListenerFactory.java similarity index 98% rename from reactor-core/src/main/java/reactor/util/observability/SignalListenerFactory.java rename to reactor-core/src/main/java/reactor/core/observability/SignalListenerFactory.java index e259c1626a..9e5b297e19 100644 --- a/reactor-core/src/main/java/reactor/util/observability/SignalListenerFactory.java +++ b/reactor-core/src/main/java/reactor/core/observability/SignalListenerFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package reactor.util.observability; +package reactor.core.observability; import org.reactivestreams.Publisher; diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 306a44ccef..e7b461c694 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -78,8 +78,8 @@ import reactor.util.function.Tuple7; import reactor.util.function.Tuple8; import reactor.util.function.Tuples; -import reactor.util.observability.SignalListener; -import reactor.util.observability.SignalListenerFactory; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; import reactor.util.retry.Retry; /** @@ -6427,7 +6427,10 @@ public final Flux mergeWith(Publisher other) { * * @see #name(String) * @see #tag(String, String) + * @deprecated Prefer using the {@link #tap(SignalListenerFactory)} with the {@link SignalListenerFactory} provided by + * the new reactor-core-micrometer module. To be removed in 3.6.0 at the earliest. */ + @Deprecated public final Flux metrics() { if (!Metrics.isInstrumentationAvailable()) { return this; @@ -6443,7 +6446,8 @@ public final Flux metrics() { * Give a name to this sequence, which can be retrieved using {@link Scannable#name()} * as long as this is the first reachable {@link Scannable#parents()}. *

    - * If {@link #metrics()} operator is called later in the chain, this name will be used as a prefix for meters' name. + * The name is typically visible at assembly time by the {@link #tap(SignalListenerFactory)} operator, + * which could for example be configured with a metrics listener using the name as a prefix for meters' id. * * @param name a name for the sequence * @@ -8746,11 +8750,10 @@ public final Flux switchMap(Function> f /** * Tag this flux with a key/value pair. These can be retrieved as a {@link Set} of * all tags throughout the publisher chain by using {@link Scannable#tags()} (as - * traversed - * by {@link Scannable#parents()}). + * traversed by {@link Scannable#parents()}). *

    - * Note that some monitoring systems like Prometheus require to have the exact same set of - * tags for each meter bearing the same name. + * The name is typically visible at assembly time by the {@link #tap(SignalListenerFactory)} operator, + * which could for example be configured with a metrics listener applying the tag(s) to its meters. * * @param key a tag key * @param value a tag value diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java b/reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java index f1dfad7132..10e698c3d5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ * @author Simon Baslé * @author Stephane Maldini */ +@Deprecated final class FluxMetrics extends InternalFluxOperator { final String name; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxMetricsFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxMetricsFuseable.java index fc2bcc0ab9..b22795c165 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxMetricsFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxMetricsFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ * @author Simon Baslé * @author Stephane Maldini */ +@Deprecated final class FluxMetricsFuseable extends InternalFluxOperator implements Fuseable { final String name; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java index 3a57782853..6bd4d1fcc5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java @@ -22,8 +22,8 @@ import reactor.core.Exceptions; import reactor.core.Fuseable.ConditionalSubscriber; import reactor.util.annotation.Nullable; -import reactor.util.observability.SignalListener; -import reactor.util.observability.SignalListenerFactory; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; /** * A generic per-Subscription side effect {@link Flux} that notifies a {@link SignalListener} of most events. diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java index 35ac96368a..7cca5abda8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java @@ -22,8 +22,8 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; -import reactor.util.observability.SignalListener; -import reactor.util.observability.SignalListenerFactory; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; /** * A {@link reactor.core.Fuseable} generic per-Subscription side effect {@link Flux} that notifies a diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 2c8bbd2f21..cdbd071348 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -72,8 +72,8 @@ import reactor.util.function.Tuple7; import reactor.util.function.Tuple8; import reactor.util.function.Tuples; -import reactor.util.observability.SignalListener; -import reactor.util.observability.SignalListenerFactory; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; import reactor.util.retry.Retry; /** @@ -3405,7 +3405,10 @@ public final Flux mergeWith(Publisher other) { * * @see #name(String) * @see #tag(String, String) + * @deprecated Prefer using the {@link #tap(SignalListenerFactory)} with the {@link SignalListenerFactory} provided by + * the new reactor-core-micrometer module. To be removed in 3.6.0 at the earliest. */ + @Deprecated public final Mono metrics() { if (!Metrics.isInstrumentationAvailable()) { return this; @@ -3421,7 +3424,8 @@ public final Mono metrics() { * Give a name to this sequence, which can be retrieved using {@link Scannable#name()} * as long as this is the first reachable {@link Scannable#parents()}. *

    - * If {@link #metrics()} operator is called later in the chain, this name will be used as a prefix for meters' name. + * The name is typically visible at assembly time by the {@link #tap(SignalListenerFactory)} operator, + * which could for example be configured with a metrics listener using the name as a prefix for meters' id. * * @param name a name for the sequence * @@ -4405,11 +4409,10 @@ public final Mono switchIfEmpty(Mono alternate) { /** * Tag this mono with a key/value pair. These can be retrieved as a {@link Set} of * all tags throughout the publisher chain by using {@link Scannable#tags()} (as - * traversed - * by {@link Scannable#parents()}). + * traversed by {@link Scannable#parents()}). *

    - * Note that some monitoring systems like Prometheus require to have the exact same set of - * tags for each meter bearing the same name. + * The name is typically visible at assembly time by the {@link #tap(SignalListenerFactory)} operator, + * which could for example be configured with a metrics listener applying the tag(s) to its meters. * * @param key a tag key * @param value a tag value diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoMetrics.java b/reactor-core/src/main/java/reactor/core/publisher/MonoMetrics.java index ce8c02b471..c79a396992 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoMetrics.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,6 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.util.Metrics; -import reactor.util.annotation.Nullable; - -import static reactor.core.publisher.FluxMetrics.*; /** * Activate metrics gathering on a {@link Mono}, assumes Micrometer is on the classpath. @@ -34,6 +31,7 @@ * @author Simon Baslé * @author Stephane Maldini */ +@Deprecated final class MonoMetrics extends InternalMonoOperator { final String name; @@ -44,8 +42,8 @@ final class MonoMetrics extends InternalMonoOperator { MonoMetrics(Mono mono) { super(mono); - this.name = resolveName(mono); - this.tags = resolveTags(mono, DEFAULT_TAGS_MONO); + this.name = FluxMetrics.resolveName(mono); + this.tags = FluxMetrics.resolveTags(mono, FluxMetrics.DEFAULT_TAGS_MONO); this.registryCandidate = Metrics.MicrometerConfiguration.getRegistry(); } @@ -89,7 +87,7 @@ final public CoreSubscriber actual() { @Override final public void cancel() { - recordCancel(sequenceName, commonTags, registry, subscribeToTerminateSample); + FluxMetrics.recordCancel(sequenceName, commonTags, registry, subscribeToTerminateSample); s.cancel(); } @@ -99,31 +97,31 @@ public void onComplete() { return; } done = true; - recordOnCompleteEmpty(sequenceName, commonTags, registry, subscribeToTerminateSample); + FluxMetrics.recordOnCompleteEmpty(sequenceName, commonTags, registry, subscribeToTerminateSample); actual.onComplete(); } @Override final public void onError(Throwable e) { if (done) { - recordMalformed(sequenceName, commonTags, registry); + FluxMetrics.recordMalformed(sequenceName, commonTags, registry); Operators.onErrorDropped(e, actual.currentContext()); return; } done = true; - recordOnError(sequenceName, commonTags, registry, subscribeToTerminateSample, e); + FluxMetrics.recordOnError(sequenceName, commonTags, registry, subscribeToTerminateSample, e); actual.onError(e); } @Override public void onNext(T t) { if (done) { - recordMalformed(sequenceName, commonTags, registry); + FluxMetrics.recordMalformed(sequenceName, commonTags, registry); Operators.onNextDropped(t, actual.currentContext()); return; } done = true; - recordOnComplete(sequenceName, commonTags, registry, subscribeToTerminateSample); + FluxMetrics.recordOnComplete(sequenceName, commonTags, registry, subscribeToTerminateSample); actual.onNext(t); actual.onComplete(); } @@ -131,7 +129,7 @@ public void onNext(T t) { @Override public void onSubscribe(Subscription s) { if (Operators.validate(this.s, s)) { - recordOnSubscribe(sequenceName, commonTags, registry); + FluxMetrics.recordOnSubscribe(sequenceName, commonTags, registry); this.subscribeToTerminateSample = Timer.start(clock); this.s = s; actual.onSubscribe(this); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoMetricsFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoMetricsFuseable.java index 95ddf2190d..25cf8f1718 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoMetricsFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoMetricsFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,9 +26,6 @@ import reactor.util.Metrics; import reactor.util.annotation.Nullable; -import static reactor.core.publisher.FluxMetrics.resolveName; -import static reactor.core.publisher.FluxMetrics.resolveTags; - /** * Activate metrics gathering on a {@link Mono} (Fuseable version), assumes Micrometer is on the classpath. @@ -38,6 +35,7 @@ * @author Simon Baslé * @author Stephane Maldini */ +@Deprecated final class MonoMetricsFuseable extends InternalMonoOperator implements Fuseable { final String name; @@ -48,8 +46,8 @@ final class MonoMetricsFuseable extends InternalMonoOperator implements MonoMetricsFuseable(Mono mono) { super(mono); - this.name = resolveName(mono); - this.tags = resolveTags(mono, FluxMetrics.DEFAULT_TAGS_MONO); + this.name = FluxMetrics.resolveName(mono); + this.tags = FluxMetrics.resolveTags(mono, FluxMetrics.DEFAULT_TAGS_MONO); this.registryCandidate = Metrics.MicrometerConfiguration.getRegistry();; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java index a924da75f5..6b4bb6f8ea 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java @@ -20,8 +20,8 @@ import reactor.core.Fuseable; import reactor.core.publisher.FluxTap.TapSubscriber; import reactor.util.annotation.Nullable; -import reactor.util.observability.SignalListener; -import reactor.util.observability.SignalListenerFactory; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; /** * A generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java index 2fe0099672..08617f2777 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java @@ -19,8 +19,8 @@ import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; -import reactor.util.observability.SignalListener; -import reactor.util.observability.SignalListenerFactory; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; /** * A {@link Fuseable} generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. diff --git a/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java b/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java index 0728a3a2ba..adbdd1fac1 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java @@ -34,6 +34,7 @@ import reactor.core.Scannable.Attr; import reactor.util.Metrics; +@Deprecated //this class is duplicated in reactor-core-micrometer final class SchedulerMetricDecorator implements BiFunction, Disposable { diff --git a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java index fc96ca910f..df4c705da1 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java @@ -557,7 +557,9 @@ public static boolean isNonBlockingThread(Thread t) { *

    * * @implNote Note that this is added as a decorator via Schedulers when enabling metrics for schedulers, which doesn't change the Factory. + * @deprecated prefer using the equivalent method in reactor-core-micrometer module. To be removed at the earliest in 3.6.0. */ + @Deprecated public static void enableMetrics() { if (Metrics.isInstrumentationAvailable()) { addExecutorServiceDecorator(SchedulerMetricDecorator.METRICS_DECORATOR_KEY, new SchedulerMetricDecorator()); @@ -567,7 +569,10 @@ public static void enableMetrics() { /** * If {@link #enableMetrics()} has been previously called, removes the decorator. * No-op if {@link #enableMetrics()} hasn't been called. + * + * @deprecated prefer using the equivalent method in reactor-core-micrometer module. To be removed at the earliest in 3.6.0. */ + @Deprecated public static void disableMetrics() { removeExecutorServiceDecorator(SchedulerMetricDecorator.METRICS_DECORATOR_KEY); } diff --git a/reactor-core/src/main/java/reactor/util/Metrics.java b/reactor-core/src/main/java/reactor/util/Metrics.java index 156ecfa5ee..4f09cd4dc0 100644 --- a/reactor-core/src/main/java/reactor/util/Metrics.java +++ b/reactor-core/src/main/java/reactor/util/Metrics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,12 @@ /** * Utilities around instrumentation and metrics with Micrometer. + * Deprecated as of 3.5.0, prefer using the new reactor-core-micrometer module. * * @author Simon Baslé + * @deprecated prefer using the new reactor-core-micrometer module Micrometer entrypoint. To be removed in 3.6.0 at the earliest. */ +@Deprecated public class Metrics { static final boolean isMicrometerAvailable; @@ -45,21 +48,36 @@ public class Metrics { /** * Check if the current runtime supports metrics / instrumentation, by * verifying if Micrometer is on the classpath. + *

    + * Note that this is regardless of whether the new reactor-core-micrometer module is also on the classpath (which + * could be the reason Micrometer is on the classpath in the first place). * * @return true if the Micrometer instrumentation facade is available + * @deprecated prefer explicit usage of the reactor-core-micrometer module. To be removed in 3.6.0 at the earliest. */ + @Deprecated public static final boolean isInstrumentationAvailable() { return isMicrometerAvailable; } + /** + * @deprecated Prefer using the reactor-core-micrometer module and configuring it using the Micrometer entrypoint. + */ + @Deprecated public static class MicrometerConfiguration { private static MeterRegistry registry = globalRegistry; /** * Set the registry to use in reactor for metrics related purposes. + *

    + * This is only used by the deprecated inline Micrometer instrumentation, and not by the reactor-core-micrometer + * module. + * * @return the previously configured registry. + * @deprecated prefer using Micrometer setup in new reactor-core-micrometer module. To be removed at the earliest in 3.6.0. */ + @Deprecated public static MeterRegistry useRegistry(MeterRegistry registry) { MeterRegistry previous = MicrometerConfiguration.registry; MicrometerConfiguration.registry = registry; @@ -68,11 +86,17 @@ public static MeterRegistry useRegistry(MeterRegistry registry) { /** * Get the registry used in reactor for metrics related purposes. + *

    + * This is only reflecting the deprecated inline Micrometer instrumentation configuration, and not the configuration + * of the reactor-core-micrometer module. + * + * @return the configured registry * @see Flux#metrics() + * @deprecated prefer using Micrometer setup in new reactor-core-micrometer module. To be removed at the earliest in 3.6.0. */ + @Deprecated public static MeterRegistry getRegistry() { - return MicrometerConfiguration.registry; + return registry; } } - } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java index cc6f5b1d28..394fe97264 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java @@ -36,8 +36,8 @@ import reactor.test.publisher.TestPublisher; import reactor.test.subscriber.TestSubscriber; import reactor.util.context.ContextView; -import reactor.util.observability.SignalListener; -import reactor.util.observability.SignalListenerFactory; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; import static org.assertj.core.api.Assertions.*; diff --git a/reactor-core/src/test/java/reactor/util/MetricsNoMicrometerTest.java b/reactor-core/src/test/java/reactor/util/MetricsNoMicrometerTest.java index 9803cddd55..36c12994b9 100644 --- a/reactor-core/src/test/java/reactor/util/MetricsNoMicrometerTest.java +++ b/reactor-core/src/test/java/reactor/util/MetricsNoMicrometerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,10 +38,11 @@ * * @author Simon Baslé */ +@Deprecated public class MetricsNoMicrometerTest { @Test - public void isMicrometerAvailable() { + public void micrometerIsNotAvailable() { assertThat(Metrics.isInstrumentationAvailable()).isFalse(); } diff --git a/reactor-core/src/withMicrometerTest/java/reactor/util/MetricsTest.java b/reactor-core/src/withMicrometerTest/java/reactor/util/MetricsTest.java index c0aa816511..b276d6d4fa 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/util/MetricsTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/util/MetricsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ public class MetricsTest { @Test public void smokeTestMicrometerActiveInTests() { assertThat(Metrics.isInstrumentationAvailable()).isTrue(); + Metrics.MicrometerConfiguration.getRegistry(); } } diff --git a/settings.gradle b/settings.gradle index 6ade4c3df6..8a1a2f1561 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,7 +19,7 @@ plugins { rootProject.name = 'reactor' -include 'benchmarks', 'reactor-core', 'reactor-test', 'reactor-tools' +include 'benchmarks', 'reactor-core', 'reactor-test', 'reactor-tools', 'reactor-core-micrometer' //libs catalog is declared in ./gradle/libs.versions.toml //TODO remove once Version Catalogs are stabilized. It is also activated in buildSrc From 6eecf859e4a740e942e9bb8770193136b47a140f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 20 Apr 2022 09:44:41 +0200 Subject: [PATCH 019/312] Add default contextView() implem to [Flux|Mono|Synchronous]Sink (#3021) This commit adds a default implementation to the `contextView()` method added in 3.5.0-M1 to the FluxSink, MonoSink and SynchronousSink classes. --- reactor-core/build.gradle | 5 ++- .../java/reactor/core/publisher/FluxSink.java | 4 +- .../java/reactor/core/publisher/MonoSink.java | 4 +- .../core/publisher/SynchronousSink.java | 4 +- ...java => ContextBestPracticesArchTest.java} | 44 +++++++++++++++++-- 5 files changed, 53 insertions(+), 8 deletions(-) rename reactor-core/src/test/java/reactor/core/{CurrentContextArchTest.java => ContextBestPracticesArchTest.java} (73%) diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index e31d21e900..a84e7d2bcb 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -184,7 +184,10 @@ task japicmp(type: JapicmpTask) { 'reactor.core.scheduler.Schedulers#newElastic(java.lang.String, int)', 'reactor.core.scheduler.Schedulers#newElastic(java.lang.String, int, boolean)', 'reactor.core.scheduler.Schedulers#newElastic(int, java.util.concurrent.ThreadFactory)', - 'reactor.core.scheduler.Schedulers$Factory#newElastic(int, java.util.concurrent.ThreadFactory)' + 'reactor.core.scheduler.Schedulers$Factory#newElastic(int, java.util.concurrent.ThreadFactory)', + 'reactor.core.publisher.FluxSink#contextView()', + 'reactor.core.publisher.MonoSink#contextView()', + 'reactor.core.publisher.SynchronousSink#contextView()' ] } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSink.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSink.java index ca70fcfe34..4d334eb28d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSink.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSink.java @@ -83,7 +83,9 @@ public interface FluxSink { * * @return the current subscriber {@link ContextView}. */ - ContextView contextView(); + default ContextView contextView() { + return currentContext(); + } /** diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSink.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSink.java index 40360bd5ea..ea4f1e12d4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSink.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSink.java @@ -83,7 +83,9 @@ public interface MonoSink { * * @return the current subscriber {@link ContextView}. */ - ContextView contextView(); + default ContextView contextView() { + return this.currentContext(); + } /** * Attaches a {@link LongConsumer} to this {@link MonoSink} that will be notified of diff --git a/reactor-core/src/main/java/reactor/core/publisher/SynchronousSink.java b/reactor-core/src/main/java/reactor/core/publisher/SynchronousSink.java index b25f0f52fb..702800dabe 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SynchronousSink.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SynchronousSink.java @@ -65,7 +65,9 @@ public interface SynchronousSink { * * @return the current subscriber {@link ContextView}. */ - ContextView contextView(); + default ContextView contextView() { + return currentContext(); + } /** * @param e the exception to signal, not null diff --git a/reactor-core/src/test/java/reactor/core/CurrentContextArchTest.java b/reactor-core/src/test/java/reactor/core/ContextBestPracticesArchTest.java similarity index 73% rename from reactor-core/src/test/java/reactor/core/CurrentContextArchTest.java rename to reactor-core/src/test/java/reactor/core/ContextBestPracticesArchTest.java index 9f94677c80..8cc11c93f1 100644 --- a/reactor-core/src/test/java/reactor/core/CurrentContextArchTest.java +++ b/reactor-core/src/test/java/reactor/core/ContextBestPracticesArchTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,15 @@ import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; +import reactor.core.publisher.MonoSink; +import reactor.core.publisher.SynchronousSink; + import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static org.assertj.core.api.Assertions.assertThat; -public class CurrentContextArchTest { +class ContextBestPracticesArchTest { static JavaClasses CORE_SUBSCRIBER_CLASSES = new ClassFileImporter() .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) @@ -50,7 +55,7 @@ void smokeTestWhereClassesLoaded() { } @Test - public void corePublishersShouldNotUseDefaultCurrentContext() { + void corePublishersShouldNotUseDefaultCurrentContext() { classes() .that().implement(CoreSubscriber.class) .and().doNotHaveModifier(JavaModifier.ABSTRACT) @@ -78,7 +83,7 @@ public void check(JavaClass item, ConditionEvents events) { @Test // This is ok as this class tests the deprecated FluxProcessor. Will be removed with it in 3.5. @SuppressWarnings("deprecation") - public void fluxProcessorsShouldNotUseDefaultCurrentContext() { + void fluxProcessorsShouldNotUseDefaultCurrentContext() { classes() .that().areAssignableTo(reactor.core.publisher.FluxProcessor.class) .and().doNotHaveModifier(JavaModifier.ABSTRACT) @@ -105,4 +110,35 @@ public void check(JavaClass item, ConditionEvents events) { }) .check(FLUXPROCESSOR_CLASSES); } + + @Test + void oldSinksShouldNotUseDefaultCurrentContext() { + classes() + .that().implement(SynchronousSink.class) + .or().implement(FluxSink.class) + .or().implement(MonoSink.class) + .and().doNotHaveModifier(JavaModifier.ABSTRACT) + .should(new ArchCondition("not use the default contextView()") { + @Override + public void check(JavaClass item, ConditionEvents events) { + boolean overridesMethod = item + .getAllMethods() + .stream() + .filter(it -> "contextView".equals(it.getName())) + .filter(it -> it.getRawParameterTypes().isEmpty()) + .anyMatch(it -> !it.getOwner().isEquivalentTo(SynchronousSink.class) + && !it.getOwner().isEquivalentTo(FluxSink.class) + && !it.getOwner().isEquivalentTo(MonoSink.class) + ); + + if (!overridesMethod) { + events.add(SimpleConditionEvent.violated( + item, + item.getFullName() + item.getSourceCodeLocation() + ": contextView() is not overridden" + )); + } + } + }) + .check(CORE_SUBSCRIBER_CLASSES); + } } From 4669fd785bc716fef0f448813b6025f0944025c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 20 Apr 2022 15:53:22 +0200 Subject: [PATCH 020/312] Upgrade to Micrometer 1.10.0-M1 for 3.5.0-M2 (#3025) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e342f3e5f8..c3d554abd6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ asciidoctor = "3.3.2" bytebuddy = "1.12.9" jmh = "1.35" junit = "5.8.2" -micrometer = "1.10.0-SNAPSHOT" +micrometer = "1.10.0-M1" reactiveStreams = "1.0.3" [libraries] From b315de6e67d8288c225a1891d3392f6cfc49a1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 20 Apr 2022 18:18:40 +0200 Subject: [PATCH 021/312] Polish README (#3007) - Remove outdated link to bintray - Force white background on README marble diagrams Fixes #3004. --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 25f11db963..94d64270c0 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ [![Join the chat at https://gitter.im/reactor/reactor](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/reactor/reactor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Reactor Core](https://maven-badges.herokuapp.com/maven-central/io.projectreactor/reactor-core/badge.svg?style=plastic)](https://mvnrepository.com/artifact/io.projectreactor/reactor-core) [![Latest](https://img.shields.io/github/release/reactor/reactor-core/all.svg)]() -[![Download](https://api.bintray.com/packages/spring/jars/io.projectreactor/images/download.svg)](https://bintray.com/spring/jars/io.projectreactor/_latestVersion) - [![CI on GHA](https://github.com/reactor/reactor-core/actions/workflows/publish.yml/badge.svg)](https://github.com/reactor/reactor-core/actions/workflows/publish.yml) [![Codecov](https://img.shields.io/codecov/c/github/reactor/reactor-core.svg)]() [![Code Quality: Java](https://img.shields.io/lgtm/grade/java/g/reactor/reactor-core.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/reactor/reactor-core/context:java) @@ -82,7 +80,7 @@ A Reactive Streams Publisher with basic flow operators. - Static factories on Flux allow for source generation from arbitrary callbacks types. - Instance methods allows operational building, materialized on each subscription (_Flux#subscribe()_, ...) or multicasting operations (such as _Flux#publish_ and _Flux#publishNext_). -[](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html) +[](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html) Flux in action : ```java @@ -101,7 +99,7 @@ A Reactive Streams Publisher constrained to *ZERO* or *ONE* element with appropr - Static factories on Mono allow for deterministic *zero or one* sequence generation from arbitrary callbacks types. - Instance methods allows operational building, materialized on each _Mono#subscribe()_ or _Mono#get()_ eventually called. -[](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html) +[](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html) Mono in action : ```java From 795536ca1e881fb17ffa4e73bfff3b7e89bd7cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 21 Apr 2022 12:16:42 +0200 Subject: [PATCH 022/312] [release] Prepare and release 3.5.0-M2 --- README.md | 6 +++--- gradle.properties | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 94d64270c0..898332614f 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.0-M1" - testCompile "io.projectreactor:reactor-test:3.5.0-M1" + compile "io.projectreactor:reactor-core:3.5.0-M2" + testCompile "io.projectreactor:reactor-test:3.5.0-M2" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.5.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.5.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.0-M1" + // implementation "io.projectreactor:reactor-tools:3.5.0-M2" } ``` diff --git a/gradle.properties b/gradle.properties index 35fcc5d7a7..38e623fcc7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-SNAPSHOT -bomVersion=2022.0.0-M1 -metricsMicrometerVersion=1.0.0-SNAPSHOT \ No newline at end of file +version=3.5.0-M2 +bomVersion=2022.0.0-M2 +metricsMicrometerVersion=1.0.0-M2 \ No newline at end of file From c1cfbaa6d85f4c40ae22ca56b0400b67910f827c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 21 Apr 2022 12:58:39 +0200 Subject: [PATCH 023/312] [release] Next development version 3.5.0-SNAPSHOT for M3 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 38e623fcc7..cdb0599c8e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-M2 +version=3.5.0-SNAPSHOT bomVersion=2022.0.0-M2 -metricsMicrometerVersion=1.0.0-M2 \ No newline at end of file +metricsMicrometerVersion=1.0.0-SNAPSHOT \ No newline at end of file From 82301b11099910031d7d4fbcc24c518cf1418a95 Mon Sep 17 00:00:00 2001 From: tmyksj <33417830+tmyksj@users.noreply.github.com> Date: Mon, 2 May 2022 18:37:46 +0900 Subject: [PATCH 024/312] Refguide on mobile to use screen space optimally (#3019) This commit changes the CSS (mainly overflow properties) so that the reference guide uses all screen space on mobile browsers. In particular, the base width of the content is no longer a fraction of fixed-width large tables. Fixes #3011. --- docs/asciidoc/stylesheets/reactor.css | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/asciidoc/stylesheets/reactor.css b/docs/asciidoc/stylesheets/reactor.css index 1cde818103..d0e19483a5 100644 --- a/docs/asciidoc/stylesheets/reactor.css +++ b/docs/asciidoc/stylesheets/reactor.css @@ -750,12 +750,19 @@ table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoo *:not(pre) > code { font-size: inherit; padding: 0; - white-space: nowrap; background-color: inherit; border: 0 solid #dddddd; -webkit-border-radius: 6px; border-radius: 6px; text-shadow: none; + overflow-wrap: anywhere; +} + +@media only screen and (min-width: 1280px) { + *:not(pre) > code { + white-space: nowrap; + overflow-wrap: normal; + } } pre, pre > code { @@ -1096,6 +1103,10 @@ p a > code:hover { color: #34302d; } +.paragraph { + overflow-wrap: break-word; +} + .imageblock, .literalblock, .listingblock, .mathblock, .verseblock, .videoblock { margin-bottom: 1.25em; margin-top: 1.25em; @@ -1106,6 +1117,10 @@ p a > code:hover { font-weight: bold; } +.tableblock { + overflow-wrap: anywhere; +} + .tableblock > caption { text-align: left; font-weight: bold; @@ -1122,6 +1137,7 @@ table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { border: 0; background: none; width: 100%; + table-layout: fixed; } .admonitionblock > table td.icon { @@ -1143,6 +1159,7 @@ table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { padding-right: 1.25em; border-left: 1px solid #dcd2c9; color: #34302d; + overflow-wrap: break-word; } .admonitionblock > table td.content > :last-child > :last-child { @@ -1402,6 +1419,7 @@ table.pyhltable .linenodiv { margin-top: -.25em; padding-bottom: 0.5625em; font-size: 0.8125em; + overflow-wrap: break-word; } .quoteblock .attribution br { @@ -1496,6 +1514,10 @@ ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, margin-bottom: 0.625em; } +.ulist { + overflow-wrap: break-word; +} + ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } @@ -1588,6 +1610,10 @@ td.hdlist1, td.hdlist2 { margin-top: -0.5em; } +.colist > table { + overflow-wrap: anywhere; +} + .colist > table tr > td:first-of-type { padding: 0 .75em; } @@ -1956,6 +1982,7 @@ body { font-weight: normal; position: relative; left: -0.0625em; + overflow-wrap: break-word; } #content h2 { From 4ca6c4521f607482002e78e54dc7803978fcfec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 23 May 2022 11:13:15 +0200 Subject: [PATCH 025/312] Scannable.tags() rework, add tagsDeduplicated() (#3037) This commit reworks Scannable.tags() with a slight behavioral change in the order in which duplicate tags are streamed, and adds a new `tagsDeduplicated()` method to get the tags from the hierarchy of parents without duplicates. In `tags()`, for a tag key that is defined multiple times, the first value to be iterated is now the one from the uppermost parent. That way, it is easier to eg. iterate and push to a `Map` in order to get values that are defined closer to the current Scannable step. Implementation-wise, tag operators now use a List to store the Tuple2 tags since the Set wasn't bringing any benefit. Fixes #2542. --- .../MicrometerListenerConfiguration.java | 19 +++---- .../MicrometerListenerConfigurationTest.java | 19 +++++++ reactor-core/build.gradle | 3 +- .../src/main/java/reactor/core/Scannable.java | 55 ++++++++++++++----- .../reactor/core/publisher/FluxMetrics.java | 23 +++----- .../java/reactor/core/publisher/FluxName.java | 51 +++++++++-------- .../core/publisher/FluxNameFuseable.java | 15 +++-- .../java/reactor/core/publisher/MonoName.java | 47 +++++++++------- .../core/publisher/MonoNameFuseable.java | 15 +++-- .../core/publisher/ParallelFluxName.java | 32 ++++++----- .../test/java/reactor/core/ScannableTest.java | 46 +++++++++++++++- .../core/publisher/FluxNameFuseableTest.java | 8 +-- .../reactor/core/publisher/FluxNameTest.java | 11 ++-- .../core/publisher/MonoNameFuseableTest.java | 8 +-- .../reactor/core/publisher/MonoNameTest.java | 8 +-- .../core/publisher/ParallelFluxNameTest.java | 8 +-- 16 files changed, 229 insertions(+), 139 deletions(-) diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java index 3a27b41571..d3397df082 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java @@ -17,8 +17,10 @@ package reactor.core.observability.micrometer; import java.util.LinkedList; +import java.util.List; import java.util.function.BiFunction; import java.util.function.BinaryOperator; +import java.util.stream.Collectors; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; @@ -95,13 +97,11 @@ static Tags resolveTags(Publisher source, Tags tags) { Scannable scannable = Scannable.from(source); if (scannable.isScanAvailable()) { - LinkedList> scannableTags = new LinkedList<>(); - scannable.tags().forEach(scannableTags::push); - return scannableTags.stream() - //Note the combiner below is for parallel streams, which won't be used - //For the identity, `commonTags` should be ok (even if reduce uses it multiple times) - //since it deduplicates - .reduce(tags, TAG_ACCUMULATOR, TAG_COMBINER); + List discoveredTags = scannable.tagsDeduplicated() + .entrySet().stream() + .map(e -> Tag.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + return tags.and(discoveredTags); } return tags; @@ -124,9 +124,4 @@ static Tags resolveTags(Publisher source, Tags tags) { this.sequenceName = sequenceName; this.registry = registryCandidate; } - - static final BiFunction, Tags> TAG_ACCUMULATOR = - (prev, tuple) -> prev.and(Tag.of(tuple.getT1(), tuple.getT2())); - - static final BinaryOperator TAG_COMBINER = Tags::and; } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java index b096d60858..83acffaa68 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java @@ -227,6 +227,25 @@ void resolveTags_multipleScatteredTagsSetAbove() { ); } + @Test + void resolveTags_multipleScatteredTagsSetAboveWithDeduplication() { + Tags defaultTags = Tags.of("common1", "commonValue1"); + Flux flux = Flux.just(1) + .tag("k1", "v1") + .tag("k2", "oldV2") + .filter(i -> i % 2 == 0) + .tag("k2", "v2") + .map(i -> i + 10); + + Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + + assertThat(resolvedTags.stream().map(Object::toString)).containsExactly( + "tag(common1=commonValue1)", + "tag(k1=v1)", + "tag(k2=v2)" + ); + } + @Test void resolveTags_notScannable() { Tags defaultTags = Tags.of("common1", "commonValue1"); diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index a84e7d2bcb..0c4111b87d 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -187,7 +187,8 @@ task japicmp(type: JapicmpTask) { 'reactor.core.scheduler.Schedulers$Factory#newElastic(int, java.util.concurrent.ThreadFactory)', 'reactor.core.publisher.FluxSink#contextView()', 'reactor.core.publisher.MonoSink#contextView()', - 'reactor.core.publisher.SynchronousSink#contextView()' + 'reactor.core.publisher.SynchronousSink#contextView()', + 'reactor.core.Scannable#tagsDeduplicated()' ] } diff --git a/reactor-core/src/main/java/reactor/core/Scannable.java b/reactor-core/src/main/java/reactor/core/Scannable.java index a4f388639a..8f5ee597f0 100644 --- a/reactor-core/src/main/java/reactor/core/Scannable.java +++ b/reactor-core/src/main/java/reactor/core/Scannable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,14 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.Spliterators; import java.util.function.Function; import java.util.regex.Pattern; @@ -33,6 +38,7 @@ import reactor.core.scheduler.Scheduler.Worker; import reactor.util.annotation.Nullable; import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; /** * A Scannable component exposes state in a non strictly memory consistent way and @@ -588,25 +594,46 @@ default T scanOrDefault(Attr key, T defaultValue) { } /** - * Visit this {@link Scannable} and its {@link #parents()} and stream all the - * observed tags + * Visit this {@link Scannable} and its {@link #parents()}, starting by the furthest reachable parent, + * and return a {@link Stream} of the tags which includes duplicates and outputs tags in declaration order + * (grandparent tag(s) > parent tag(s) > current tag(s)). + *

    + * Tags can only be discovered until no parent can be inspected, which happens either + * when the source publisher has been reached or when a non-reactor intermediate operator + * is present in the parent chain (i.e. a stage that is not {@link Scannable} for {@link Attr#PARENT}). * - * @return the stream of tags for this {@link Scannable} and its parents + * @return the stream of tags for this {@link Scannable} and its reachable parents, including duplicates + * @see #tagsDeduplicated() */ default Stream> tags() { - Stream> parentTags = - parents().flatMap(s -> s.scan(Attr.TAGS)); - - Stream> thisTags = scan(Attr.TAGS); + List sources = new LinkedList<>(); - if (thisTags == null) { - return parentTags; + Scannable aSource = this; + while (aSource != null && aSource.isScanAvailable()) { + sources.add(0, aSource); + aSource = aSource.scan(Attr.PARENT); } - return Stream.concat( - thisTags, - parentTags - ); + return sources.stream() + .flatMap(source -> source.scanOrDefault(Attr.TAGS, Stream.empty())); + } + + /** + * Visit this {@link Scannable} and its {@link #parents()}, starting by the furthest reachable parent, + * deduplicate tags that have a common key by favoring the value declared last (current tag(s) > parent tag(s) > grandparent tag(s)) + * and return a {@link Map} of the deduplicated tags. Note that while the values are the "latest", the key iteration order reflects + * the tags' declaration order. + *

    + * Tags can only be discovered until no parent can be inspected, which happens either + * when the source publisher has been reached or when a non-reactor intermediate operator + * is present in the parent chain (i.e. a stage that is not {@link Scannable} for {@link Attr#PARENT}). + * + * @return a {@link Map} of deduplicated tags from this {@link Scannable} and its reachable parents + * @see #tags() + */ + default Map tagsDeduplicated() { + return tags().collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2, + (s1, s2) -> s2, LinkedHashMap::new)); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java b/reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java index 10e698c3d5..64704cc26c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxMetrics.java @@ -16,10 +16,9 @@ package reactor.core.publisher; -import java.util.LinkedList; +import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; -import java.util.function.BinaryOperator; +import java.util.stream.Collectors; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Counter; @@ -30,12 +29,12 @@ import io.micrometer.core.instrument.Timer; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; + import reactor.core.CoreSubscriber; import reactor.core.Scannable; import reactor.util.Logger; import reactor.util.Loggers; import reactor.util.Metrics.MicrometerConfiguration; -import reactor.util.function.Tuple2; /** * Activate metrics gathering on a {@link Flux}, assuming Micrometer is on the classpath. @@ -258,10 +257,6 @@ public Object scanUnsafe(Attr key) { static final Logger log = Loggers.getLogger(FluxMetrics.class); - static final BiFunction, Tags> TAG_ACCUMULATOR = - (prev, tuple) -> prev.and(Tag.of(tuple.getT1(), tuple.getT2())); - static final BinaryOperator TAG_COMBINER = Tags::and; - /** * Extract the name from the upstream, and detect if there was an actual name (ie. distinct from {@link * Scannable#stepName()}) set by the user. @@ -300,13 +295,11 @@ static Tags resolveTags(Publisher source, Tags tags) { Scannable scannable = Scannable.from(source); if (scannable.isScanAvailable()) { - LinkedList> scannableTags = new LinkedList<>(); - scannable.tags().forEach(scannableTags::push); - return scannableTags.stream() - //Note the combiner below is for parallel streams, which won't be used - //For the identity, `commonTags` should be ok (even if reduce uses it multiple times) - //since it deduplicates - .reduce(tags, TAG_ACCUMULATOR, TAG_COMBINER); + List discoveredTags = scannable.tagsDeduplicated() + .entrySet().stream() + .map(e -> Tag.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + return tags.and(discoveredTags); } return tags; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxName.java b/reactor-core/src/main/java/reactor/core/publisher/FluxName.java index 28bd3c21b8..13c400411a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxName.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ package reactor.core.publisher; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Objects; -import java.util.Set; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; @@ -42,19 +42,18 @@ final class FluxName extends InternalFluxOperator { final String name; - final Set> tags; + final List> tagsWithDuplicates; - @SuppressWarnings("unchecked") static Flux createOrAppend(Flux source, String name) { Objects.requireNonNull(name, "name"); if (source instanceof FluxName) { FluxName s = (FluxName) source; - return new FluxName<>(s.source, name, s.tags); + return new FluxName<>(s.source, name, s.tagsWithDuplicates); } if (source instanceof FluxNameFuseable) { FluxNameFuseable s = (FluxNameFuseable) source; - return new FluxNameFuseable<>(s.source, name, s.tags); + return new FluxNameFuseable<>(s.source, name, s.tagsWithDuplicates); } if (source instanceof Fuseable) { return new FluxNameFuseable<>(source, name, null); @@ -62,41 +61,49 @@ static Flux createOrAppend(Flux source, String name) { return new FluxName<>(source, name, null); } - @SuppressWarnings("unchecked") static Flux createOrAppend(Flux source, String tagName, String tagValue) { Objects.requireNonNull(tagName, "tagName"); Objects.requireNonNull(tagValue, "tagValue"); - Set> tags = Collections.singleton(Tuples.of(tagName, tagValue)); + Tuple2 newTag = Tuples.of(tagName, tagValue); if (source instanceof FluxName) { FluxName s = (FluxName) source; - if(s.tags != null) { - tags = new HashSet<>(tags); - tags.addAll(s.tags); + List> tags; + if(s.tagsWithDuplicates != null) { + tags = new LinkedList<>(s.tagsWithDuplicates); + tags.add(newTag); + } + else { + tags = Collections.singletonList(newTag); } return new FluxName<>(s.source, s.name, tags); } + if (source instanceof FluxNameFuseable) { FluxNameFuseable s = (FluxNameFuseable) source; - if (s.tags != null) { - tags = new HashSet<>(tags); - tags.addAll(s.tags); + List> tags; + if (s.tagsWithDuplicates != null) { + tags = new LinkedList<>(s.tagsWithDuplicates); + tags.add(newTag); + } + else { + tags = Collections.singletonList(newTag); } return new FluxNameFuseable<>(s.source, s.name, tags); } if (source instanceof Fuseable) { - return new FluxNameFuseable<>(source, null, tags); + return new FluxNameFuseable<>(source, null, Collections.singletonList(newTag)); } - return new FluxName<>(source, null, tags); + return new FluxName<>(source, null, Collections.singletonList(newTag)); } FluxName(Flux source, @Nullable String name, - @Nullable Set> tags) { + @Nullable List> tags) { super(source); this.name = name; - this.tags = tags; + this.tagsWithDuplicates = tags; } @Override @@ -111,8 +118,8 @@ public Object scanUnsafe(Attr key) { return name; } - if (key == Attr.TAGS && tags != null) { - return tags.stream(); + if (key == Attr.TAGS && tagsWithDuplicates != null) { + return tagsWithDuplicates.stream(); } if (key == RUN_STYLE) { @@ -121,6 +128,4 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - - } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxNameFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxNameFuseable.java index 7ed8527004..756345ecf8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxNameFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxNameFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package reactor.core.publisher; -import java.util.Set; +import java.util.List; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; @@ -24,7 +24,6 @@ import reactor.util.function.Tuple2; import static reactor.core.Scannable.Attr.RUN_STYLE; -import static reactor.core.Scannable.Attr.RunStyle.SYNC; /** * An operator that just bears a name or a set of tags, which can be retrieved via the @@ -38,14 +37,14 @@ final class FluxNameFuseable extends InternalFluxOperator implements Fu final String name; - final Set> tags; + final List> tagsWithDuplicates; FluxNameFuseable(Flux source, @Nullable String name, - @Nullable Set> tags) { + @Nullable List> tags) { super(source); this.name = name; - this.tags = tags; + this.tagsWithDuplicates = tags; } @Override @@ -60,8 +59,8 @@ public Object scanUnsafe(Attr key) { return name; } - if (key == Attr.TAGS && tags != null) { - return tags.stream(); + if (key == Attr.TAGS && tagsWithDuplicates != null) { + return tagsWithDuplicates.stream(); } if (key == RUN_STYLE) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoName.java b/reactor-core/src/main/java/reactor/core/publisher/MonoName.java index 1e2262d797..1525e47f86 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoName.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ package reactor.core.publisher; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Objects; -import java.util.Set; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; @@ -38,7 +38,7 @@ final class MonoName extends InternalMonoOperator { final String name; - final Set> tags; + final List> tagsWithDuplicates; @SuppressWarnings("unchecked") static Mono createOrAppend(Mono source, String name) { @@ -46,11 +46,11 @@ static Mono createOrAppend(Mono source, String name) { if (source instanceof MonoName) { MonoName s = (MonoName) source; - return new MonoName<>(s.source, name, s.tags); + return new MonoName<>(s.source, name, s.tagsWithDuplicates); } if (source instanceof MonoNameFuseable) { MonoNameFuseable s = (MonoNameFuseable) source; - return new MonoNameFuseable<>(s.source, name, s.tags); + return new MonoNameFuseable<>(s.source, name, s.tagsWithDuplicates); } if (source instanceof Fuseable) { return new MonoNameFuseable<>(source, name, null); @@ -58,41 +58,48 @@ static Mono createOrAppend(Mono source, String name) { return new MonoName<>(source, name, null); } - @SuppressWarnings("unchecked") static Mono createOrAppend(Mono source, String tagName, String tagValue) { Objects.requireNonNull(tagName, "tagName"); Objects.requireNonNull(tagValue, "tagValue"); - Set> tags = Collections.singleton(Tuples.of(tagName, tagValue)); + Tuple2 newTag = Tuples.of(tagName, tagValue); if (source instanceof MonoName) { MonoName s = (MonoName) source; - if(s.tags != null) { - tags = new HashSet<>(tags); - tags.addAll(s.tags); + List> tags; + if(s.tagsWithDuplicates != null) { + tags = new LinkedList<>(s.tagsWithDuplicates); + tags.add(newTag); + } + else { + tags = Collections.singletonList(newTag); } return new MonoName<>(s.source, s.name, tags); } if (source instanceof MonoNameFuseable) { MonoNameFuseable s = (MonoNameFuseable) source; - if (s.tags != null) { - tags = new HashSet<>(tags); - tags.addAll(s.tags); + List> tags; + if (s.tagsWithDuplicates != null) { + tags = new LinkedList<>(s.tagsWithDuplicates); + tags.add(newTag); + } + else { + tags = Collections.singletonList(newTag); } return new MonoNameFuseable<>(s.source, s.name, tags); } if (source instanceof Fuseable) { - return new MonoNameFuseable<>(source, null, tags); + return new MonoNameFuseable<>(source, null, Collections.singletonList(newTag)); } - return new MonoName<>(source, null, tags); + return new MonoName<>(source, null, Collections.singletonList(newTag)); } MonoName(Mono source, @Nullable String name, - @Nullable Set> tags) { + @Nullable List> tags) { super(source); this.name = name; - this.tags = tags; + this.tagsWithDuplicates = tags; } @Override @@ -107,8 +114,8 @@ public Object scanUnsafe(Attr key) { return name; } - if (key == Attr.TAGS && tags != null) { - return tags.stream(); + if (key == Attr.TAGS && tagsWithDuplicates != null) { + return tagsWithDuplicates.stream(); } if (key == Attr.RUN_STYLE) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoNameFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoNameFuseable.java index 10c3351805..d13223f946 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoNameFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoNameFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package reactor.core.publisher; -import java.util.Set; +import java.util.List; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; @@ -24,7 +24,6 @@ import reactor.util.function.Tuple2; import static reactor.core.Scannable.Attr.RUN_STYLE; -import static reactor.core.Scannable.Attr.RunStyle.SYNC; /** * An operator that just bears a name or a set of tags, which can be retrieved via the @@ -37,14 +36,14 @@ final class MonoNameFuseable extends InternalMonoOperator implements Fu final String name; - final Set> tags; + final List> tagsWithDuplicates; MonoNameFuseable(Mono source, @Nullable String name, - @Nullable Set> tags) { + @Nullable List> tags) { super(source); this.name = name; - this.tags = tags; + this.tagsWithDuplicates = tags; } @Override @@ -59,8 +58,8 @@ public Object scanUnsafe(Attr key) { return name; } - if (key == Attr.TAGS && tags != null) { - return tags.stream(); + if (key == Attr.TAGS && tagsWithDuplicates != null) { + return tagsWithDuplicates.stream(); } if (key == RUN_STYLE) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxName.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxName.java index 0bc7f10340..8053ddc06f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxName.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ package reactor.core.publisher; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Objects; -import java.util.Set; import reactor.core.CoreSubscriber; import reactor.core.Scannable; @@ -42,7 +42,7 @@ final class ParallelFluxName extends ParallelFlux implements Scannable{ final String name; - final Set> tags; + final List> tagsWithDuplicates; @SuppressWarnings("unchecked") static ParallelFlux createOrAppend(ParallelFlux source, String name) { @@ -50,7 +50,7 @@ static ParallelFlux createOrAppend(ParallelFlux source, String name) { if (source instanceof ParallelFluxName) { ParallelFluxName s = (ParallelFluxName) source; - return new ParallelFluxName<>(s.source, name, s.tags); + return new ParallelFluxName<>(s.source, name, s.tagsWithDuplicates); } return new ParallelFluxName<>(source, name, null); } @@ -60,25 +60,29 @@ static ParallelFlux createOrAppend(ParallelFlux source, String tagName Objects.requireNonNull(tagName, "tagName"); Objects.requireNonNull(tagValue, "tagValue"); - Set> tags = Collections.singleton(Tuples.of(tagName, tagValue)); + Tuple2 newTag = Tuples.of(tagName, tagValue); if (source instanceof ParallelFluxName) { ParallelFluxName s = (ParallelFluxName) source; - if(s.tags != null) { - tags = new HashSet<>(tags); - tags.addAll(s.tags); + List> tags; + if(s.tagsWithDuplicates != null) { + tags = new LinkedList<>(s.tagsWithDuplicates); + tags.add(newTag); + } + else { + tags = Collections.singletonList(newTag); } return new ParallelFluxName<>(s.source, s.name, tags); } - return new ParallelFluxName<>(source, null, tags); + return new ParallelFluxName<>(source, null, Collections.singletonList(newTag)); } ParallelFluxName(ParallelFlux source, @Nullable String name, - @Nullable Set> tags) { + @Nullable List> tags) { this.source = source; this.name = name; - this.tags = tags; + this.tagsWithDuplicates = tags; } @Override @@ -98,8 +102,8 @@ public Object scanUnsafe(Attr key) { return name; } - if (key == Attr.TAGS && tags != null) { - return tags.stream(); + if (key == Attr.TAGS && tagsWithDuplicates != null) { + return tagsWithDuplicates.stream(); } if (key == Attr.PARENT) return source; diff --git a/reactor-core/src/test/java/reactor/core/ScannableTest.java b/reactor-core/src/test/java/reactor/core/ScannableTest.java index 76b68030b5..e53c90e42e 100644 --- a/reactor-core/src/test/java/reactor/core/ScannableTest.java +++ b/reactor-core/src/test/java/reactor/core/ScannableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,11 @@ package reactor.core; import java.time.Duration; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Stream; @@ -475,6 +477,46 @@ public void taggedAppendedParallelFluxTest() { .containsExactlyInAnyOrder(Tuples.of("1", "One"), Tuples.of( "2", "Two")); } + @Test + void tagsIncludesDuplicatesAndReverseDeclarationOrder() { + Flux f = Flux.just(1, 2, 3) + .tag("key2", "earlyValue2") + .tag("key1", "earlyValue1") + .tag("key1", "earlyOverwriteValue1") + .filter(i -> true) //this separates two macro-fused tag stages + .tag("key1", "lateValue1") + .tag("key2", "lateValue2") + .tag("key1", "lateOverwriteValue1"); + + assertThat(Scannable.from(f).tags()) + .containsExactly( + Tuples.of("key2", "earlyValue2"), + Tuples.of("key1", "earlyValue1"), + Tuples.of("key1", "earlyOverwriteValue1"), + Tuples.of("key1", "lateValue1"), + Tuples.of("key2", "lateValue2"), + Tuples.of("key1", "lateOverwriteValue1") + ); + } + + @Test + void tagsDeduplicatedUsesLatestValueButOriginalKeyOrder() { + Flux f = Flux.just(1, 2, 3) + .tag("key2", "earlyValue2") + .tag("key1", "earlyValue1") + .tag("key1", "earlyOverwriteValue1") + .filter(i -> true) //this separates two macro-fused tag stages + .tag("key1", "lateValue1") + .tag("key2", "lateValue2") + .tag("key1", "lateOverwriteValue1"); + + assertThat(Scannable.from(f).tagsDeduplicated()) + .containsExactly( + new AbstractMap.SimpleImmutableEntry<>("key2", "lateValue2"), + new AbstractMap.SimpleImmutableEntry<>("key1", "lateOverwriteValue1") + ); + } + @Test public void scanForParentIsSafe() { Scannable scannable = key -> "String"; @@ -615,7 +657,7 @@ public void operatorChainWithCheckpoint() { assertThat(Scannable.from(flux).steps()) .containsExactly( "source(FluxJust)", - "Flux.checkpoint ⇢ at reactor.core.ScannableTest.operatorChainWithCheckpoint(ScannableTest.java:612)", + "Flux.checkpoint ⇢ at reactor.core.ScannableTest.operatorChainWithCheckpoint(ScannableTest.java:654)", "map" ); } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxNameFuseableTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxNameFuseableTest.java index bbb7c07d38..33a2202b83 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxNameFuseableTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxNameFuseableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package reactor.core.publisher; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -33,9 +35,7 @@ public class FluxNameFuseableTest { public void scanOperator() throws Exception { Tuple2 tag1 = Tuples.of("foo", "oof"); Tuple2 tag2 = Tuples.of("bar", "rab"); - Set> tags = new HashSet<>(); - tags.add(tag1); - tags.add(tag2); + List> tags = Arrays.asList(tag1, tag2); Flux source = Flux.range(1, 4).map(i -> i); FluxNameFuseable test = new FluxNameFuseable<>(source, "foo", tags); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxNameTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxNameTest.java index 36b1ff54d8..728d5a91fc 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxNameTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxNameTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,9 @@ package reactor.core.publisher; -import java.util.HashSet; -import java.util.Set; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -33,9 +34,7 @@ public class FluxNameTest { public void scanOperator() throws Exception { Tuple2 tag1 = Tuples.of("foo", "oof"); Tuple2 tag2 = Tuples.of("bar", "rab"); - Set> tags = new HashSet<>(); - tags.add(tag1); - tags.add(tag2); + List> tags = Arrays.asList(tag1, tag2); Flux source = Flux.range(1, 4).map(i -> i); FluxName test = new FluxName<>(source, "foo", tags); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoNameFuseableTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoNameFuseableTest.java index c135577c9c..242420ac46 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoNameFuseableTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoNameFuseableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package reactor.core.publisher; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -33,9 +35,7 @@ public class MonoNameFuseableTest { public void scanOperator() throws Exception { Tuple2 tag1 = Tuples.of("foo", "oof"); Tuple2 tag2 = Tuples.of("bar", "rab"); - Set> tags = new HashSet<>(); - tags.add(tag1); - tags.add(tag2); + List> tags = Arrays.asList(tag1, tag2); Mono source = Mono.just(1).map(i -> i); MonoNameFuseable test = new MonoNameFuseable<>(source, "foo", tags); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoNameTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoNameTest.java index 44a95b6e15..039fcf3108 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoNameTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoNameTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package reactor.core.publisher; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -33,9 +35,7 @@ public class MonoNameTest { public void scanOperator() throws Exception { Tuple2 tag1 = Tuples.of("foo", "oof"); Tuple2 tag2 = Tuples.of("bar", "rab"); - Set> tags = new HashSet<>(); - tags.add(tag1); - tags.add(tag2); + List> tags = Arrays.asList(tag1, tag2); Mono source = Mono.just(1).map(i -> i); MonoName test = new MonoName<>(source, "foo", tags); diff --git a/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxNameTest.java b/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxNameTest.java index 1a38bb1ba6..10bb8b3a0f 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxNameTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxNameTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package reactor.core.publisher; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Stream; @@ -43,9 +45,7 @@ public void parallelism() { public void scanOperator() throws Exception { Tuple2 tag1 = Tuples.of("foo", "oof"); Tuple2 tag2 = Tuples.of("bar", "rab"); - Set> tags = new HashSet<>(); - tags.add(tag1); - tags.add(tag2); + List> tags = Arrays.asList(tag1, tag2); ParallelFlux source = Flux.range(1, 4).parallel(3); ParallelFluxName test = new ParallelFluxName<>(source, "foo", tags); From 178e0c7cf799122afdd6bf89be32975d2b433609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 24 May 2022 16:01:05 +0200 Subject: [PATCH 026/312] Remove Processors from public API (#3051) This commit removes the Processor implementations from the public API. These have had a Sinks alternative since 3.4.0, at which point they were deprecated. In 3.4.19, the ability to subscribe a `Sinks.Many` to an upstream publisher is partly restored in a non-deprecated fashion with the `Sinks.ManyWithUpstream` specialization of `Sinks.Many`. This commit makes the relevant processor implementations that are also implementing the various flavor of Sinks package-private. Most have been renamed to use the `SinkMany` prefix in class name for grouping in the class list. Others like the `DelegateProcessor` have been removed entirely. This commit also amends the javadocs and reference guide to remove mentions of processors (or tone them done) in the documentation. Finally, japicmp exclusions have been added for removed classes. --- docs/api/overview.html | 12 +- docs/asciidoc/coreFeatures.adoc | 4 +- docs/asciidoc/processors.adoc | 43 +- reactor-core/build.gradle | 12 +- .../core/publisher/DelegateProcessor.java | 127 ------ .../core/publisher/DirectInnerContainer.java | 43 -- .../core/publisher/DirectProcessor.java | 359 --------------- .../java/reactor/core/publisher/Flux.java | 15 +- .../reactor/core/publisher/FluxProcessor.java | 279 ------------ .../java/reactor/core/publisher/Mono.java | 22 +- .../reactor/core/publisher/MonoProcessor.java | 239 ---------- .../reactor/core/publisher/NextProcessor.java | 110 ++++- .../core/publisher/SinkManyBestEffort.java | 28 +- ...sor.java => SinkManyEmitterProcessor.java} | 167 ++----- ...ssor.java => SinkManyReplayProcessor.java} | 159 +++---- ...astProcessor.java => SinkManyUnicast.java} | 238 +++------- ...ava => SinkManyUnicastNoBackpressure.java} | 20 +- .../java/reactor/core/publisher/Sinks.java | 1 - .../reactor/core/publisher/SinksSpecs.java | 74 ++-- .../reactor/core/publisher/package-info.java | 21 +- .../tck/AbstractProcessorVerification.java | 54 --- .../tck/EmitterProcessorVerification.java | 50 --- .../core/ContextBestPracticesArchTest.java | 40 -- .../core/publisher/DelegateProcessorTest.java | 50 --- .../core/publisher/DirectProcessorTest.java | 293 ------------- .../core/publisher/FluxProcessorTest.java | 275 ------------ .../core/publisher/NextProcessorTest.java | 32 +- .../publisher/SinkManyBestEffortTest.java | 7 +- ...java => SinkManyEmitterProcessorTest.java} | 405 ++++++----------- ....java => SinkManyReplayProcessorTest.java} | 110 +++-- ...=> SinkManyUnicastNoBackpressureTest.java} | 32 +- .../core/publisher/SinkManyUnicastTest.java | 320 ++++++++++++++ .../reactor/core/publisher/SinksTest.java | 3 +- .../core/publisher/UnicastProcessorTest.java | 411 ------------------ .../tools/agent/ReactorDebugAgentTest.java | 11 - 35 files changed, 872 insertions(+), 3194 deletions(-) delete mode 100644 reactor-core/src/main/java/reactor/core/publisher/DelegateProcessor.java delete mode 100644 reactor-core/src/main/java/reactor/core/publisher/DirectInnerContainer.java delete mode 100644 reactor-core/src/main/java/reactor/core/publisher/DirectProcessor.java delete mode 100644 reactor-core/src/main/java/reactor/core/publisher/FluxProcessor.java delete mode 100644 reactor-core/src/main/java/reactor/core/publisher/MonoProcessor.java rename reactor-core/src/main/java/reactor/core/publisher/{EmitterProcessor.java => SinkManyEmitterProcessor.java} (71%) rename reactor-core/src/main/java/reactor/core/publisher/{ReplayProcessor.java => SinkManyReplayProcessor.java} (69%) rename reactor-core/src/main/java/reactor/core/publisher/{UnicastProcessor.java => SinkManyUnicast.java} (58%) rename reactor-core/src/main/java/reactor/core/publisher/{UnicastManySinkNoBackpressure.java => SinkManyUnicastNoBackpressure.java} (87%) delete mode 100644 reactor-core/src/tckTest/java/reactor/core/publisher/tck/AbstractProcessorVerification.java delete mode 100644 reactor-core/src/tckTest/java/reactor/core/publisher/tck/EmitterProcessorVerification.java delete mode 100644 reactor-core/src/test/java/reactor/core/publisher/DelegateProcessorTest.java delete mode 100644 reactor-core/src/test/java/reactor/core/publisher/DirectProcessorTest.java delete mode 100644 reactor-core/src/test/java/reactor/core/publisher/FluxProcessorTest.java rename reactor-core/src/test/java/reactor/core/publisher/{EmitterProcessorTest.java => SinkManyEmitterProcessorTest.java} (67%) rename reactor-core/src/test/java/reactor/core/publisher/{ReplayProcessorTest.java => SinkManyReplayProcessorTest.java} (81%) rename reactor-core/src/test/java/reactor/core/publisher/{UnicastManySinkNoBackpressureTest.java => SinkManyUnicastNoBackpressureTest.java} (82%) create mode 100644 reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastTest.java delete mode 100644 reactor-core/src/test/java/reactor/core/publisher/UnicastProcessorTest.java diff --git a/docs/api/overview.html b/docs/api/overview.html index 8eefc51754..f9f4f96e1f 100644 --- a/docs/api/overview.html +++ b/docs/api/overview.html @@ -8,15 +8,9 @@ Routine use of Reactor will involve the composable Reactive Streams -Flux, Mono and message passing Processor located -under -reactor.core.publisher. +Flux +and Mono +located under reactor.core.publisher.


    diff --git a/docs/asciidoc/coreFeatures.adoc b/docs/asciidoc/coreFeatures.adoc index dda3b969df..ddced66066 100644 --- a/docs/asciidoc/coreFeatures.adoc +++ b/docs/asciidoc/coreFeatures.adoc @@ -1099,6 +1099,6 @@ converted.subscribe( ---- ==== -[[processors]] -== Processors and Sinks +[[sinks]] +== Sinks include::processors.adoc[leveloffset=3] \ No newline at end of file diff --git a/docs/asciidoc/processors.adoc b/docs/asciidoc/processors.adoc index 846f8b6417..98c72c0665 100644 --- a/docs/asciidoc/processors.adoc +++ b/docs/asciidoc/processors.adoc @@ -1,3 +1,21 @@ +In Reactor a sink is a class that allows safe manual triggering of signals in a standalone fashion, creating a `Publisher`-like structure capable of dealing with multiple `Subscriber` (with the exception of `unicast()` flavors). + +Before `3.5.0`, there was also a set of `Processor` implementations which has been phased out. + +[[sinks-intro]] += Safely Produce from Multiple Threads by Using `Sinks.One` and `Sinks.Many` + +Default flavors of `Sinks` exposed by reactor-core ensure that multi-threaded usage is detected +and cannot lead to spec violations or undefined behavior from the perspective of downstream +subscribers. When using the `tryEmit*` API, parallel calls fail fast. When using the `emit*` +API, the provided `EmissionFailureHandler` may allow to retry on contention (eg. busy looping), +otherwise the sink will terminate with an error. + +This is an improvement over `Processor.onNext`, which must be synchronized externally or +lead to undefined behavior from the perspective of the downstream subscribers. + +[NOTE] +==== Processors are a special kind of `Publisher` that are also a `Subscriber`. They were originally intended as a possible representation of an intermediate step that could then be shared between Reactive Streams implementations. @@ -12,28 +30,7 @@ Processors are actually probably marginally useful, unless one comes across a Re based API that requires a `Subscriber` to be passed, rather than exposing a `Publisher`. Sinks are usually a better alternative. -In Reactor a sink is a class that allows safe manual triggering of signals. It can either -be associated to a subscription (from inside an operator) or completely standalone. - -Since `3.4.0`, sinks become the first class citizen and `Processor` are being phased out entirely: - - - both abstract and concrete `FluxProcessor` and `MonoProcessor` are deprecated and slated for removal in 3.5.0 - - sinks which are not produced by an operator are constructed through factory methods in the `Sinks` class. - - we expect all processor usages to be replaceable with either an existing operator or a new sink from `Sinks`. - Users have time until 3.5 to surface situations where that isn't the case, while falling back to using the deprecated APIs - in the meantime. - -[[sinks]] -= Safely Produce from Multiple Threads by Using `Sinks.One` and `Sinks.Many` - -Default flavors of `Sinks` exposed by reactor-core ensure that multi-threaded usage is detected -and cannot lead to spec violations or undefined behavior from the perspective of downstream -subscribers. When using the `tryEmit*` API, parallel calls fail fast. When using the `emit*` -API, the provided `EmissionFailureHandler` may allow to retry on contention (eg. busy looping), -otherwise the sink will terminate with an error. - -This is an improvement over `Processor.onNext`, which must be synchronized externally or -lead to undefined behavior from the perspective of the downstream subscribers. +==== The `Sinks` builder provide a guided API to the main supported producer types. You will recognize some of the behavior found in `Flux` such as `onBackpressureBuffer`. @@ -85,7 +82,7 @@ The `Sinks` categories are: . `one()`: a sink that will play a single element to its subscribers . `empty()`: a sink that will play a terminal signal only to its subscribers (error or complete), but can still be viewed as a `Mono` (notice the generic type ``). -[[processor-overview]] +[[sinks-overview]] = Overview of Available Sinks == Sinks.many().unicast().onBackpressureBuffer(args?) diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 5bbfabecf1..7b3842ca65 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -164,7 +164,14 @@ task japicmp(type: JapicmpTask) { "reactor.core.publisher.FluxExtensionsKt", "reactor.core.publisher.MonoExtensionsKt", "reactor.core.publisher.MonoWhenFunctionsKt", - "reactor.util.function.TupleExtensionsKt" + "reactor.util.function.TupleExtensionsKt", + "reactor.core.publisher.DirectProcessor", + "reactor.core.publisher.DirectInnerContainer", + "reactor.core.publisher.FluxProcessor", + "reactor.core.publisher.EmitterProcessor", + "reactor.core.publisher.MonoProcessor", + "reactor.core.publisher.ReplayProcessor", + "reactor.core.publisher.UnicastProcessor" ] methodExcludes = [ 'reactor.core.publisher.Mono#doAfterSuccessOrError(java.util.function.BiConsumer)', @@ -189,7 +196,8 @@ task japicmp(type: JapicmpTask) { 'reactor.core.publisher.MonoSink#contextView()', 'reactor.core.publisher.SynchronousSink#contextView()', 'reactor.core.publisher.Sinks#unsafe()', - 'reactor.core.Scannable#tagsDeduplicated()' + 'reactor.core.Scannable#tagsDeduplicated()', + "reactor.core.publisher.Mono#toProcessor()" ] } diff --git a/reactor-core/src/main/java/reactor/core/publisher/DelegateProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/DelegateProcessor.java deleted file mode 100644 index 5cc3a23234..0000000000 --- a/reactor-core/src/main/java/reactor/core/publisher/DelegateProcessor.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -import java.util.Objects; -import java.util.stream.Stream; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.Exceptions; -import reactor.core.Scannable; -import reactor.util.annotation.Nullable; -import reactor.util.context.Context; - -/** - * @author Stephane Maldini - */ -@Deprecated -final class DelegateProcessor extends FluxProcessor { - - final Publisher downstream; - final Subscriber upstream; - - DelegateProcessor(Publisher downstream, - Subscriber upstream) { - this.downstream = Objects.requireNonNull(downstream, "Downstream must not be null"); - this.upstream = Objects.requireNonNull(upstream, "Upstream must not be null"); - } - - @Override - public Context currentContext() { - if(upstream instanceof CoreSubscriber){ - return ((CoreSubscriber)upstream).currentContext(); - } - return Context.empty(); - } - - @Override - public void onComplete() { - upstream.onComplete(); - } - - @Override - public void onError(Throwable t) { - upstream.onError(t); - } - - @Override - public void onNext(IN in) { - upstream.onNext(in); - } - - @Override - public void onSubscribe(Subscription s) { - upstream.onSubscribe(s); - } - - @Override - public void subscribe(CoreSubscriber actual) { - Objects.requireNonNull(actual, "subscribe"); - downstream.subscribe(actual); - } - - @Override - @SuppressWarnings("unchecked") - public boolean isSerialized() { - return upstream instanceof SerializedSubscriber || - (upstream instanceof FluxProcessor && - ((FluxProcessor)upstream).isSerialized()); - } - - @Override - public Stream inners() { - //noinspection ConstantConditions - return Scannable.from(upstream) - .inners(); - } - - @Override - public int getBufferSize() { - //noinspection ConstantConditions - return Scannable.from(upstream) - .scanOrDefault(Attr.CAPACITY, super.getBufferSize()); - } - - @Override - @Nullable - public Throwable getError() { - //noinspection ConstantConditions - return Scannable.from(upstream) - .scanOrDefault(Attr.ERROR, super.getError()); - } - - @Override - public boolean isTerminated() { - //noinspection ConstantConditions - return Scannable.from(upstream) - .scanOrDefault(Attr.TERMINATED, super.isTerminated()); - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) { - return downstream; - } - //noinspection ConstantConditions - return Scannable.from(upstream) - .scanUnsafe(key); - } -} diff --git a/reactor-core/src/main/java/reactor/core/publisher/DirectInnerContainer.java b/reactor-core/src/main/java/reactor/core/publisher/DirectInnerContainer.java deleted file mode 100644 index d0b2c882c0..0000000000 --- a/reactor-core/src/main/java/reactor/core/publisher/DirectInnerContainer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -/** - * A package-private interface allowing to mutualize logic between {@link DirectProcessor} - * and {@link SinkManyBestEffort}. - * - * @author Simon Baslé - */ -interface DirectInnerContainer { - - /** - * Add a new {@link SinkManyBestEffort.DirectInner} to this publisher. - * - * @param s the new {@link SinkManyBestEffort.DirectInner} to add - * - * @return {@code true} if the inner could be added, {@code false} if the publisher cannot accept new subscribers - */ - boolean add(SinkManyBestEffort.DirectInner s); - - /** - * Remove an {@link SinkManyBestEffort.DirectInner} from this publisher. Does nothing if the inner is not currently managed - * by the publisher. - * - * @param s the {@link SinkManyBestEffort.DirectInner} to remove - */ - void remove(SinkManyBestEffort.DirectInner s); -} diff --git a/reactor-core/src/main/java/reactor/core/publisher/DirectProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/DirectProcessor.java deleted file mode 100644 index 859f3f7a60..0000000000 --- a/reactor-core/src/main/java/reactor/core/publisher/DirectProcessor.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -import java.util.Objects; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import java.util.stream.Stream; - -import org.reactivestreams.Subscription; - -import reactor.core.CoreSubscriber; -import reactor.core.Exceptions; -import reactor.core.Scannable; -import reactor.core.publisher.SinkManyBestEffort.DirectInner; -import reactor.core.publisher.Sinks.EmitResult; -import reactor.util.annotation.Nullable; -import reactor.util.context.Context; - -/** - * Dispatches onNext, onError and onComplete signals to zero-to-many Subscribers. - * Please note, that along with multiple consumers, current implementation of - * DirectProcessor supports multiple producers. However, all producers must produce - * messages on the same Thread, otherwise - * Reactive Streams Spec contract is - * violated. - *

    - * - *

    - * - *
    - *
    - * - *

    - * Note: DirectProcessor does not coordinate backpressure between its - * Subscribers and the upstream, but consumes its upstream in an - * unbounded manner. - * In the case where a downstream Subscriber is not ready to receive items (hasn't - * requested yet or enough), it will be terminated with an - * {@link IllegalStateException}. - * Hence in terms of interaction model, DirectProcessor only supports PUSH from the - * source through the processor to the Subscribers. - * - *

    - * - *

    - *

    - * - *
    - *
    - * - *

    - * Note: If there are no Subscribers, upstream items are dropped and only - * the terminal events are retained. A terminated DirectProcessor will emit the - * terminal signal to late subscribers. - * - *

    - * - *

    - *

    - * - *
    - *
    - * - *

    - * Note: The implementation ignores Subscriptions set via onSubscribe. - *

    - * - * @param the input and output value type - * @deprecated To be removed in 3.5, prefer clear cut usage of {@link Sinks}. Closest sink - * is {@link Sinks.MulticastSpec#directBestEffort() Sinks.many().multicast().directBestEffort()}, - * except it doesn't terminate overflowing downstreams. - */ -@Deprecated -public final class DirectProcessor extends FluxProcessor - implements DirectInnerContainer { - - /** - * Create a new {@link DirectProcessor} - * - * @param Type of processed signals - * - * @return a fresh processor - * @deprecated To be removed in 3.5. Closest sink is {@link Sinks.MulticastSpec#directBestEffort() Sinks.many().multicast().directBestEffort()}, - * except it doesn't terminate overflowing downstreams. - */ - @Deprecated - public static DirectProcessor create() { - return new DirectProcessor<>(); - } - - @SuppressWarnings("unchecked") - private volatile DirectInner[] subscribers = SinkManyBestEffort.EMPTY; - @SuppressWarnings("rawtypes") - private static final AtomicReferenceFieldUpdaterSUBSCRIBERS = - AtomicReferenceFieldUpdater.newUpdater(DirectProcessor.class, DirectInner[].class, "subscribers"); - - Throwable error; - - DirectProcessor() { - } - - @Override - public int getPrefetch() { - return Integer.MAX_VALUE; - } - - @Override - public Context currentContext() { - return Operators.multiSubscribersContext(subscribers); - } - - @Override - public void onSubscribe(Subscription s) { - Objects.requireNonNull(s, "s"); - if (subscribers != SinkManyBestEffort.TERMINATED) { - s.request(Long.MAX_VALUE); - } - else { - s.cancel(); - } - } - - @Override - public void onComplete() { - //no particular error condition handling for onComplete - @SuppressWarnings("unused") Sinks.EmitResult emitResult = tryEmitComplete(); - } - - private void emitComplete() { - //no particular error condition handling for onComplete - @SuppressWarnings("unused") EmitResult emitResult = tryEmitComplete(); - } - - private EmitResult tryEmitComplete() { - @SuppressWarnings("unchecked") - DirectInner[] inners = SUBSCRIBERS.getAndSet(this, SinkManyBestEffort.TERMINATED); - - if (inners == SinkManyBestEffort.TERMINATED) { - return EmitResult.FAIL_TERMINATED; - } - - for (DirectInner s : inners) { - s.emitComplete(); - } - return EmitResult.OK; - } - - @Override - public void onError(Throwable throwable) { - emitError(throwable); - } - - private void emitError(Throwable error) { - Sinks.EmitResult result = tryEmitError(error); - if (result == EmitResult.FAIL_TERMINATED) { - Operators.onErrorDroppedMulticast(error, subscribers); - } - } - - private Sinks.EmitResult tryEmitError(Throwable t) { - Objects.requireNonNull(t, "t"); - - @SuppressWarnings("unchecked") - DirectInner[] inners = SUBSCRIBERS.getAndSet(this, SinkManyBestEffort.TERMINATED); - - if (inners == SinkManyBestEffort.TERMINATED) { - return EmitResult.FAIL_TERMINATED; - } - - error = t; - for (DirectInner s : inners) { - s.emitError(t); - } - return Sinks.EmitResult.OK; - } - - private void emitNext(T value) { - switch (tryEmitNext(value)) { - case FAIL_ZERO_SUBSCRIBER: - //we want to "discard" without rendering the sink terminated. - // effectively NO-OP cause there's no subscriber, so no context :( - break; - case FAIL_OVERFLOW: - Operators.onDiscard(value, currentContext()); - //the emitError will onErrorDropped if already terminated - emitError(Exceptions.failWithOverflow("Backpressure overflow during Sinks.Many#emitNext")); - break; - case FAIL_CANCELLED: - Operators.onDiscard(value, currentContext()); - break; - case FAIL_TERMINATED: - Operators.onNextDroppedMulticast(value, subscribers); - break; - case OK: - break; - default: - throw new IllegalStateException("unexpected return code"); - } - } - - @Override - public void onNext(T t) { - emitNext(t); - } - - private EmitResult tryEmitNext(T t) { - Objects.requireNonNull(t, "t"); - - DirectInner[] inners = subscribers; - - if (inners == SinkManyBestEffort.TERMINATED) { - return EmitResult.FAIL_TERMINATED; - } - if (inners == SinkManyBestEffort.EMPTY) { - return Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER; - } - - for (DirectInner s : inners) { - s.directEmitNext(t); - } - return Sinks.EmitResult.OK; - } - - @Override - protected boolean isIdentityProcessor() { - return true; - } - - @Override - public void subscribe(CoreSubscriber actual) { - Objects.requireNonNull(actual, "subscribe"); - - DirectInner p = new DirectInner<>(actual, this); - actual.onSubscribe(p); - - if (add(p)) { - if (p.isCancelled()) { - remove(p); - } - } - else { - Throwable e = error; - if (e != null) { - actual.onError(e); - } - else { - actual.onComplete(); - } - } - } - - @Override - public Stream inners() { - return Stream.of(subscribers); - } - - @Override - public boolean isTerminated() { - return SinkManyBestEffort.TERMINATED == subscribers; - } - - @Override - public long downstreamCount() { - return subscribers.length; - } - - @Override - public boolean add(DirectInner s) { - DirectInner[] a = subscribers; - if (a == SinkManyBestEffort.TERMINATED) { - return false; - } - - synchronized (this) { - a = subscribers; - if (a == SinkManyBestEffort.TERMINATED) { - return false; - } - int len = a.length; - - @SuppressWarnings("unchecked") DirectInner[] b = new DirectInner[len + 1]; - System.arraycopy(a, 0, b, 0, len); - b[len] = s; - - subscribers = b; - - return true; - } - } - - @Override - @SuppressWarnings("unchecked") - public void remove(DirectInner s) { - DirectInner[] a = subscribers; - if (a == SinkManyBestEffort.TERMINATED || a == SinkManyBestEffort.EMPTY) { - return; - } - - synchronized (this) { - a = subscribers; - if (a == SinkManyBestEffort.TERMINATED || a == SinkManyBestEffort.EMPTY) { - return; - } - int len = a.length; - - int j = -1; - - for (int i = 0; i < len; i++) { - if (a[i] == s) { - j = i; - break; - } - } - if (j < 0) { - return; - } - if (len == 1) { - subscribers = SinkManyBestEffort.EMPTY; - return; - } - - DirectInner[] b = new DirectInner[len - 1]; - System.arraycopy(a, 0, b, 0, j); - System.arraycopy(a, j + 1, b, j, len - j - 1); - - subscribers = b; - } - } - - @Override - public boolean hasDownstreams() { - DirectInner[] s = subscribers; - return s != SinkManyBestEffort.EMPTY && s != SinkManyBestEffort.TERMINATED; - } - - @Override - @Nullable - public Throwable getError() { - if (subscribers == SinkManyBestEffort.TERMINATED) { - return error; - } - return null; - } - -} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index e7b461c694..9060c4a29b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -1914,7 +1914,7 @@ public static Flux range(int start, int count) { * @param mergedPublishers The {@link Publisher} of {@link Publisher} to switch on and mirror. * @param the produced type * - * @return a {@link FluxProcessor} accepting publishers and producing T + * @return a {@link SinkManyAbstractBase} accepting publishers and producing T */ public static Flux switchOnNext(Publisher> mergedPublishers) { return onAssembly(new FluxSwitchMapNoPrefetch<>(from(mergedPublishers), @@ -1935,7 +1935,7 @@ public static Flux switchOnNext(Publisher the produced type * - * @return a {@link FluxProcessor} accepting publishers and producing T + * @return a {@link SinkManyAbstractBase} accepting publishers and producing T * * @deprecated to be removed in 3.6.0 at the earliest. In 3.5.0, you should replace * calls with prefetch=0 with calls to switchOnNext(mergedPublishers), as the default @@ -8568,13 +8568,10 @@ public final Flux subscribeOn(Scheduler scheduler, boolean requestOnSeparateT } /** - * Subscribe the given {@link Subscriber} to this {@link Flux} and return said - * {@link Subscriber} (eg. a {@link FluxProcessor}). - * - *
    -	 * {@code flux.subscribeWith(EmitterProcessor.create()).subscribe() }
    -	 * 
    - * + * Subscribe a provided instance of a subclass of {@link Subscriber} to this {@link Flux} + * and return said instance for further chaining calls. This is similar to {@link #as(Function)}, + * except a subscription is explicitly performed by this method. + *

    * If you need more control over backpressure and the request, use a {@link BaseSubscriber}. * * @param subscriber the {@link Subscriber} to subscribe with and return diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/FluxProcessor.java deleted file mode 100644 index a867e088c2..0000000000 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxProcessor.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -import java.util.Objects; -import java.util.concurrent.CancellationException; -import java.util.stream.Stream; - -import org.reactivestreams.Processor; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; - -import reactor.core.CoreSubscriber; -import reactor.core.Disposable; -import reactor.core.Scannable; -import reactor.util.annotation.Nullable; -import reactor.util.context.Context; - -import static reactor.core.publisher.Sinks.Many; - -/** - * A base processor that exposes {@link Flux} API for {@link Processor}. - * - * Implementors include {@link UnicastProcessor}, {@link EmitterProcessor}, {@link ReplayProcessor}. - * - * @author Stephane Maldini - * - * @param the input value type - * @param the output value type - * @deprecated Processors will be removed in 3.5. Prefer using {@link Sinks.Many} instead, - * or see https://github.com/reactor/reactor-core/issues/2431 for alternatives - */ -@Deprecated -public abstract class FluxProcessor extends Flux - implements Processor, CoreSubscriber, Scannable, Disposable, ContextHolder { - - /** - * Build a {@link FluxProcessor} whose data are emitted by the most recent emitted {@link Publisher}. - * The {@link Flux} will complete once both the publishers source and the last switched to {@link Publisher} have - * completed. - * - *

    - * - * - * @param the produced type - * @return a {@link FluxProcessor} accepting publishers and producing T - * @deprecated should use {@link Sinks}, {@link Many#asFlux()} and {@link Flux#switchOnNext(Publisher)}. To be removed in 3.5.0. - */ - @Deprecated - public static FluxProcessor, T> switchOnNext() { - UnicastProcessor> emitter = UnicastProcessor.create(); - FluxProcessor, T> p = FluxProcessor.wrap(emitter, switchOnNext(emitter)); - return p; - } - /** - * Transform a receiving {@link Subscriber} and a producing {@link Publisher} in a logical {@link FluxProcessor}. - * The link between the passed upstream and returned downstream will not be created automatically, e.g. not - * subscribed together. A {@link Processor} might choose to have orthogonal sequence input and output. - * - * @param the receiving type - * @param the producing type - * - * @param upstream the upstream subscriber - * @param downstream the downstream publisher - * @return a new blackboxed {@link FluxProcessor} - */ - public static FluxProcessor wrap(final Subscriber upstream, final Publisher downstream) { - return new DelegateProcessor<>(downstream, upstream); - } - - @Override - public void dispose() { - onError(new CancellationException("Disposed")); - } - - /** - * Return the number of active {@link Subscriber} or {@literal -1} if untracked. - * - * @return the number of active {@link Subscriber} or {@literal -1} if untracked - */ - public long downstreamCount(){ - return inners().count(); - } - - /** - * Return the processor buffer capacity if any or {@link Integer#MAX_VALUE} - * - * @return processor buffer capacity if any or {@link Integer#MAX_VALUE} - */ - public int getBufferSize() { - return Integer.MAX_VALUE; - } - - /** - * Current error if any, default to null - * - * @return Current error if any, default to null - */ - @Nullable - public Throwable getError() { - return null; - } - - /** - * Return true if any {@link Subscriber} is actively subscribed - * - * @return true if any {@link Subscriber} is actively subscribed - */ - public boolean hasDownstreams() { - return downstreamCount() != 0L; - } - - /** - * Return true if terminated with onComplete - * - * @return true if terminated with onComplete - */ - public final boolean hasCompleted() { - return isTerminated() && getError() == null; - } - - /** - * Return true if terminated with onError - * - * @return true if terminated with onError - */ - public final boolean hasError() { - return isTerminated() && getError() != null; - } - - @Override - public Stream inners() { - return Stream.empty(); - } - - /** - * Has this upstream finished or "completed" / "failed" ? - * - * @return has this upstream finished or "completed" / "failed" ? - */ - public boolean isTerminated() { - return false; - } - - /** - * Return true if this {@link FluxProcessor} supports multithread producing - * - * @return true if this {@link FluxProcessor} supports multithread producing - */ - public boolean isSerialized() { - return false; - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - if (key == Attr.TERMINATED) return isTerminated(); - if (key == Attr.ERROR) return getError(); - if (key == Attr.CAPACITY) return getBufferSize(); - - return null; - } - - @Override - public Context currentContext() { - return Context.empty(); - } - - /** - * Create a {@link FluxProcessor} that safely gates multi-threaded producer - * {@link Subscriber#onNext(Object)}. - * - *

    Discard Support: The resulting processor discards elements received from the source - * {@link Publisher} (if any) when it cancels subscription to said source. - * - * @return a serializing {@link FluxProcessor} - */ - public final FluxProcessor serialize() { - return new DelegateProcessor<>(this, Operators.serialize(this)); - } - - /** - * Create a {@link FluxSink} that safely gates multi-threaded producer - * {@link Subscriber#onNext(Object)}. This processor will be subscribed to - * that {@link FluxSink}, and any previous subscribers will be unsubscribed. - * - *

    The returned {@link FluxSink} will not apply any - * {@link FluxSink.OverflowStrategy} and overflowing {@link FluxSink#next(Object)} - * will behave in two possible ways depending on the Processor: - *

      - *
    • an unbounded processor will handle the overflow itself by dropping or - * buffering
    • - *
    • a bounded processor will block/spin
    • - *
    - * - * @return a serializing {@link FluxSink} - * @deprecated To be removed in 3.5, prefer clear cut usage of {@link Sinks} - * through the {@link Sinks#many()} spec. - */ - @Deprecated - public final FluxSink sink() { - return sink(FluxSink.OverflowStrategy.IGNORE); - } - - /** - * Create a {@link FluxSink} that safely gates multi-threaded producer - * {@link Subscriber#onNext(Object)}. This processor will be subscribed to - * that {@link FluxSink}, and any previous subscribers will be unsubscribed. - * - *

    The returned {@link FluxSink} will not apply any - * {@link FluxSink.OverflowStrategy} and overflowing {@link FluxSink#next(Object)} - * will behave in two possible ways depending on the Processor: - *

      - *
    • an unbounded processor will handle the overflow itself by dropping or - * buffering
    • - *
    • a bounded processor will block/spin on IGNORE strategy, or apply the - * strategy behavior
    • - *
    - * - * @param strategy the overflow strategy, see {@link FluxSink.OverflowStrategy} - * for the - * available strategies - * @return a serializing {@link FluxSink} - * @deprecated To be removed in 3.5, prefer clear cut usage of {@link Sinks} - * through the {@link Sinks#many()} spec. - */ - @Deprecated - public final FluxSink sink(FluxSink.OverflowStrategy strategy) { - Objects.requireNonNull(strategy, "strategy"); - if (getBufferSize() == Integer.MAX_VALUE){ - strategy = FluxSink.OverflowStrategy.IGNORE; - } - - FluxCreate.BaseSink s = FluxCreate.createSink(this, strategy); - onSubscribe(s); - - if(s.isCancelled() || - (isSerialized() && getBufferSize() == Integer.MAX_VALUE)){ - return s; - } - if (serializeAlways()) - return new FluxCreate.SerializedFluxSink<>(s); - else - return new FluxCreate.SerializeOnRequestSink<>(s); - } - - /** - * Returns serialization strategy. If true, {@link FluxProcessor#sink()} will always - * be serialized. Otherwise sink is serialized only if {@link FluxSink#onRequest(java.util.function.LongConsumer)} - * is invoked. - * @return true to serialize any sink, false to delay serialization till onRequest - */ - protected boolean serializeAlways() { - return true; - } - - /** - * Return true if {@code FluxProcessor} - * - * @return true if {@code FluxProcessor} - */ - protected boolean isIdentityProcessor() { - return false; - } -} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index cdbd071348..b694a1352c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -4379,7 +4379,7 @@ public final Mono subscribeOn(Scheduler scheduler) { /** * Subscribe the given {@link Subscriber} to this {@link Mono} and return said - * {@link Subscriber} (eg. a {@link MonoProcessor}). + * {@link Subscriber}, allowing subclasses with a richer API to be used fluently. * * @param subscriber the {@link Subscriber} to subscribe with * @param the reified type of the {@link Subscriber} for chaining @@ -4884,26 +4884,6 @@ public final CompletableFuture toFuture() { return subscribeWith(new MonoToCompletableFuture<>(false)); } - /** - * Wrap this {@link Mono} into a {@link MonoProcessor} (turning it hot and allowing to block, - * cancel, as well as many other operations). Note that the {@link MonoProcessor} - * is subscribed to its parent source if any. - * - * @return a {@link MonoProcessor} to use to either retrieve value or cancel the underlying {@link Subscription} - * @deprecated prefer {@link #share()} to share a parent subscription, or use {@link Sinks} - */ - @Deprecated - public final MonoProcessor toProcessor() { - if (this instanceof MonoProcessor) { - return (MonoProcessor) this; - } - else { - NextProcessor result = new NextProcessor<>(this); - result.connect(); - return result; - } - } - /** * Transform this {@link Mono} in order to generate a target {@link Mono}. Unlike {@link #transformDeferred(Function)}, the * provided function is executed as part of assembly. diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/MonoProcessor.java deleted file mode 100644 index bfbb903221..0000000000 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoProcessor.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -import java.time.Duration; -import java.util.concurrent.CancellationException; -import java.util.stream.Stream; - -import org.reactivestreams.Processor; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; - -import reactor.core.CoreSubscriber; -import reactor.core.Disposable; -import reactor.core.Scannable; -import reactor.util.annotation.Nullable; -import reactor.util.context.Context; - -/** - * A {@code MonoProcessor} is a {@link Processor} that is also a {@link Mono}. - * - *

    - * - * - *

    - * Implementations might implements stateful semantics, allowing multiple subscriptions. - * Once a {@link MonoProcessor} has been resolved, implementations may also replay cached signals to newer subscribers. - *

    - * Despite having default implementations, most methods should be reimplemented with meaningful semantics relevant to - * concrete child classes. - * - * @param the type of the value that will be made available - * - * @author Stephane Maldini - * @deprecated Processors will be removed in 3.5. Prefer using {@link Sinks.One} or {@link Sinks.Empty} instead, - * or see https://github.com/reactor/reactor-core/issues/2431 for alternatives - */ -@Deprecated -public abstract class MonoProcessor extends Mono - implements Processor, CoreSubscriber, Disposable, - Subscription, - Scannable { - - /** - * Create a {@link MonoProcessor} that will eagerly request 1 on {@link #onSubscribe(Subscription)}, cache and emit - * the eventual result for 1 or N subscribers. - * - * @param type of the expected value - * - * @return A {@link MonoProcessor}. - * @deprecated Use {@link Sinks#one()}, to be removed in 3.5 - */ - @Deprecated - public static MonoProcessor create() { - return new NextProcessor<>(null); - } - - /** - * @deprecated the {@link MonoProcessor} will cease to implement {@link Subscription} in 3.5 - */ - @Override - @Deprecated - public void cancel() { - } - - /** - * Indicates whether this {@code MonoProcessor} has been interrupted via cancellation. - * - * @return {@code true} if this {@code MonoProcessor} is cancelled, {@code false} - * otherwise. - * @deprecated the {@link MonoProcessor} will cease to implement {@link Subscription} and this method will be removed in 3.5 - */ - @Deprecated - public boolean isCancelled() { - return false; - } - - /** - * @param n the request amount - * @deprecated the {@link MonoProcessor} will cease to implement {@link Subscription} in 3.5 - */ - @Override - @Deprecated - public void request(long n) { - Operators.validate(n); - } - - @Override - public void dispose() { - onError(new CancellationException("Disposed")); - } - - /** - * Block the calling thread indefinitely, waiting for the completion of this {@code MonoProcessor}. If the - * {@link MonoProcessor} is completed with an error a RuntimeException that wraps the error is thrown. - * - * @return the value of this {@code MonoProcessor} - */ - @Override - @Nullable - public O block() { - return block(null); - } - - /** - * Block the calling thread for the specified time, waiting for the completion of this {@code MonoProcessor}. If the - * {@link MonoProcessor} is completed with an error a RuntimeException that wraps the error is thrown. - * - * @param timeout the timeout value as a {@link Duration} - * - * @return the value of this {@code MonoProcessor} or {@code null} if the timeout is reached and the {@code MonoProcessor} has - * not completed - */ - @Override - @Nullable - public O block(@Nullable Duration timeout) { - return peek(); - } - - /** - * Return the produced {@link Throwable} error if any or null - * - * @return the produced {@link Throwable} error if any or null - */ - @Nullable - public Throwable getError() { - return null; - } - - /** - * Indicates whether this {@code MonoProcessor} has been completed with an error. - * - * @return {@code true} if this {@code MonoProcessor} was completed with an error, {@code false} otherwise. - */ - public final boolean isError() { - return getError() != null; - } - - /** - * Indicates whether this {@code MonoProcessor} has been successfully completed a value. - * - * @return {@code true} if this {@code MonoProcessor} is successful, {@code false} otherwise. - */ - public final boolean isSuccess() { - return isTerminated() && !isError(); - } - - /** - * Indicates whether this {@code MonoProcessor} has been terminated by the - * source producer with a success or an error. - * - * @return {@code true} if this {@code MonoProcessor} is successful, {@code false} otherwise. - */ - public boolean isTerminated() { - return false; - } - - @Override - public boolean isDisposed() { - return isTerminated() || isCancelled(); - } - - /** - * Returns the value that completed this {@link MonoProcessor}. Returns {@code null} if the {@link MonoProcessor} has not been completed. If the - * {@link MonoProcessor} is completed with an error a RuntimeException that wraps the error is thrown. - * - * @return the value that completed the {@link MonoProcessor}, or {@code null} if it has not been completed - * - * @throws RuntimeException if the {@link MonoProcessor} was completed with an error - * @deprecated this method is discouraged, consider peeking into a MonoProcessor by {@link Mono#toFuture() turning it into a CompletableFuture} - */ - @Nullable - @Deprecated - public O peek() { - return null; - } - - @Override - public Context currentContext() { - InnerProducer[] innerProducersArray = - inners().filter(InnerProducer.class::isInstance) - .map(InnerProducer.class::cast) - .toArray(InnerProducer[]::new); - - return Operators.multiSubscribersContext(innerProducersArray); - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - //touch guard - boolean t = isTerminated(); - - if (key == Attr.TERMINATED) return t; - if (key == Attr.ERROR) return getError(); - if (key == Attr.PREFETCH) return Integer.MAX_VALUE; - if (key == Attr.CANCELLED) return isCancelled(); - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - - return null; - } - - /** - * Return the number of active {@link Subscriber} or {@literal -1} if untracked. - * - * @return the number of active {@link Subscriber} or {@literal -1} if untracked - */ - public long downstreamCount() { - return inners().count(); - } - - /** - * Return true if any {@link Subscriber} is actively subscribed - * - * @return true if any {@link Subscriber} is actively subscribed - */ - public final boolean hasDownstreams() { - return downstreamCount() != 0; - } - - @Override - public Stream inners() { - return Stream.empty(); - } -} diff --git a/reactor-core/src/main/java/reactor/core/publisher/NextProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/NextProcessor.java index 174f9e5c0f..58717d6a79 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/NextProcessor.java +++ b/reactor-core/src/main/java/reactor/core/publisher/NextProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.util.stream.Stream; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CorePublisher; @@ -35,9 +36,7 @@ import reactor.util.annotation.Nullable; import reactor.util.context.Context; -// NextProcessor extends a deprecated class but is itself not deprecated and is here to stay, hence the following line is ok. -@SuppressWarnings("deprecation") -class NextProcessor extends MonoProcessor { +class NextProcessor extends Mono implements CoreSubscriber, reactor.core.Disposable, Scannable { /** * This boolean indicates a usage as `Mono#share()` where, for alignment with Flux#share(), the removal of all @@ -47,6 +46,41 @@ class NextProcessor extends MonoProcessor { volatile NextInner[] subscribers; + /** + * Block the calling thread indefinitely, waiting for the completion of this {@link NextProcessor}. If the + * {@link NextProcessor} is completed with an error a RuntimeException that wraps the error is thrown. + * + * @return the value of this {@link NextProcessor} + */ + @Override + @Nullable + public O block() { + return block(null); + } + + @Override + public boolean isDisposed() { + return isTerminated(); + } + + /** + * Indicates whether this {@link NextProcessor} has been completed with an error. + * + * @return {@code true} if this {@link NextProcessor} was completed with an error, {@code false} otherwise. + */ + public final boolean isError() { + return getError() != null; + } + + /** + * Indicates whether this {@link NextProcessor} has been successfully completed a value. + * + * @return {@code true} if this {@link NextProcessor} is successful, {@code false} otherwise. + */ + public final boolean isSuccess() { + return isTerminated() && !isError(); + } + @SuppressWarnings("rawtypes") static final AtomicReferenceFieldUpdater SUBSCRIBERS = AtomicReferenceFieldUpdater.newUpdater(NextProcessor.class, NextInner[].class, "subscribers"); @@ -82,8 +116,18 @@ class NextProcessor extends MonoProcessor { SUBSCRIBERS.lazySet(this, source != null ? EMPTY_WITH_SOURCE : EMPTY); } - @Override - public O peek() { + /** + * For testing purpose. + *

    + * Returns the value that completed this {@link NextProcessor}. Returns {@code null} if the {@link NextProcessor} has not been completed. If the + * {@link NextProcessor} is completed with an error a RuntimeException that wraps the error is thrown. + * + * @return the value that completed the {@link NextProcessor}, or {@code null} if it has not been completed + * + * @throws RuntimeException if the {@link NextProcessor} was completed with an error + */ + @Nullable + O peek() { if (!isTerminated()) { return null; } @@ -101,6 +145,15 @@ public O peek() { return null; } + /** + * Block the calling thread for the specified time, waiting for the completion of this {@link NextProcessor}. If the + * {@link NextProcessor} is completed with an error a RuntimeException that wraps the error is thrown. + * + * @param timeout the timeout value as a {@link Duration} + * + * @return the value of this {@link NextProcessor} or {@code null} if the timeout is reached and the {@link NextProcessor} has + * not completed + */ @Override @Nullable public O block(@Nullable Duration timeout) { @@ -128,7 +181,7 @@ public O block(@Nullable Duration timeout) { return value; } if (timeout != null && delay < System.nanoTime()) { - cancel(); + doCancel(); throw new IllegalStateException("Timeout on Mono blocking read"); } @@ -304,11 +357,21 @@ EmitResult tryEmitValue(@Nullable O value) { return EmitResult.OK; } + @Nullable @Override public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return subscription; - return super.scanUnsafe(key); + //touch guard + boolean t = isTerminated(); + + if (key == Attr.TERMINATED) return t; + if (key == Attr.CANCELLED) return !t && subscription == Operators.cancelledSubscription(); + if (key == Attr.ERROR) return getError(); + if (key == Attr.PREFETCH) return Integer.MAX_VALUE; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return null; } @Override @@ -316,7 +379,11 @@ public Context currentContext() { return Operators.multiSubscribersContext(subscribers); } - @Override + /** + * Return the number of active {@link Subscriber} or {@literal -1} if untracked. + * + * @return the number of active {@link Subscriber} or {@literal -1} if untracked + */ public long downstreamCount() { return subscribers.length; } @@ -347,10 +414,7 @@ public void dispose() { } } - @Override - // This method is inherited from a deprecated class and will be removed in 3.5. - @SuppressWarnings("deprecation") - public void cancel() { + void doCancel() { //TODO compare with the cancellation in remove(), do we need both approaches? if (isTerminated()) { return; } @@ -373,20 +437,22 @@ public final void onSubscribe(Subscription subscription) { } } - @Override - // This method is inherited from a deprecated class and will be removed in 3.5. - @SuppressWarnings("deprecation") - public boolean isCancelled() { - return subscription == Operators.cancelledSubscription() && !isTerminated(); - } - - @Override + /** + * Indicates whether this {@link NextProcessor} has been terminated by the + * source producer with a success or an error. + * + * @return {@code true} if this {@link NextProcessor} is successful, {@code false} otherwise. + */ public boolean isTerminated() { return subscribers == TERMINATED; } + /** + * Return the produced {@link Throwable} error if any or null + * + * @return the produced {@link Throwable} error if any or null + */ @Nullable - @Override public Throwable getError() { return error; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java index cf45c905df..208c2ae172 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,8 +33,7 @@ * @author Simon Baslé */ final class SinkManyBestEffort extends Flux - implements InternalManySink, Scannable, - DirectInnerContainer { + implements InternalManySink, Scannable { static final DirectInner[] EMPTY = new DirectInner[0]; static final DirectInner[] TERMINATED = new DirectInner[0]; @@ -215,8 +214,14 @@ public void subscribe(CoreSubscriber actual) { } } - @Override - public boolean add(DirectInner s) { + /** + * Add a new {@link SinkManyBestEffort.DirectInner} to this publisher. + * + * @param s the new {@link SinkManyBestEffort.DirectInner} to add + * + * @return {@code true} if the inner could be added, {@code false} if the publisher cannot accept new subscribers + */ + boolean add(DirectInner s) { DirectInner[] a = subscribers; if (a == TERMINATED) { return false; @@ -240,9 +245,14 @@ public boolean add(DirectInner s) { } } + /** + * Remove an {@link SinkManyBestEffort.DirectInner} from this publisher. Does nothing if the inner is not currently managed + * by the publisher. + * + * @param s the {@link SinkManyBestEffort.DirectInner} to remove + */ @SuppressWarnings("unchecked") - @Override - public void remove(DirectInner s) { + void remove(DirectInner s) { DirectInner[] a = subscribers; if (a == TERMINATED || a == EMPTY) { return; @@ -285,14 +295,14 @@ public void remove(DirectInner s) { static class DirectInner extends AtomicBoolean implements InnerProducer { final CoreSubscriber actual; - final DirectInnerContainer parent; + final SinkManyBestEffort parent; volatile long requested; @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater( DirectInner.class, "requested"); - DirectInner(CoreSubscriber actual, DirectInnerContainer parent) { + DirectInner(CoreSubscriber actual, SinkManyBestEffort parent) { this.actual = actual; this.parent = parent; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/EmitterProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyEmitterProcessor.java similarity index 71% rename from reactor-core/src/main/java/reactor/core/publisher/EmitterProcessor.java rename to reactor-core/src/main/java/reactor/core/publisher/SinkManyEmitterProcessor.java index 73644a34c0..ad815c50a4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/EmitterProcessor.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyEmitterProcessor.java @@ -29,7 +29,6 @@ import reactor.core.CoreSubscriber; import reactor.core.Disposable; -import reactor.core.Disposables; import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; @@ -41,7 +40,7 @@ import static reactor.core.publisher.FluxPublish.PublishSubscriber.TERMINATED; /** - * An implementation of a message-passing Processor implementing + * An implementation of a {@link Sinks.ManyWithUpstream} implementing * publish-subscribe with synchronous (thread-stealing and happen-before interactions) * drain loops. *

    @@ -56,119 +55,44 @@ * @param the input and output value type * * @author Stephane Maldini - * @deprecated To be removed in 3.5. Prefer clear cut usage of {@link Sinks} through - * variations of {@link Sinks.MulticastSpec#onBackpressureBuffer() Sinks.many().multicast().onBackpressureBuffer()}. - * If you really need the subscribe-to-upstream functionality of a {@link org.reactivestreams.Processor}, switch - * to {@link Sinks.ManyWithUpstream} with {@link Sinks#unsafe()} variants of - * {@link Sinks.MulticastUnsafeSpec#onBackpressureBuffer() Sinks.unsafe().many().multicast().onBackpressureBuffer()}. - *

    This processor was blocking in {@link EmitterProcessor#onNext(Object)}. This behaviour can be implemented with the {@link Sinks} API by calling - * {@link Sinks.Many#tryEmitNext(Object)} and retrying, e.g.: - *

    {@code while (sink.tryEmitNext(v).hasFailed()) {
    - *     LockSupport.parkNanos(10);
    - * }
    - * }
    */ -@Deprecated -public final class EmitterProcessor extends FluxProcessor implements InternalManySink, - Sinks.ManyWithUpstream { +final class SinkManyEmitterProcessor extends Flux implements InternalManySink, + Sinks.ManyWithUpstream, CoreSubscriber, Scannable, Disposable, ContextHolder { @SuppressWarnings("rawtypes") static final FluxPublish.PubSubInner[] EMPTY = new FluxPublish.PublishInner[0]; - /** - * Create a new {@link EmitterProcessor} using {@link Queues#SMALL_BUFFER_SIZE} - * backlog size and auto-cancel. - * - * @param Type of processed signals - * - * @return a fresh processor - * @deprecated use {@link Sinks.MulticastSpec#onBackpressureBuffer() Sinks.many().multicast().onBackpressureBuffer()} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. - */ - @Deprecated - public static EmitterProcessor create() { - return create(Queues.SMALL_BUFFER_SIZE, true); - } - - /** - * Create a new {@link EmitterProcessor} using {@link Queues#SMALL_BUFFER_SIZE} - * backlog size and the provided auto-cancel. - * - * @param Type of processed signals - * @param autoCancel automatically cancel - * - * @return a fresh processor - * @deprecated use {@link Sinks.MulticastSpec#onBackpressureBuffer(int, boolean) Sinks.many().multicast().onBackpressureBuffer(bufferSize, boolean)} - * using the old default of {@link Queues#SMALL_BUFFER_SIZE} for the {@code bufferSize} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. - */ - @Deprecated - public static EmitterProcessor create(boolean autoCancel) { - return create(Queues.SMALL_BUFFER_SIZE, autoCancel); - } - - /** - * Create a new {@link EmitterProcessor} using the provided backlog size, with auto-cancel. - * - * @param Type of processed signals - * @param bufferSize the internal buffer size to hold signals - * - * @return a fresh processor - * @deprecated use {@link Sinks.MulticastSpec#onBackpressureBuffer(int) Sinks.many().multicast().onBackpressureBuffer(bufferSize)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. - */ - @Deprecated - public static EmitterProcessor create(int bufferSize) { - return create(bufferSize, true); - } - - /** - * Create a new {@link EmitterProcessor} using the provided backlog size and auto-cancellation. - * - * @param Type of processed signals - * @param bufferSize the internal buffer size to hold signals - * @param autoCancel automatically cancel - * - * @return a fresh processor - * @deprecated use {@link Sinks.MulticastSpec#onBackpressureBuffer(int, boolean) Sinks.many().multicast().onBackpressureBuffer(bufferSize, autoCancel)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. - */ - @Deprecated - public static EmitterProcessor create(int bufferSize, boolean autoCancel) { - return new EmitterProcessor<>(autoCancel, bufferSize); - } - final int prefetch; final boolean autoCancel; - volatile Subscription s; + volatile Subscription s; @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater S = - AtomicReferenceFieldUpdater.newUpdater(EmitterProcessor.class, + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater(SinkManyEmitterProcessor.class, Subscription.class, "s"); volatile FluxPublish.PubSubInner[] subscribers; @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater - SUBSCRIBERS = AtomicReferenceFieldUpdater.newUpdater(EmitterProcessor.class, + static final AtomicReferenceFieldUpdater + SUBSCRIBERS = AtomicReferenceFieldUpdater.newUpdater(SinkManyEmitterProcessor.class, FluxPublish.PubSubInner[].class, "subscribers"); volatile EmitterDisposable upstreamDisposable; @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater UPSTREAM_DISPOSABLE = - AtomicReferenceFieldUpdater.newUpdater(EmitterProcessor.class, EmitterDisposable.class, "upstreamDisposable"); + static final AtomicReferenceFieldUpdater UPSTREAM_DISPOSABLE = + AtomicReferenceFieldUpdater.newUpdater(SinkManyEmitterProcessor.class, EmitterDisposable.class, "upstreamDisposable"); @SuppressWarnings("unused") volatile int wip; @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP = - AtomicIntegerFieldUpdater.newUpdater(EmitterProcessor.class, "wip"); + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(SinkManyEmitterProcessor.class, "wip"); volatile Queue queue; @@ -179,12 +103,12 @@ public static EmitterProcessor create(int bufferSize, boolean autoCancel) volatile Throwable error; @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater ERROR = - AtomicReferenceFieldUpdater.newUpdater(EmitterProcessor.class, + static final AtomicReferenceFieldUpdater ERROR = + AtomicReferenceFieldUpdater.newUpdater(SinkManyEmitterProcessor.class, Throwable.class, "error"); - EmitterProcessor(boolean autoCancel, int prefetch) { + SinkManyEmitterProcessor(boolean autoCancel, int prefetch) { if (prefetch < 1) { throw new IllegalArgumentException("bufferSize must be strictly positive, " + "was: " + prefetch); } @@ -214,7 +138,7 @@ private boolean detach() { if (Operators.terminate(S, this)) { done = true; CancellationException detachException = new CancellationException("the ManyWithUpstream sink had a Subscription to an upstream which has been manually cancelled"); - if (ERROR.compareAndSet(EmitterProcessor.this, null, detachException)) { + if (ERROR.compareAndSet(this, null, detachException)) { Queue q = queue; if (q != null) { q.clear(); @@ -288,7 +212,7 @@ public void onError(Throwable throwable) { @Override public EmitResult tryEmitError(Throwable t) { - Objects.requireNonNull(t, "onError"); + Objects.requireNonNull(t, "tryEmitError must be invoked with a non-null Throwable"); if (done) { return EmitResult.FAIL_TERMINATED; } @@ -317,7 +241,7 @@ public EmitResult tryEmitNext(T t) { return Sinks.EmitResult.FAIL_TERMINATED; } - Objects.requireNonNull(t, "onNext"); + Objects.requireNonNull(t, "tryEmitNext must be invoked with a non-null value"); Queue q = queue; @@ -356,21 +280,22 @@ public Flux asFlux() { return this; } - @Override - protected boolean isIdentityProcessor() { - return true; - } - /** * Return the number of parked elements in the emitter backlog. * * @return the number of parked elements in the emitter backlog. */ - public int getPending() { + int getPending() { Queue q = queue; return q != null ? q.size() : 0; } + //TODO evaluate the use case for Disposable in the context of Sinks + @Override + public void dispose() { + onError(new CancellationException("Disposed")); + } + @Override public boolean isDisposed() { return isTerminated() || isCancelled(); @@ -406,25 +331,28 @@ else if (m == Fuseable.ASYNC) { } } - @Override + /** + * Current error if any, default to null + * + * @return Current error if any, default to null + */ @Nullable - public Throwable getError() { + Throwable getError() { return error; } /** * @return true if all subscribers have actually been cancelled and the processor auto shut down */ - public boolean isCancelled() { + boolean isCancelled() { return Operators.cancelledSubscription() == s; } - @Override - final public int getBufferSize() { - return prefetch; - } - - @Override + /** + * Has this upstream finished or "completed" / "failed" ? + * + * @return has this upstream finished or "completed" / "failed" ? + */ public boolean isTerminated() { return done && getPending() == 0; } @@ -442,7 +370,11 @@ public Object scanUnsafe(Attr key) { if (key == Attr.CANCELLED) return isCancelled(); if (key == Attr.PREFETCH) return getPrefetch(); - return super.scanUnsafe(key); + if (key == Attr.TERMINATED) return isTerminated(); + if (key == Attr.ERROR) return getError(); + if (key == Attr.CAPACITY) return getPrefetch(); + + return null; } final void drain() { @@ -669,16 +601,11 @@ final void remove(FluxPublish.PubSubInner inner) { } } - @Override - public long downstreamCount() { - return subscribers.length; - } - static final class EmitterInner extends FluxPublish.PubSubInner { - final EmitterProcessor parent; + final SinkManyEmitterProcessor parent; - EmitterInner(CoreSubscriber actual, EmitterProcessor parent) { + EmitterInner(CoreSubscriber actual, SinkManyEmitterProcessor parent) { super(actual); this.parent = parent; } @@ -698,9 +625,9 @@ void removeAndDrainParent() { static final class EmitterDisposable implements Disposable { @Nullable - EmitterProcessor target; + SinkManyEmitterProcessor target; - public EmitterDisposable(EmitterProcessor emitterProcessor) { + public EmitterDisposable(SinkManyEmitterProcessor emitterProcessor) { this.target = emitterProcessor; } @@ -711,7 +638,7 @@ public boolean isDisposed() { @Override public void dispose() { - EmitterProcessor t = target; + SinkManyEmitterProcessor t = target; if (t == null) { return; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ReplayProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyReplayProcessor.java similarity index 69% rename from reactor-core/src/main/java/reactor/core/publisher/ReplayProcessor.java rename to reactor-core/src/main/java/reactor/core/publisher/SinkManyReplayProcessor.java index aaed24f5c8..9493e79738 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ReplayProcessor.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyReplayProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.Objects; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -27,6 +28,7 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; +import reactor.core.Disposable; import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.core.publisher.Sinks.EmitResult; @@ -47,16 +49,12 @@ *

    * * @param the value type - * @deprecated To be removed in 3.5, prefer clear cut usage of {@link Sinks} through - * variations under {@link reactor.core.publisher.Sinks.MulticastReplaySpec Sinks.many().replay()}. */ -@Deprecated -public final class ReplayProcessor extends FluxProcessor - implements Fuseable, InternalManySink { +final class SinkManyReplayProcessor extends Flux implements InternalManySink, CoreSubscriber, ContextHolder, Disposable, Fuseable, Scannable { /** - * Create a {@link ReplayProcessor} that caches the last element it has pushed, - * replaying it to late subscribers. This is a buffer-based ReplayProcessor with + * Create a {@link SinkManyReplayProcessor} that caches the last element it has pushed, + * replaying it to late subscribers. This is a buffer-based SinkManyReplayProcessor with * a history size of 1. *

    * extends FluxProcessor * * @param the type of the pushed elements * - * @return a new {@link ReplayProcessor} that replays its last pushed element to each new + * @return a new {@link SinkManyReplayProcessor} that replays its last pushed element to each new * {@link Subscriber} - * @deprecated use {@link Sinks.MulticastReplaySpec#latest() Sinks.many().replay().latest()} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. */ - @Deprecated - public static ReplayProcessor cacheLast() { + static SinkManyReplayProcessor cacheLast() { return cacheLastOrDefault(null); } /** - * Create a {@link ReplayProcessor} that caches the last element it has pushed, + * Create a {@link SinkManyReplayProcessor} that caches the last element it has pushed, * replaying it to late subscribers. If a {@link Subscriber} comes in before * any value has been pushed, then the {@code defaultValue} is emitted instead. - * This is a buffer-based ReplayProcessor with a history size of 1. + * This is a buffer-based SinkManyReplayProcessor with a history size of 1. *

    * @@ -87,14 +82,11 @@ public static ReplayProcessor cacheLast() { * cached yet. * @param the type of the pushed elements * - * @return a new {@link ReplayProcessor} that replays its last pushed element to each new + * @return a new {@link SinkManyReplayProcessor} that replays its last pushed element to each new * {@link Subscriber}, or a default one if nothing was pushed yet - * @deprecated use {@link Sinks.MulticastReplaySpec#latestOrDefault(Object) Sinks.many().replay().latestOrDefault(value)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. */ - @Deprecated - public static ReplayProcessor cacheLastOrDefault(@Nullable T value) { - ReplayProcessor b = create(1); + static SinkManyReplayProcessor cacheLastOrDefault(@Nullable T value) { + SinkManyReplayProcessor b = create(1); if (value != null) { b.onNext(value); } @@ -102,54 +94,44 @@ public static ReplayProcessor cacheLastOrDefault(@Nullable T value) { } /** - * Create a new {@link ReplayProcessor} that replays an unbounded number of elements, + * Create a new {@link SinkManyReplayProcessor} that replays an unbounded number of elements, * using a default internal {@link Queues#SMALL_BUFFER_SIZE Queue}. * * @param the type of the pushed elements * - * @return a new {@link ReplayProcessor} that replays the whole history to each new + * @return a new {@link SinkManyReplayProcessor} that replays the whole history to each new * {@link Subscriber}. - * @deprecated use {@link Sinks.MulticastReplaySpec#all() Sinks.many().replay().all()} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. */ - @Deprecated - public static ReplayProcessor create() { + static SinkManyReplayProcessor create() { return create(Queues.SMALL_BUFFER_SIZE, true); } /** - * Create a new {@link ReplayProcessor} that replays up to {@code historySize} + * Create a new {@link SinkManyReplayProcessor} that replays up to {@code historySize} * elements. * * @param historySize the backlog size, ie. maximum items retained for replay. * @param the type of the pushed elements * - * @return a new {@link ReplayProcessor} that replays a limited history to each new + * @return a new {@link SinkManyReplayProcessor} that replays a limited history to each new * {@link Subscriber}. - * @deprecated use {@link Sinks.MulticastReplaySpec#limit(int) Sinks.many().replay().limit(historySize)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. */ - @Deprecated - public static ReplayProcessor create(int historySize) { + static SinkManyReplayProcessor create(int historySize) { return create(historySize, false); } /** - * Create a new {@link ReplayProcessor} that either replay all the elements or a + * Create a new {@link SinkManyReplayProcessor} that either replay all the elements or a * limited amount of elements depending on the {@code unbounded} parameter. * * @param historySize maximum items retained if bounded, or initial link size if unbounded * @param unbounded true if "unlimited" data store must be supplied * @param the type of the pushed elements * - * @return a new {@link ReplayProcessor} that replays the whole history to each new + * @return a new {@link SinkManyReplayProcessor} that replays the whole history to each new * {@link Subscriber} if configured as unbounded, a limited history otherwise. - * @deprecated use {@link Sinks.MulticastReplaySpec#limit(int) Sinks.many().replay().limit(historySize)} - * for bounded cases ({@code unbounded == false}) or {@link Sinks.MulticastReplaySpec#all(int) Sinks.many().replay().all(bufferSize)} - * otherwise (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. */ - @Deprecated - public static ReplayProcessor create(int historySize, boolean unbounded) { + static SinkManyReplayProcessor create(int historySize, boolean unbounded) { FluxReplay.ReplayBuffer buffer; if (unbounded) { buffer = new FluxReplay.UnboundedReplayBuffer<>(historySize); @@ -157,13 +139,13 @@ public static ReplayProcessor create(int historySize, boolean unbounded) else { buffer = new FluxReplay.SizeBoundReplayBuffer<>(historySize); } - return new ReplayProcessor<>(buffer); + return new SinkManyReplayProcessor<>(buffer); } /** * Creates a time-bounded replay processor. *

    - * In this setting, the {@code ReplayProcessor} internally tags each observed item + * In this setting, the {@code SinkManyReplayProcessor} internally tags each observed item * with a timestamp value supplied by the {@link Schedulers#parallel()} and keeps only * those whose age is less than the supplied time value converted to milliseconds. For * example, an item arrives at T=0 and the max age is set to 5; at T>=5 this first @@ -173,7 +155,7 @@ public static ReplayProcessor create(int historySize, boolean unbounded) * Once the processor is terminated, subscribers subscribing to it will receive items * that remained in the buffer after the terminal signal, regardless of their age. *

    - * If an subscriber subscribes while the {@code ReplayProcessor} is active, it will + * If an subscriber subscribes while the {@code SinkManyReplayProcessor} is active, it will * observe only those items from within the buffer that have an age less than the * specified time, and each item observed thereafter, even if the buffer evicts items * due to the time constraint in the mean time. In other words, once an subscriber @@ -181,22 +163,19 @@ public static ReplayProcessor create(int historySize, boolean unbounded) * items at the beginning of the sequence. *

    * - * @param the type of items observed and emitted by the Processor + * @param the type of items observed and emitted by the sink * @param maxAge the maximum age of the contained items * - * @return a new {@link ReplayProcessor} that replays elements based on their age. - * @deprecated use {@link Sinks.MulticastReplaySpec#limit(Duration) Sinks.many().replay().limit(maxAge)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + * @return a new {@link SinkManyReplayProcessor} that replays elements based on their age. */ - @Deprecated - public static ReplayProcessor createTimeout(Duration maxAge) { + static SinkManyReplayProcessor createTimeout(Duration maxAge) { return createTimeout(maxAge, Schedulers.parallel()); } /** * Creates a time-bounded replay processor. *

    - * In this setting, the {@code ReplayProcessor} internally tags each observed item + * In this setting, the {@code SinkManyReplayProcessor} internally tags each observed item * with a timestamp value supplied by the {@link Scheduler} and keeps only * those whose age is less than the supplied time value converted to milliseconds. For * example, an item arrives at T=0 and the max age is set to 5; at T>=5 this first @@ -206,7 +185,7 @@ public static ReplayProcessor createTimeout(Duration maxAge) { * Once the processor is terminated, subscribers subscribing to it will receive items * that remained in the buffer after the terminal signal, regardless of their age. *

    - * If an subscriber subscribes while the {@code ReplayProcessor} is active, it will + * If an subscriber subscribes while the {@code SinkManyReplayProcessor} is active, it will * observe only those items from within the buffer that have an age less than the * specified time, and each item observed thereafter, even if the buffer evicts items * due to the time constraint in the mean time. In other words, once an subscriber @@ -214,33 +193,30 @@ public static ReplayProcessor createTimeout(Duration maxAge) { * items at the beginning of the sequence. *

    * - * @param the type of items observed and emitted by the Processor + * @param the type of items observed and emitted by the sink * @param maxAge the maximum age of the contained items * - * @return a new {@link ReplayProcessor} that replays elements based on their age. - * @deprecated use {@link Sinks.MulticastReplaySpec#limit(Duration, Scheduler) Sinks.many().replay().limit(maxAge, scheduler)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + * @return a new {@link SinkManyReplayProcessor} that replays elements based on their age. */ - @Deprecated - public static ReplayProcessor createTimeout(Duration maxAge, Scheduler scheduler) { + static SinkManyReplayProcessor createTimeout(Duration maxAge, Scheduler scheduler) { return createSizeAndTimeout(Integer.MAX_VALUE, maxAge, scheduler); } /** * Creates a time- and size-bounded replay processor. *

    - * In this setting, the {@code ReplayProcessor} internally tags each received item + * In this setting, the {@code SinkManyReplayProcessor} internally tags each received item * with a timestamp value supplied by the {@link Schedulers#parallel()} and holds at * most * {@code size} items in its internal buffer. It evicts items from the start of the * buffer if their age becomes less-than or equal to the supplied age in milliseconds * or the buffer reaches its {@code size} limit. *

    - * When subscribers subscribe to a terminated {@code ReplayProcessor}, they observe + * When subscribers subscribe to a terminated {@code SinkManyReplayProcessor}, they observe * the items that remained in the buffer after the terminal signal, regardless of * their age, but at most {@code size} items. *

    - * If an subscriber subscribes while the {@code ReplayProcessor} is active, it will + * If an subscriber subscribes while the {@code SinkManyReplayProcessor} is active, it will * observe only those items from within the buffer that have age less than the * specified time and each subsequent item, even if the buffer evicts items due to the * time constraint in the mean time. In other words, once an subscriber subscribes, it @@ -248,34 +224,31 @@ public static ReplayProcessor createTimeout(Duration maxAge, Scheduler sc * beginning of the sequence. *

    * - * @param the type of items observed and emitted by the Processor + * @param the type of items observed and emitted by the sink * @param maxAge the maximum age of the contained items * @param size the maximum number of buffered items * - * @return a new {@link ReplayProcessor} that replay up to {@code size} elements, but + * @return a new {@link SinkManyReplayProcessor} that replay up to {@code size} elements, but * will evict them from its history based on their age. - * @deprecated use {@link Sinks.MulticastReplaySpec#limit(int, Duration) Sinks.many().replay().limit(size, maxAge)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. */ - @Deprecated - public static ReplayProcessor createSizeAndTimeout(int size, Duration maxAge) { + static SinkManyReplayProcessor createSizeAndTimeout(int size, Duration maxAge) { return createSizeAndTimeout(size, maxAge, Schedulers.parallel()); } /** * Creates a time- and size-bounded replay processor. *

    - * In this setting, the {@code ReplayProcessor} internally tags each received item + * In this setting, the {@code SinkManyReplayProcessor} internally tags each received item * with a timestamp value supplied by the {@link Scheduler} and holds at most * {@code size} items in its internal buffer. It evicts items from the start of the * buffer if their age becomes less-than or equal to the supplied age in milliseconds * or the buffer reaches its {@code size} limit. *

    - * When subscribers subscribe to a terminated {@code ReplayProcessor}, they observe + * When subscribers subscribe to a terminated {@code SinkManyReplayProcessor}, they observe * the items that remained in the buffer after the terminal signal, regardless of * their age, but at most {@code size} items. *

    - * If an subscriber subscribes while the {@code ReplayProcessor} is active, it will + * If an subscriber subscribes while the {@code SinkManyReplayProcessor} is active, it will * observe only those items from within the buffer that have age less than the * specified time and each subsequent item, even if the buffer evicts items due to the * time constraint in the mean time. In other words, once an subscriber subscribes, it @@ -283,25 +256,22 @@ public static ReplayProcessor createSizeAndTimeout(int size, Duration max * beginning of the sequence. *

    * - * @param the type of items observed and emitted by the Processor + * @param the type of items observed and emitted by the sink * @param maxAge the maximum age of the contained items in milliseconds * @param size the maximum number of buffered items * @param scheduler the {@link Scheduler} that provides the current time * - * @return a new {@link ReplayProcessor} that replay up to {@code size} elements, but + * @return a new {@link SinkManyReplayProcessor} that replay up to {@code size} elements, but * will evict them from its history based on their age. - * @deprecated use {@link Sinks.MulticastReplaySpec#limit(int, Duration, Scheduler) Sinks.many().replay().limit(size, maxAge, scheduler)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. */ - @Deprecated - public static ReplayProcessor createSizeAndTimeout(int size, - Duration maxAge, - Scheduler scheduler) { + static SinkManyReplayProcessor createSizeAndTimeout(int size, + Duration maxAge, + Scheduler scheduler) { Objects.requireNonNull(scheduler, "scheduler is null"); if (size <= 0) { throw new IllegalArgumentException("size > 0 required but it was " + size); } - return new ReplayProcessor<>(new FluxReplay.SizeAndTimeBoundReplayBuffer<>(size, + return new SinkManyReplayProcessor<>(new FluxReplay.SizeAndTimeBoundReplayBuffer<>(size, maxAge.toNanos(), scheduler)); } @@ -312,12 +282,12 @@ public static ReplayProcessor createSizeAndTimeout(int size, volatile FluxReplay.ReplaySubscription[] subscribers; @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater - SUBSCRIBERS = AtomicReferenceFieldUpdater.newUpdater(ReplayProcessor.class, + static final AtomicReferenceFieldUpdater + SUBSCRIBERS = AtomicReferenceFieldUpdater.newUpdater(SinkManyReplayProcessor.class, FluxReplay.ReplaySubscription[].class, "subscribers"); - ReplayProcessor(FluxReplay.ReplayBuffer buffer) { + SinkManyReplayProcessor(FluxReplay.ReplayBuffer buffer) { this.buffer = buffer; SUBSCRIBERS.lazySet(this, EMPTY); } @@ -337,12 +307,6 @@ public void subscribe(CoreSubscriber actual) { buffer.replay(rs); } - @Override - @Nullable - public Throwable getError() { - return buffer.getError(); - } - @Override @Nullable public Object scanUnsafe(Attr key) { @@ -350,8 +314,10 @@ public Object scanUnsafe(Attr key) { return subscription; } if (key == Attr.CAPACITY) return buffer.capacity(); + if (key == Attr.TERMINATED) return buffer.isDone(); + if (key == Attr.ERROR) return buffer.getError(); - return super.scanUnsafe(key); + return null; } @Override @@ -360,12 +326,12 @@ public Stream inners() { } @Override - public long downstreamCount() { - return subscribers.length; + public void dispose() { + emitError(new CancellationException("Disposed"), Sinks.EmitFailureHandler.FAIL_FAST); } @Override - public boolean isTerminated() { + public boolean isDisposed() { return buffer.isDone(); } @@ -502,7 +468,7 @@ public Sinks.EmitResult tryEmitNext(T t) { return Sinks.EmitResult.FAIL_TERMINATED; } - //note: ReplayProcessor can so far ALWAYS buffer the element, no FAIL_ZERO_SUBSCRIBER here + //note: SinkManyReplayProcessor can so far ALWAYS buffer the element, no FAIL_ZERO_SUBSCRIBER here b.add(t); for (FluxReplay.ReplaySubscription rs : subscribers) { b.replay(rs); @@ -520,17 +486,12 @@ public Flux asFlux() { return this; } - @Override - protected boolean isIdentityProcessor() { - return true; - } - static final class ReplayInner implements FluxReplay.ReplaySubscription { final CoreSubscriber actual; - final ReplayProcessor parent; + final SinkManyReplayProcessor parent; final FluxReplay.ReplayBuffer buffer; @@ -555,7 +516,7 @@ static final class ReplayInner int fusionMode; ReplayInner(CoreSubscriber actual, - ReplayProcessor parent) { + SinkManyReplayProcessor parent) { this.actual = actual; this.parent = parent; this.buffer = parent.buffer; diff --git a/reactor-core/src/main/java/reactor/core/publisher/UnicastProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java similarity index 58% rename from reactor-core/src/main/java/reactor/core/publisher/UnicastProcessor.java rename to reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java index d21f4280b9..d266bd3cd4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/UnicastProcessor.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,17 +18,14 @@ import java.util.Objects; import java.util.Queue; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import java.util.function.Consumer; import java.util.stream.Stream; -import org.reactivestreams.Subscription; - import reactor.core.CoreSubscriber; import reactor.core.Disposable; -import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.core.publisher.Sinks.EmitResult; @@ -37,12 +34,11 @@ import reactor.util.context.Context; /** - * A Processor implementation that takes a custom queue and allows - * only a single subscriber. UnicastProcessor allows multiplexing of the events which + * A {@link Sinks.Many} implementation that takes a custom queue and allows + * only a single subscriber. {@link SinkManyUnicast} allows multiplexing of the events which * means that it supports multiple producers and only one consumer. * However, it should be noticed that multi-producer case is only valid if appropriate - * Queue - * is provided. Otherwise, it could break + * Queue is provided. Otherwise, it could break * Reactive Streams Spec if Publishers * publish on different threads. * @@ -54,19 +50,19 @@ *
    * *

    - * Note: UnicastProcessor does not respect the actual subscriber's + * Note: SinkManyUnicast does not respect the actual subscriber's * demand as it is described in * Reactive Streams Spec. However, - * UnicastProcessor embraces configurable Queue internally which allows enabling + * SinkManyUnicast embraces configurable Queue internally which allows enabling * backpressure support and preventing of consumer's overwhelming. * - * Hence, interaction model between producers and UnicastProcessor will be PUSH - * only. In opposite, interaction model between UnicastProcessor and consumer will be + * Hence, interaction model between producers and SinkManyUnicast will be PUSH + * only. In opposite, interaction model between SinkManyUnicast and consumer will be * PUSH-PULL as defined in * Reactive Streams Spec. * * In the case when upstream's signals overflow the bound of internal Queue, - * UnicastProcessor will fail with signaling onError( + * SinkManyUnicast will fail with signaling onError( * {@literal reactor.core.Exceptions.OverflowException}). * *

    @@ -87,88 +83,51 @@ *

    * * @param the input and output type - * @deprecated to be removed in 3.5, prefer clear cut usage of {@link Sinks} through - * variations under {@link reactor.core.publisher.Sinks.UnicastSpec Sinks.many().unicast()}. */ -@Deprecated -public final class UnicastProcessor extends FluxProcessor - implements Fuseable.QueueSubscription, Fuseable, InnerOperator, - InternalManySink { +final class SinkManyUnicast extends Flux implements InternalManySink, Disposable, Fuseable.QueueSubscription, Fuseable { /** - * Create a new {@link UnicastProcessor} that will buffer on an internal queue in an + * Create a new {@link SinkManyUnicast} that will buffer on an internal queue in an * unbounded fashion. * * @param the relayed type - * @return a unicast {@link FluxProcessor} - * @deprecated use {@link Sinks.UnicastSpec#onBackpressureBuffer() Sinks.many().unicast().onBackpressureBuffer()} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + * @return a unicast {@link Sinks.Many} */ - @Deprecated - public static UnicastProcessor create() { - return new UnicastProcessor<>(Queues.unbounded().get()); + static SinkManyUnicast create() { + return new SinkManyUnicast<>(Queues.unbounded().get()); } /** - * Create a new {@link UnicastProcessor} that will buffer on a provided queue in an + * Create a new {@link SinkManyUnicast} that will buffer on a provided queue in an * unbounded fashion. * * @param queue the buffering queue * @param the relayed type - * @return a unicast {@link FluxProcessor} - * @deprecated use {@link Sinks.UnicastSpec#onBackpressureBuffer(Queue) Sinks.many().unicast().onBackpressureBuffer(queue)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + * @return a unicast {@link Sinks.Many} */ - @Deprecated - public static UnicastProcessor create(Queue queue) { - return new UnicastProcessor<>(Hooks.wrapQueue(queue)); + static SinkManyUnicast create(Queue queue) { + return new SinkManyUnicast<>(Hooks.wrapQueue(queue)); } /** - * Create a new {@link UnicastProcessor} that will buffer on a provided queue in an + * Create a new {@link SinkManyUnicast} that will buffer on a provided queue in an * unbounded fashion. * * @param queue the buffering queue * @param endcallback called on any terminal signal * @param the relayed type - * @return a unicast {@link FluxProcessor} - * @deprecated use {@link Sinks.UnicastSpec#onBackpressureBuffer(Queue, Disposable) Sinks.many().unicast().onBackpressureBuffer(queue, endCallback)} - * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + * @return a unicast {@link Sinks.Many} */ - @Deprecated - public static UnicastProcessor create(Queue queue, Disposable endcallback) { - return new UnicastProcessor<>(Hooks.wrapQueue(queue), endcallback); - } - - /** - * Create a new {@link UnicastProcessor} that will buffer on a provided queue in an - * unbounded fashion. - * - * @param queue the buffering queue - * @param endcallback called on any terminal signal - * @param onOverflow called when queue.offer return false and unicastProcessor is - * about to emit onError. - * @param the relayed type - * - * @return a unicast {@link FluxProcessor} - * @deprecated use {@link Sinks.UnicastSpec#onBackpressureBuffer(Queue, Disposable) Sinks.many().unicast().onBackpressureBuffer(queue, endCallback)} - * (or the unsafe variant if you're sure about external synchronization). The {@code onOverflow} callback is not - * supported anymore. To be removed in 3.5. - */ - @Deprecated - public static UnicastProcessor create(Queue queue, - Consumer onOverflow, - Disposable endcallback) { - return new UnicastProcessor<>(Hooks.wrapQueue(queue), onOverflow, endcallback); + static SinkManyUnicast create(Queue queue, Disposable endcallback) { + return new SinkManyUnicast<>(Hooks.wrapQueue(queue), endcallback); } final Queue queue; - final Consumer onOverflow; - volatile Disposable onTerminate; + volatile Disposable onTerminate; @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater ON_TERMINATE = - AtomicReferenceFieldUpdater.newUpdater(UnicastProcessor.class, Disposable.class, "onTerminate"); + static final AtomicReferenceFieldUpdater ON_TERMINATE = + AtomicReferenceFieldUpdater.newUpdater(SinkManyUnicast.class, Disposable.class, "onTerminate"); volatile boolean done; Throwable error; @@ -178,54 +137,38 @@ public static UnicastProcessor create(Queue queue, volatile boolean cancelled; - volatile int once; + volatile int once; @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE = - AtomicIntegerFieldUpdater.newUpdater(UnicastProcessor.class, "once"); + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(SinkManyUnicast.class, "once"); - volatile int wip; + volatile int wip; @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP = - AtomicIntegerFieldUpdater.newUpdater(UnicastProcessor.class, "wip"); + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(SinkManyUnicast.class, "wip"); - volatile int discardGuard; + volatile int discardGuard; @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater DISCARD_GUARD = - AtomicIntegerFieldUpdater.newUpdater(UnicastProcessor.class, "discardGuard"); + static final AtomicIntegerFieldUpdater DISCARD_GUARD = + AtomicIntegerFieldUpdater.newUpdater(SinkManyUnicast.class, "discardGuard"); - volatile long requested; + volatile long requested; @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = - AtomicLongFieldUpdater.newUpdater(UnicastProcessor.class, "requested"); + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(SinkManyUnicast.class, "requested"); boolean outputFused; - public UnicastProcessor(Queue queue) { + SinkManyUnicast(Queue queue) { this.queue = Objects.requireNonNull(queue, "queue"); this.onTerminate = null; - this.onOverflow = null; - } - - public UnicastProcessor(Queue queue, Disposable onTerminate) { - this.queue = Objects.requireNonNull(queue, "queue"); - this.onTerminate = Objects.requireNonNull(onTerminate, "onTerminate"); - this.onOverflow = null; } - @Deprecated - public UnicastProcessor(Queue queue, - Consumer onOverflow, - Disposable onTerminate) { + SinkManyUnicast(Queue queue, Disposable onTerminate) { this.queue = Objects.requireNonNull(queue, "queue"); - this.onOverflow = Objects.requireNonNull(onOverflow, "onOverflow"); this.onTerminate = Objects.requireNonNull(onTerminate, "onTerminate"); } - @Override - public int getBufferSize() { - return Queues.capacity(this.queue); - } - @Override public Stream inners() { return hasDownstream ? Stream.of(Scannable.from(actual)) : Stream.empty(); @@ -233,19 +176,15 @@ public Stream inners() { @Override public Object scanUnsafe(Attr key) { - if (Attr.ACTUAL == key) return actual(); + if (Attr.ACTUAL == key) return actual; if (Attr.BUFFERED == key) return queue.size(); + if (Attr.CAPACITY == key) return Queues.capacity(this.queue); if (Attr.PREFETCH == key) return Integer.MAX_VALUE; if (Attr.CANCELLED == key) return cancelled; + if (Attr.TERMINATED == key) return done; + if (Attr.ERROR == key) return error; - //TERMINATED and ERROR covered in super - return super.scanUnsafe(key); - } - - @Override - public void onComplete() { - //no particular error condition handling for onComplete - @SuppressWarnings("unused") EmitResult emitResult = tryEmitComplete(); + return null; } @Override @@ -265,11 +204,6 @@ public EmitResult tryEmitComplete() { return Sinks.EmitResult.OK; } - @Override - public void onError(Throwable throwable) { - emitError(throwable, Sinks.EmitFailureHandler.FAIL_FAST); - } - @Override public Sinks.EmitResult tryEmitError(Throwable t) { if (done) { @@ -288,41 +222,6 @@ public Sinks.EmitResult tryEmitError(Throwable t) { return EmitResult.OK; } - @Override - public void onNext(T t) { - emitNext(t, Sinks.EmitFailureHandler.FAIL_FAST); - } - - @Override - public void emitNext(T value, Sinks.EmitFailureHandler failureHandler) { - if (onOverflow == null) { - InternalManySink.super.emitNext(value, failureHandler); - return; - } - - // TODO consider deprecating onOverflow and suggesting using a strategy instead - InternalManySink.super.emitNext( - value, (signalType, emission) -> { - boolean shouldRetry = failureHandler.onEmitFailure(SignalType.ON_NEXT, emission); - if (!shouldRetry) { - switch (emission) { - case FAIL_ZERO_SUBSCRIBER: - case FAIL_OVERFLOW: - try { - onOverflow.accept(value); - } - catch (Throwable e) { - Exceptions.throwIfFatal(e); - emitError(e, Sinks.EmitFailureHandler.FAIL_FAST); - } - break; - } - } - return shouldRetry; - } - ); - } - @Override public EmitResult tryEmitNext(T t) { if (done) { @@ -349,11 +248,6 @@ public Flux asFlux() { return this; } - @Override - protected boolean isIdentityProcessor() { - return true; - } - void doTerminate() { Disposable r = onTerminate; if (r != null && ON_TERMINATE.compareAndSet(this, r, null)) { @@ -500,15 +394,6 @@ boolean checkTerminated(boolean d, boolean empty, CoreSubscriber a, Q return false; } - @Override - public void onSubscribe(Subscription s) { - if (done || cancelled) { - s.cancel(); - } else { - s.request(Long.MAX_VALUE); - } - } - @Override public int getPrefetch() { return Integer.MAX_VALUE; @@ -534,8 +419,7 @@ public void subscribe(CoreSubscriber actual) { drain(null); } } else { - Operators.error(actual, new IllegalStateException("UnicastProcessor " + - "allows only a single Subscriber")); + Operators.error(actual, new IllegalStateException("Sinks.many().unicast() sinks only allow a single Subscriber")); } } @@ -619,34 +503,12 @@ public int requestFusion(int requestedMode) { } @Override - public boolean isDisposed() { - return cancelled || done; - } - - @Override - public boolean isTerminated() { - return done; - } - - @Override - @Nullable - public Throwable getError() { - return error; + public void dispose() { + emitError(new CancellationException("Disposed"), Sinks.EmitFailureHandler.FAIL_FAST); } @Override - public CoreSubscriber actual() { - return actual; - } - - @Override - public long downstreamCount() { - return hasDownstreams() ? 1L : 0L; - } - - @Override - public boolean hasDownstreams() { - return hasDownstream; + public boolean isDisposed() { + return cancelled || done; } - } diff --git a/reactor-core/src/main/java/reactor/core/publisher/UnicastManySinkNoBackpressure.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicastNoBackpressure.java similarity index 87% rename from reactor-core/src/main/java/reactor/core/publisher/UnicastManySinkNoBackpressure.java rename to reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicastNoBackpressure.java index 7714c33efa..3c9d12f539 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/UnicastManySinkNoBackpressure.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicastNoBackpressure.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,10 +29,10 @@ import reactor.util.annotation.Nullable; import reactor.util.context.Context; -final class UnicastManySinkNoBackpressure extends Flux implements InternalManySink, Subscription, ContextHolder { +final class SinkManyUnicastNoBackpressure extends Flux implements InternalManySink, Subscription, ContextHolder { - public static UnicastManySinkNoBackpressure create() { - return new UnicastManySinkNoBackpressure<>(); + public static SinkManyUnicastNoBackpressure create() { + return new SinkManyUnicastNoBackpressure<>(); } enum State { @@ -45,20 +45,20 @@ enum State { volatile State state; @SuppressWarnings("rawtypes") - private static final AtomicReferenceFieldUpdater STATE = AtomicReferenceFieldUpdater.newUpdater( - UnicastManySinkNoBackpressure.class, + private static final AtomicReferenceFieldUpdater STATE = AtomicReferenceFieldUpdater.newUpdater( + SinkManyUnicastNoBackpressure.class, State.class, "state" ); private volatile CoreSubscriber actual = null; - volatile long requested; + volatile long requested; @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = - AtomicLongFieldUpdater.newUpdater(UnicastManySinkNoBackpressure.class, "requested"); + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(SinkManyUnicastNoBackpressure.class, "requested"); - UnicastManySinkNoBackpressure() { + SinkManyUnicastNoBackpressure() { STATE.lazySet(this, State.INITIAL); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Sinks.java b/reactor-core/src/main/java/reactor/core/publisher/Sinks.java index 7eff34ac30..e84732ed1b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Sinks.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Sinks.java @@ -22,7 +22,6 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; -import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.Exceptions; import reactor.core.Scannable; diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinksSpecs.java b/reactor-core/src/main/java/reactor/core/publisher/SinksSpecs.java index 5ffa3da3f5..eebde32595 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinksSpecs.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinksSpecs.java @@ -99,17 +99,17 @@ public Sinks.MulticastReplaySpec replay() { @Override public Sinks.ManyWithUpstream onBackpressureBuffer() { - return new EmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); + return new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); } @Override public Sinks.ManyWithUpstream onBackpressureBuffer(int bufferSize) { - return new EmitterProcessor<>(true, bufferSize); + return new SinkManyEmitterProcessor<>(true, bufferSize); } @Override public Sinks.ManyWithUpstream onBackpressureBuffer(int bufferSize, boolean autoCancel) { - return new EmitterProcessor<>(autoCancel, bufferSize); + return new SinkManyEmitterProcessor<>(autoCancel, bufferSize); } @Override @@ -129,47 +129,47 @@ public Many directBestEffort() { @Override public Many all() { - return ReplayProcessor.create(); + return SinkManyReplayProcessor.create(); } @Override public Many all(int batchSize) { - return ReplayProcessor.create(batchSize); + return SinkManyReplayProcessor.create(batchSize); } @Override public Many latest() { - return ReplayProcessor.cacheLast(); + return SinkManyReplayProcessor.cacheLast(); } @Override public Many latestOrDefault(T value) { - return ReplayProcessor.cacheLastOrDefault(value); + return SinkManyReplayProcessor.cacheLastOrDefault(value); } @Override public Many limit(int historySize) { - return ReplayProcessor.create(historySize); + return SinkManyReplayProcessor.create(historySize); } @Override public Many limit(Duration maxAge) { - return ReplayProcessor.createTimeout(maxAge); + return SinkManyReplayProcessor.createTimeout(maxAge); } @Override public Many limit(Duration maxAge, Scheduler scheduler) { - return ReplayProcessor.createTimeout(maxAge, scheduler); + return SinkManyReplayProcessor.createTimeout(maxAge, scheduler); } @Override public Many limit(int historySize, Duration maxAge) { - return ReplayProcessor.createSizeAndTimeout(historySize, maxAge); + return SinkManyReplayProcessor.createSizeAndTimeout(historySize, maxAge); } @Override public Many limit(int historySize, Duration maxAge, Scheduler scheduler) { - return ReplayProcessor.createSizeAndTimeout(historySize, maxAge, scheduler); + return SinkManyReplayProcessor.createSizeAndTimeout(historySize, maxAge, scheduler); } } @@ -224,23 +224,17 @@ public Sinks.MulticastReplaySpec replay() { @Override public Many onBackpressureBuffer() { - @SuppressWarnings("deprecation") // EmitterProcessor will be removed in 3.5. - final EmitterProcessor original = EmitterProcessor.create(); - return wrapMany(original); + return wrapMany(new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE)); } @Override public Many onBackpressureBuffer(int bufferSize) { - @SuppressWarnings("deprecation") // EmitterProcessor will be removed in 3.5. - final EmitterProcessor original = EmitterProcessor.create(bufferSize); - return wrapMany(original); + return wrapMany(new SinkManyEmitterProcessor<>(true, bufferSize)); } @Override public Many onBackpressureBuffer(int bufferSize, boolean autoCancel) { - @SuppressWarnings("deprecation") // EmitterProcessor will be removed in 3.5. - final EmitterProcessor original = EmitterProcessor.create(bufferSize, autoCancel); - return wrapMany(original); + return wrapMany(new SinkManyEmitterProcessor<>(autoCancel, bufferSize)); } @Override @@ -258,29 +252,25 @@ public Many directBestEffort() { @Override public Many all() { - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5. - final ReplayProcessor original = ReplayProcessor.create(); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.create(); return wrapMany(original); } @Override public Many all(int batchSize) { - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5 - final ReplayProcessor original = ReplayProcessor.create(batchSize, true); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.create(batchSize, true); return wrapMany(original); } @Override public Many latest() { - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5. - final ReplayProcessor original = ReplayProcessor.cacheLast(); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.cacheLast(); return wrapMany(original); } @Override public Many latestOrDefault(T value) { - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5. - final ReplayProcessor original = ReplayProcessor.cacheLastOrDefault(value); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.cacheLastOrDefault(value); return wrapMany(original); } @@ -289,22 +279,19 @@ public Many limit(int historySize) { if (historySize <= 0) { throw new IllegalArgumentException("historySize must be > 0"); } - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5. - final ReplayProcessor original = ReplayProcessor.create(historySize); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.create(historySize); return wrapMany(original); } @Override public Many limit(Duration maxAge) { - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5. - final ReplayProcessor original = ReplayProcessor.createTimeout(maxAge); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.createTimeout(maxAge); return wrapMany(original); } @Override public Many limit(Duration maxAge, Scheduler scheduler) { - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5. - final ReplayProcessor original = ReplayProcessor.createTimeout(maxAge, scheduler); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.createTimeout(maxAge, scheduler); return wrapMany(original); } @@ -313,8 +300,7 @@ public Many limit(int historySize, Duration maxAge) { if (historySize <= 0) { throw new IllegalArgumentException("historySize must be > 0"); } - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5. - final ReplayProcessor original = ReplayProcessor.createSizeAndTimeout(historySize, maxAge); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.createSizeAndTimeout(historySize, maxAge); return wrapMany(original); } @@ -323,8 +309,7 @@ public Many limit(int historySize, Duration maxAge, Scheduler scheduler) if (historySize <= 0) { throw new IllegalArgumentException("historySize must be > 0"); } - @SuppressWarnings("deprecation") // ReplayProcessor will be removed in 3.5. - final ReplayProcessor original = ReplayProcessor.createSizeAndTimeout(historySize, maxAge, scheduler); + final SinkManyReplayProcessor original = SinkManyReplayProcessor.createSizeAndTimeout(historySize, maxAge, scheduler); return wrapMany(original); } } @@ -346,28 +331,25 @@ & ContextHolder> Many wrapMany(MANY original) { @Override public Many onBackpressureBuffer() { - @SuppressWarnings("deprecation") // UnicastProcessor will be removed in 3.5. - final UnicastProcessor original = UnicastProcessor.create(); + final SinkManyUnicast original = SinkManyUnicast.create(); return wrapMany(original); } @Override public Many onBackpressureBuffer(Queue queue) { - @SuppressWarnings("deprecation") // UnicastProcessor will be removed in 3.5. - final UnicastProcessor original = UnicastProcessor.create(queue); + final SinkManyUnicast original = SinkManyUnicast.create(queue); return wrapMany(original); } @Override public Many onBackpressureBuffer(Queue queue, Disposable endCallback) { - @SuppressWarnings("deprecation") // UnicastProcessor will be removed in 3.5. - final UnicastProcessor original = UnicastProcessor.create(queue, endCallback); + final SinkManyUnicast original = SinkManyUnicast.create(queue, endCallback); return wrapMany(original); } @Override public Many onBackpressureError() { - final UnicastManySinkNoBackpressure original = UnicastManySinkNoBackpressure.create(); + final SinkManyUnicastNoBackpressure original = SinkManyUnicastNoBackpressure.create(); return wrapMany(original); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/package-info.java b/reactor-core/src/main/java/reactor/core/publisher/package-info.java index 9b1b66d4e9..296538c4ce 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/package-info.java +++ b/reactor-core/src/main/java/reactor/core/publisher/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,28 +15,15 @@ */ /** - * Provide for - * {@link reactor.core.publisher.Flux}, {@link reactor.core.publisher.Mono} composition - * API and {@link org.reactivestreams.Processor} implementations + * Provide main Reactive APIs in {@link reactor.core.publisher.Flux} and {@link reactor.core.publisher.Mono}, + * as well as various helper classes, interfaces used in the composition API, variants of Flux and operator-building + * utilities. * *

    Flux

    * A typed N-elements or zero sequence {@link org.reactivestreams.Publisher} with core reactive extensions. * *

    Mono

    * A typed one-element at most sequence {@link org.reactivestreams.Publisher} with core reactive extensions. - * - *

    Processors

    - * The following - * {@link org.reactivestreams.Processor} extending {@link reactor.core.publisher.FluxProcessor} are available: - *
      - *
    • A synchronous/non-opinionated pub-sub replaying capable event emitter : - * {@link reactor.core.publisher.EmitterProcessor}, - * {@link reactor.core.publisher.ReplayProcessor}, - * {@link reactor.core.publisher.UnicastProcessor} and - * {@link reactor.core.publisher.DirectProcessor}
    • - *
    • {@link reactor.core.publisher.FluxProcessor} itself offers factories to build arbitrary {@link org.reactivestreams.Processor}
    • - *
    - *

    ** * @author Stephane Maldini */ diff --git a/reactor-core/src/tckTest/java/reactor/core/publisher/tck/AbstractProcessorVerification.java b/reactor-core/src/tckTest/java/reactor/core/publisher/tck/AbstractProcessorVerification.java deleted file mode 100644 index 41963c571e..0000000000 --- a/reactor-core/src/tckTest/java/reactor/core/publisher/tck/AbstractProcessorVerification.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher.tck; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import org.reactivestreams.Publisher; -import org.reactivestreams.tck.TestEnvironment; -import reactor.core.publisher.Flux; - -import static org.reactivestreams.tck.TestEnvironment.envDefaultNoSignalsTimeoutMillis; - -/** - * @author Stephane Maldini - */ -public abstract class AbstractProcessorVerification extends org.reactivestreams.tck.IdentityProcessorVerification { - -// final ExecutorService executorService = Executors.newCachedThreadPool(); - - @Override - public ExecutorService publisherExecutorService() { -// return executorService; - return Executors.newCachedThreadPool(); - } - - AbstractProcessorVerification() { - super(new TestEnvironment(500, envDefaultNoSignalsTimeoutMillis(), false)); - } - - @Override - public Long createElement(int element) { - return (long) element; - } - - @Override - public Publisher createFailedPublisher() { - return Flux.error(new Exception("test")); - } -} diff --git a/reactor-core/src/tckTest/java/reactor/core/publisher/tck/EmitterProcessorVerification.java b/reactor-core/src/tckTest/java/reactor/core/publisher/tck/EmitterProcessorVerification.java deleted file mode 100644 index ff0ccdd1ea..0000000000 --- a/reactor-core/src/tckTest/java/reactor/core/publisher/tck/EmitterProcessorVerification.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher.tck; - -import java.util.logging.Level; - -import org.reactivestreams.Processor; -import org.testng.SkipException; - -/** - * @author Stephane Maldini - */ -@org.testng.annotations.Test -public class EmitterProcessorVerification extends AbstractProcessorVerification { - - @Override - @SuppressWarnings("deprecation") // This is ok because this uses FluxProcessor and EmitterProcessor, to be removed in 3.5 - public Processor createIdentityProcessor(int bufferSize) { - reactor.core.publisher.FluxProcessor p = reactor.core.publisher.EmitterProcessor.create(bufferSize); - return reactor.core.publisher.FluxProcessor.wrap(p, p.log("EmitterProcessorVerification", Level.FINE)); - } - - @Override - public void required_mustRequestFromUpstreamForElementsThatHaveBeenRequestedLongAgo() - throws Throwable { - throw new SkipException("WARNING: EmitterProcessor does not emit until all " + - "subscribers request at least 1"); - } - - @Override - public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError() - throws Throwable { - throw new SkipException("WARNING: EmitterProcessor does not emit until all " + - "subscribers request at least 1"); - } -} diff --git a/reactor-core/src/test/java/reactor/core/ContextBestPracticesArchTest.java b/reactor-core/src/test/java/reactor/core/ContextBestPracticesArchTest.java index 8cc11c93f1..ccd366afe2 100644 --- a/reactor-core/src/test/java/reactor/core/ContextBestPracticesArchTest.java +++ b/reactor-core/src/test/java/reactor/core/ContextBestPracticesArchTest.java @@ -24,10 +24,8 @@ import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; - import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.MonoSink; import reactor.core.publisher.SynchronousSink; @@ -42,13 +40,6 @@ class ContextBestPracticesArchTest { .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) .importPackagesOf(CoreSubscriber.class); - // This is ok as this class tests the deprecated FluxProcessor. Will be removed with it in 3.5. - @SuppressWarnings("deprecation") - static JavaClasses FLUXPROCESSOR_CLASSES = new ClassFileImporter() - .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) - .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) - .importPackagesOf(reactor.core.publisher.FluxProcessor.class); - @Test void smokeTestWhereClassesLoaded() { assertThat(CORE_SUBSCRIBER_CLASSES).isNotEmpty(); @@ -80,37 +71,6 @@ public void check(JavaClass item, ConditionEvents events) { .check(CORE_SUBSCRIBER_CLASSES); } - @Test - // This is ok as this class tests the deprecated FluxProcessor. Will be removed with it in 3.5. - @SuppressWarnings("deprecation") - void fluxProcessorsShouldNotUseDefaultCurrentContext() { - classes() - .that().areAssignableTo(reactor.core.publisher.FluxProcessor.class) - .and().doNotHaveModifier(JavaModifier.ABSTRACT) - .should(new ArchCondition("not use the default currentContext()") { - @Override - public void check(JavaClass item, ConditionEvents events) { - boolean overridesMethod = item - .getAllMethods() - .stream() - .filter(it -> "currentContext".equals(it.getName())) - .filter(it -> it.getRawParameterTypes().isEmpty()) - //method declared in a class derived from FluxProcessor but NOT FluxProcessor itself ! - .anyMatch(it -> it.getOwner().isAssignableTo(reactor.core.publisher.FluxProcessor.class) - && !it.getOwner().isEquivalentTo(reactor.core.publisher.FluxProcessor.class)); - - - if (!overridesMethod) { - events.add(SimpleConditionEvent.violated( - item, - item.getFullName() + item.getSourceCodeLocation() + ": FluxProcessor#currentContext() is not overridden" - )); - } - } - }) - .check(FLUXPROCESSOR_CLASSES); - } - @Test void oldSinksShouldNotUseDefaultCurrentContext() { classes() diff --git a/reactor-core/src/test/java/reactor/core/publisher/DelegateProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/DelegateProcessorTest.java deleted file mode 100644 index ca2cb1b86e..0000000000 --- a/reactor-core/src/test/java/reactor/core/publisher/DelegateProcessorTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.reactivestreams.Publisher; -import reactor.core.Scannable; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -// This is ok as this class tests the deprecated DelegateProcessor. Will be removed with it in 3.5. -@SuppressWarnings("deprecation") -public class DelegateProcessorTest { - - @Test - public void scanReturnsDownStreamForParentElseDelegates() { - Publisher downstream = Mockito.mock(FluxOperator.class); - - IllegalStateException boom = new IllegalStateException("boom"); - InnerConsumer upstream = Mockito.mock(InnerConsumer.class); - when(upstream.scanUnsafe(Scannable.Attr.ERROR)) - .thenReturn(boom); - when(upstream.scanUnsafe(Scannable.Attr.DELAY_ERROR)) - .thenReturn(true); - - DelegateProcessor processor = new DelegateProcessor<>( - downstream, upstream); - - assertThat(processor.scan(Scannable.Attr.PARENT)).isSameAs(downstream); - - assertThat(processor.scan(Scannable.Attr.ERROR)).isSameAs(boom); - assertThat(processor.scan(Scannable.Attr.DELAY_ERROR)).isTrue(); - } -} diff --git a/reactor-core/src/test/java/reactor/core/publisher/DirectProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/DirectProcessorTest.java deleted file mode 100644 index 5f2edabbf6..0000000000 --- a/reactor-core/src/test/java/reactor/core/publisher/DirectProcessorTest.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -import org.junit.jupiter.api.Test; -import java.time.Duration; - -import org.reactivestreams.Subscriber; - -import reactor.test.StepVerifier; -import reactor.test.subscriber.AssertSubscriber; -import reactor.util.context.Context; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -// This is ok as this class tests the deprecated DirectProcessor. Will be removed with it in 3.5. -@SuppressWarnings("deprecation") -public class DirectProcessorTest { - - @Test - public void onNextNull() { - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - DirectProcessor.create().onNext(null); - }); - } - - @Test - public void onErrorNull() { - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - DirectProcessor.create().onError(null); - }); - } - - @Test - public void onSubscribeNull() { - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - DirectProcessor.create().onSubscribe(null); - }); - } - - @Test - public void subscribeNull() { - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - DirectProcessor.create().subscribe((Subscriber) null); - }); - } - - @Test - public void normal() { - DirectProcessor tp = DirectProcessor.create(); - - StepVerifier.create(tp) - .then(() -> { - assertThat(tp.hasDownstreams()).as("has downstreams").isTrue(); - assertThat(tp.hasCompleted()).as("hasCompleted").isFalse(); - assertThat(tp.getError()).as("getError").isNull(); - assertThat(tp.hasError()).as("hasError").isFalse(); - }) - .then(() -> { - tp.onNext(1); - tp.onNext(2); - }) - .expectNext(1, 2) - .then(() -> { - tp.onNext(3); - tp.onComplete(); - }) - .expectNext(3) - .expectComplete() - .verify(); - - assertThat(tp.hasDownstreams()).as("has downstreams").isFalse(); - assertThat(tp.hasCompleted()).as("hasCompleted").isTrue(); - assertThat(tp.getError()).as("getError").isNull(); - assertThat(tp.hasError()).as("hasError").isFalse(); - } - - @Test - public void normalBackpressured() { - DirectProcessor tp = DirectProcessor.create(); - - StepVerifier.create(tp, 0L) - .then(() -> { - assertThat(tp.hasDownstreams()).as("has downstreams").isTrue(); - assertThat(tp.hasCompleted()).as("hasCompleted").isFalse(); - assertThat(tp.getError()).as("getError").isNull(); - assertThat(tp.hasError()).as("hasError").isFalse(); - }) - .thenRequest(10L) - .then(() -> { - tp.onNext(1); - tp.onNext(2); - tp.onComplete(); - }) - .expectNext(1, 2) - .expectComplete() - .verify(); - - assertThat(tp.hasDownstreams()).as("has downstreams").isFalse(); - assertThat(tp.hasCompleted()).as("hasCompleted").isTrue(); - assertThat(tp.getError()).as("getError").isNull(); - assertThat(tp.hasError()).as("hasError").isFalse(); - } - - @Test - public void notEnoughRequests() { - DirectProcessor tp = DirectProcessor.create(); - - StepVerifier.create(tp, 1L) - .then(() -> { - tp.onNext(1); - tp.onNext(2); - tp.onComplete(); - }) - .expectNext(1) - .expectError(IllegalStateException.class) - .verify(); - } - - @Test - public void error() { - AssertSubscriber ts = AssertSubscriber.create(); - - DirectProcessor tp = DirectProcessor.create(); - - tp.subscribe(ts); - - assertThat(tp.hasDownstreams()).as("has downstreams").isTrue(); - assertThat(tp.hasCompleted()).as("hasCompleted").isFalse(); - assertThat(tp.getError()).as("getError").isNull(); - assertThat(tp.hasError()).as("hasError").isFalse(); - - ts.assertNoValues() - .assertNoError() - .assertNotComplete(); - - tp.onNext(1); - tp.onNext(2); - - ts.assertValues(1, 2) - .assertNotComplete() - .assertNoError(); - - tp.onNext(3); - tp.onError(new RuntimeException("forced failure")); - - assertThat(tp.hasDownstreams()).as("has downstreams").isFalse(); - assertThat(tp.hasCompleted()).as("hasCompleted").isFalse(); - assertThat(tp.hasError()).as("hasError").isTrue(); - assertThat(tp.getError()).as("getError") - .isNotNull() - .isExactlyInstanceOf(RuntimeException.class) - .hasMessage("forced failure"); - - ts.assertValues(1, 2, 3) - .assertNotComplete() - .assertError(RuntimeException.class) - .assertErrorMessage("forced failure"); - } - - @Test - public void terminatedWithError() { - AssertSubscriber ts = AssertSubscriber.create(); - - DirectProcessor tp = DirectProcessor.create(); - tp.onError(new RuntimeException("forced failure")); - - tp.subscribe(ts); - - assertThat(tp.hasDownstreams()).as("has downstreams").isFalse(); - assertThat(tp.hasCompleted()).as("hasCompleted").isFalse(); - assertThat(tp.hasError()).as("hasError").isTrue(); - assertThat(tp.getError()).as("getError") - .isNotNull() - .isExactlyInstanceOf(RuntimeException.class) - .hasMessage("forced failure"); - - ts.assertNoValues() - .assertNotComplete() - .assertError(RuntimeException.class) - .assertErrorMessage("forced failure"); - } - - @Test - public void terminatedNormally() { - AssertSubscriber ts = AssertSubscriber.create(); - - DirectProcessor tp = DirectProcessor.create(); - tp.onComplete(); - - tp.subscribe(ts); - - assertThat(tp.hasDownstreams()).as("has downstreams").isFalse(); - assertThat(tp.hasCompleted()).as("hasCompleted").isTrue(); - assertThat(tp.getError()).as("getError").isNull(); - assertThat(tp.hasError()).as("hasError").isFalse(); - - ts.assertNoValues() - .assertComplete() - .assertNoError(); - } - - @Test - public void subscriberAlreadyCancelled() { - AssertSubscriber ts = AssertSubscriber.create(); - ts.cancel(); - - DirectProcessor tp = DirectProcessor.create(); - - tp.subscribe(ts); - - assertThat(tp.hasDownstreams()).as("has downstreams").isFalse(); - - tp.onNext(1); - - - ts.assertNoValues() - .assertNotComplete() - .assertNoError(); - } - - @Test - public void subscriberCancels() { - AssertSubscriber ts = AssertSubscriber.create(); - - DirectProcessor tp = DirectProcessor.create(); - - tp.subscribe(ts); - - assertThat(tp.hasDownstreams()).as("has downstreams").isTrue(); - - tp.onNext(1); - - ts.assertValues(1) - .assertNoError() - .assertNotComplete(); - - ts.cancel(); - - assertThat(tp.hasDownstreams()).as("has downstreams").isFalse(); - - tp.onNext(2); - - ts.assertValues(1) - .assertNotComplete() - .assertNoError(); - } - - @Test - public void currentContextDelegatesToFirstSubscriber() { - AssertSubscriber testSubscriber1 = new AssertSubscriber<>(Context.of("key", "value1")); - AssertSubscriber testSubscriber2 = new AssertSubscriber<>(Context.of("key", "value2")); - - DirectProcessor directProcessor = new DirectProcessor<>(); - directProcessor.subscribe(testSubscriber1); - directProcessor.subscribe(testSubscriber2); - - Context processorContext = directProcessor.currentContext(); - - assertThat(processorContext.getOrDefault("key", "EMPTY")).isEqualTo("value1"); - } - - @Test - public void onNextWithNoSubscriberJustDiscardsWithoutTerminatingTheSink() { - DirectProcessor directProcessor = DirectProcessor.create(); - directProcessor.onNext(1); - - StepVerifier.create(directProcessor) - .expectSubscription() - .expectNoEvent(Duration.ofSeconds(1)) - .then(() -> directProcessor.onNext(2)) - .then(directProcessor::onComplete) - .expectNext(2) - .expectComplete() - .verify(Duration.ofSeconds(5)); - } - -} diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxProcessorTest.java deleted file mode 100644 index 6d123df4a5..0000000000 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxProcessorTest.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -import java.time.Duration; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.assertj.core.api.AssertionsForClassTypes; -import org.junit.jupiter.api.Test; -import org.reactivestreams.Subscriber; -import reactor.core.Scannable; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; -import reactor.test.StepVerifier; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; - -// This is ok as this class tests the deprecated FluxProcessor. Will be removed with it in 3.5. -@SuppressWarnings("deprecation") -public class FluxProcessorTest { - - @Test - @SuppressWarnings("unchecked") - public void failNullSubscriber() { - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - FluxProcessor.wrap(UnicastProcessor.create(), UnicastProcessor.create()) - .subscribe((Subscriber) null); - }); - } - - @Test - public void failNullUpstream() { - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - FluxProcessor.wrap(null, UnicastProcessor.create()); - }); - } - - @Test - public void failNullDownstream() { - assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - FluxProcessor.wrap(UnicastProcessor.create(), null); - }); - } - - @Test - public void testCapacity(){ - assertThat(FluxProcessor.wrap(UnicastProcessor.create(), UnicastProcessor - .create()).getBufferSize()) - .isEqualTo(Integer.MAX_VALUE); - } - - @Test - @SuppressWarnings("unchecked") - public void normalBlackboxProcessor(){ - UnicastProcessor upstream = UnicastProcessor.create(); - FluxProcessor processor = - FluxProcessor.wrap(upstream, upstream.map(i -> i + 1) - .filter(i -> i % 2 == 0)); - - DelegateProcessor delegateProcessor = - (DelegateProcessor)processor; - - delegateProcessor.parents().findFirst().ifPresent(s -> - assertThat(s).isInstanceOf(FluxFilterFuseable.class)); - - - StepVerifier.create(processor) - .then(() -> Flux.just(1, 2, 3).subscribe(processor)) - .expectNext(2, 4) - .verifyComplete(); - } - - @Test - public void disconnectedBlackboxProcessor(){ - UnicastProcessor upstream = UnicastProcessor.create(); - FluxProcessor processor = - FluxProcessor.wrap(upstream, Flux.just(1)); - - StepVerifier.create(processor) - .expectNext(1) - .verifyComplete(); - } - - @Test - public void symmetricBlackboxProcessor(){ - UnicastProcessor upstream = UnicastProcessor.create(); - FluxProcessor processor = - FluxProcessor.wrap(upstream, upstream); - - StepVerifier.create(processor) - .then(() -> Flux.just(1).subscribe(processor)) - .expectNext(1) - .verifyComplete(); - } - - @Test - public void errorSymmetricBlackboxProcessor(){ - UnicastProcessor upstream = UnicastProcessor.create(); - FluxProcessor processor = - FluxProcessor.wrap(upstream, upstream); - - StepVerifier.create(processor) - .then(() -> Flux.error(new Exception("test")).subscribe(processor)) - .verifyErrorMessage("test"); - } - - @Test - public void testSubmitSession() throws Exception { - FluxProcessor processor = EmitterProcessor.create(); - AtomicInteger count = new AtomicInteger(); - CountDownLatch latch = new CountDownLatch(1); - Scheduler scheduler = Schedulers.parallel(); - processor.publishOn(scheduler) - .delaySubscription(Duration.ofMillis(1000)) - .limitRate(1) - .subscribe(d -> { - count.incrementAndGet(); - latch.countDown(); - }); - - FluxSink session = processor.sink(); - session.next(1); - //System.out.println(emission); - session.complete(); - - latch.await(5, TimeUnit.SECONDS); - assertThat(count.get()).as("latch").isEqualTo(1); - scheduler.dispose(); - } - - @Test - public void testEmitter() throws Throwable { - FluxProcessor processor = EmitterProcessor.create(); - - int n = 100_000; - int subs = 4; - final CountDownLatch latch = new CountDownLatch((n + 1) * subs); - Scheduler c = Schedulers.single(); - for (int i = 0; i < subs; i++) { - processor.publishOn(c) - .limitRate(1) - .subscribe(d -> latch.countDown(), null, latch::countDown); - } - - FluxSink session = processor.sink(); - - for (int i = 0; i < n; i++) { - while (session.requestedFromDownstream() == 0) { - } - session.next(i); - } - session.complete(); - - boolean waited = latch.await(5, TimeUnit.SECONDS); - assertThat(waited).as("latch : %d", latch.getCount()).isTrue(); - c.dispose(); - } - @Test - public void testEmitter2() throws Throwable { - FluxProcessor processor = EmitterProcessor.create(); - - int n = 100_000; - int subs = 4; - final CountDownLatch latch = new CountDownLatch((n + 1) * subs); - Scheduler c = Schedulers.single(); - for (int i = 0; i < subs; i++) { - processor.publishOn(c) - .doOnComplete(latch::countDown) - .doOnNext(d -> latch.countDown()) - .subscribe(); - } - - FluxSink session = processor.sink(); - - for (int i = 0; i < n; i++) { - while (session.requestedFromDownstream() == 0) { - } - session.next(i); - } - session.complete(); - - boolean waited = latch.await(5, TimeUnit.SECONDS); - assertThat(waited).as("latch : %d", latch.getCount()).isTrue(); - c.dispose(); - } - - @Test - public void serializedConcurrent() { - Scheduler.Worker w1 = Schedulers.boundedElastic().createWorker(); - Scheduler.Worker w2 = Schedulers.boundedElastic().createWorker(); - CountDownLatch latch = new CountDownLatch(1); - CountDownLatch latch2 = new CountDownLatch(1); - AtomicReference ref = new AtomicReference<>(); - - ref.set(Thread.currentThread()); - - DirectProcessor rp = DirectProcessor.create(); - FluxProcessor serialized = rp.serialize(); - - try { - StepVerifier.create(serialized) - .then(() -> { - w1.schedule(() -> serialized.onNext("test1")); - try { - latch2.await(); - } - catch (InterruptedException e) { - fail("Unexpected InterruptedException"); - } - w2.schedule(() -> { - serialized.onNext("test2"); - serialized.onNext("test3"); - serialized.onComplete(); - latch.countDown(); - }); - }) - .assertNext(s -> { - AssertionsForClassTypes.assertThat(s).isEqualTo("test1"); - AssertionsForClassTypes.assertThat(ref.get()).isNotEqualTo(Thread.currentThread()); - ref.set(Thread.currentThread()); - latch2.countDown(); - try { - latch.await(); - } - catch (InterruptedException e) { - fail("Unexpected InterruptedException"); - } - }) - .assertNext(s -> { - assertThat(ref).hasValue(Thread.currentThread()); - AssertionsForClassTypes.assertThat(s).isEqualTo("test2"); - }) - .assertNext(s -> { - assertThat(ref).hasValue(Thread.currentThread()); - AssertionsForClassTypes.assertThat(s).isEqualTo("test3"); - }) - .verifyComplete(); - } - finally { - w1.dispose(); - w2.dispose(); - } - } - - @Test - public void scanProcessor() { - FluxProcessor test = DirectProcessor.create().serialize(); - - assertThat(test.scan(Scannable.Attr.CAPACITY)).isEqualTo(16); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.ERROR)).isNull(); - test.onError(new IllegalStateException("boom")); - assertThat(test.scan(Scannable.Attr.ERROR)).hasMessage("boom"); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - } -} diff --git a/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java index e0c25ec6a3..c1865ad544 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java @@ -19,7 +19,6 @@ import java.lang.ref.WeakReference; import java.time.Duration; import java.util.Date; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -34,11 +33,11 @@ import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.Scannable; -import reactor.test.util.LoggerUtils; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; -import reactor.test.util.TestLogger; import reactor.test.subscriber.AssertSubscriber; +import reactor.test.util.LoggerUtils; +import reactor.test.util.TestLogger; import reactor.util.function.Tuple2; import static org.assertj.core.api.Assertions.assertThat; @@ -652,7 +651,7 @@ void scanProcessorCancelled() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); + test.doCancel(); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @@ -759,29 +758,6 @@ void monoProcessorBlockZeroIsImmediateTimeout() { .isLessThan(Duration.ofMillis(500)); } - @Test - @SuppressWarnings("deprecation") - void toProcessorDisposeBeforeValueSendsCancellationException() { - Mono processor = Mono.never().toProcessor(); - AtomicReference e1 = new AtomicReference<>(); - AtomicReference e2 = new AtomicReference<>(); - AtomicReference e3 = new AtomicReference<>(); - AtomicReference late = new AtomicReference<>(); - - processor.subscribe(v -> Assertions.fail("expected first subscriber to error"), e1::set); - processor.subscribe(v -> Assertions.fail("expected second subscriber to error"), e2::set); - processor.subscribe(v -> Assertions.fail("expected third subscriber to error"), e3::set); - - processor.subscribe().dispose(); - - assertThat(e1.get()).isInstanceOf(CancellationException.class); - assertThat(e2.get()).isInstanceOf(CancellationException.class); - assertThat(e3.get()).isInstanceOf(CancellationException.class); - - processor.subscribe(v -> Assertions.fail("expected late subscriber to error"), late::set); - assertThat(late.get()).isInstanceOf(CancellationException.class); - } - @Test void sharedDisposeBeforeValueDoesNotDisposeProcessor() { AtomicInteger cancellationErrorCount = new AtomicInteger(); @@ -828,7 +804,7 @@ void scanCancelled() { assertThat(sinkCancelled.scan(Scannable.Attr.CANCELLED)).as("pre-cancellation").isFalse(); - ((NextProcessor) sinkCancelled).cancel(); + sinkCancelled.doCancel(); assertThat(sinkCancelled.scan(Scannable.Attr.CANCELLED)).as("cancelled").isTrue(); } diff --git a/reactor-core/src/test/java/reactor/core/publisher/SinkManyBestEffortTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinkManyBestEffortTest.java index 365789ab45..e5145ec213 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/SinkManyBestEffortTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SinkManyBestEffortTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -167,12 +167,11 @@ void scanSink() { void scanInner() { @SuppressWarnings("unchecked") InnerConsumer actual = mock(InnerConsumer.class); - @SuppressWarnings("unchecked") - DirectInnerContainer parent = mock(DirectInnerContainer.class); + SinkManyBestEffort parent = new SinkManyBestEffort<>(false); DirectInner test = new SinkManyBestEffort.DirectInner<>(actual, parent); - assertThat(test.scanUnsafe(Scannable.Attr.PARENT)).isSameAs(parent); //the mock isn't scannable + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scanUnsafe(Scannable.Attr.ACTUAL)).isSameAs(actual); //the mock isn't scannable assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/EmitterProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinkManyEmitterProcessorTest.java similarity index 67% rename from reactor-core/src/test/java/reactor/core/publisher/EmitterProcessorTest.java rename to reactor-core/src/test/java/reactor/core/publisher/SinkManyEmitterProcessorTest.java index f02a13ab6d..8d0f810571 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/EmitterProcessorTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SinkManyEmitterProcessorTest.java @@ -23,17 +23,13 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; -import org.assertj.core.data.Percentage; import org.awaitility.Awaitility; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.reactivestreams.Processor; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -48,7 +44,6 @@ import reactor.test.publisher.TestPublisher; import reactor.test.subscriber.AssertSubscriber; import reactor.test.subscriber.TestSubscriber; -import reactor.test.util.RaceTestUtils; import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; import reactor.util.context.Context; @@ -61,10 +56,9 @@ /** * @author Stephane Maldini + * @author Simon Baslé */ -// This is ok as this class tests the deprecated EmitterProcessor. Will be removed with it in 3.5. -@SuppressWarnings("deprecation") -public class EmitterProcessorTest { +class SinkManyEmitterProcessorTest { @RegisterExtension AutoDisposingExtension afterTest = new AutoDisposingExtension(); @@ -92,7 +86,7 @@ void smokeTestManySubscriber() { testSubscriber1.block(); testSubscriber2.block(); - assertThat(adapter).isInstanceOf(EmitterProcessor.class); + assertThat(adapter).isInstanceOf(SinkManyEmitterProcessor.class); assertThat(testSubscriber1.getReceivedOnNext()).as("ts1 onNexts").containsExactly(9, 10); assertThat(testSubscriber1.isTerminatedComplete()).as("ts1 isTerminatedComplete").isTrue(); @@ -147,8 +141,8 @@ void subscribeToUpstreamTwiceSkipsSecondSubscription() { } @Test - public void currentSubscriberCount() { - Sinks.Many sink = EmitterProcessor.create(); + void currentSubscriberCount() { + Sinks.Many sink = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); assertThat(sink.currentSubscriberCount()).isZero(); @@ -168,7 +162,7 @@ void concurrentSubscriberDisposalDoesntLeak() { Scheduler disposeScheduler = afterTest.autoDispose(Schedulers.newParallel("concurrentSubscriberDisposalDoesntLeak", 5)); List toDisposeInMultipleThreads = new ArrayList<>(); - Sinks.Many sink = EmitterProcessor.create(); + Sinks.Many sink = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); for (int i = 0; i < 10; i++) { toDisposeInMultipleThreads.add( @@ -186,8 +180,8 @@ void concurrentSubscriberDisposalDoesntLeak() { //see https://github.com/reactor/reactor-core/issues/1364 @Test - public void subscribeWithSyncFusionUpstreamFirst() { - EmitterProcessor processor = EmitterProcessor.create(16); + void subscribeWithSyncFusionUpstreamFirst() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, 16); StepVerifier.create( Mono.just("DATA") @@ -203,8 +197,8 @@ public void subscribeWithSyncFusionUpstreamFirst() { //see https://github.com/reactor/reactor-core/issues/1290 @Test - public void subscribeWithSyncFusionSingle() { - Processor processor = EmitterProcessor.create(16); + void subscribeWithSyncFusionSingle() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, 16); StepVerifier.create(processor) .then(() -> Flux.just(1).subscribe(processor)) @@ -215,8 +209,8 @@ public void subscribeWithSyncFusionSingle() { //see https://github.com/reactor/reactor-core/issues/1290 @Test - public void subscribeWithSyncFusionMultiple() { - Processor processor = EmitterProcessor.create(16); + void subscribeWithSyncFusionMultiple() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, 16); StepVerifier.create(processor) .then(() -> Flux.range(1, 5).subscribe(processor)) @@ -227,8 +221,8 @@ public void subscribeWithSyncFusionMultiple() { //see https://github.com/reactor/reactor-core/issues/1290 @Test - public void subscribeWithAsyncFusion() { - Processor processor = EmitterProcessor.create(16); + void subscribeWithAsyncFusion() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, 16); StepVerifier.create(processor) .then(() -> Flux.range(1, 5).publishOn(Schedulers.boundedElastic()).subscribe(processor)) @@ -238,11 +232,11 @@ public void subscribeWithAsyncFusion() { } @Test - public void emitNextNullWithAsyncFusion() { - EmitterProcessor processor = EmitterProcessor.create(); + void emitNextNullWithAsyncFusion() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); // Any `QueueSubscription` capable of doing ASYNC fusion - Fuseable.QueueSubscription queueSubscription = UnicastProcessor.create(); + Fuseable.QueueSubscription queueSubscription = SinkManyUnicast.create(); processor.onSubscribe(queueSubscription); // Expect the ASYNC fusion mode @@ -253,11 +247,11 @@ public void emitNextNullWithAsyncFusion() { } @Test - public void testColdIdentityProcessor() throws InterruptedException { + void testColdIdentityProcessor() throws InterruptedException { final int elements = 10; CountDownLatch latch = new CountDownLatch(elements + 1); - Processor processor = EmitterProcessor.create(16); + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, 16); List list = new ArrayList<>(); @@ -308,92 +302,37 @@ public void onComplete() { } - /*@Test - public void test100Hot() throws InterruptedException { - for (int i = 0; i < 10000; i++) { - testHotIdentityProcessor(); - } - } -*/ @Test - public void testHotIdentityProcessor() throws InterruptedException { - final int elements = 10000; - CountDownLatch latch = new CountDownLatch(elements); - - Processor processor = EmitterProcessor.create(1024); - - EmitterProcessor stream = EmitterProcessor.create(); - FluxSink session = stream.sink(); - stream.subscribe(processor); - - processor.subscribe(new CoreSubscriber() { - @Override - public void onSubscribe(Subscription s) { - s.request(elements); - } - - @Override - public void onNext(Integer integer) { - latch.countDown(); - } - - @Override - public void onError(Throwable t) { - System.out.println("error! " + t); - } - - @Override - public void onComplete() { - System.out.println("completed!"); - //latch.countDown(); - } - }); - - for (int i = 0; i < elements; i++) { - session.next(i); - } - //stream.then(); - - latch.await(8, TimeUnit.SECONDS); - - long count = latch.getCount(); - assertThat(latch.getCount()).as("Count > 0 : %d, Running on %d CPUs", count, DEFAULT_POOL_SIZE).isEqualTo(0); - - stream.onComplete(); - - } - - @Test - public void onNextNull() { + void onNextNull() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - EmitterProcessor.create().onNext(null); + new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE).onNext(null); }); } @Test - public void onErrorNull() { + void onErrorNull() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - EmitterProcessor.create().onError(null); + new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE).onError(null); }); } @Test - public void onSubscribeNull() { + void onSubscribeNull() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - EmitterProcessor.create().onSubscribe(null); + new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE).onSubscribe(null); }); } @Test - public void subscribeNull() { + void subscribeNull() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - EmitterProcessor.create().subscribe((Subscriber) null); + new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE).subscribe((Subscriber) null); }); } @Test - public void normal() { - EmitterProcessor tp = EmitterProcessor.create(); + void normal() { + SinkManyEmitterProcessor tp = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); StepVerifier.create(tp) .then(() -> { assertThat(tp.currentSubscriberCount()).as("has subscriber").isPositive(); @@ -419,8 +358,8 @@ public void normal() { } @Test - public void normalBackpressured() { - EmitterProcessor tp = EmitterProcessor.create(); + void normalBackpressured() { + SinkManyEmitterProcessor tp = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); StepVerifier.create(tp, 0L) .then(() -> { assertThat(tp.currentSubscriberCount()).as("has subscriber").isPositive(); @@ -443,8 +382,8 @@ public void normalBackpressured() { } @Test - public void normalAtomicRingBufferBackpressured() { - EmitterProcessor tp = EmitterProcessor.create(100); + void normalAtomicRingBufferBackpressured() { + SinkManyEmitterProcessor tp = new SinkManyEmitterProcessor<>(true,100); StepVerifier.create(tp, 0L) .then(() -> { assertThat(tp.currentSubscriberCount()).as("has subscriber").isPositive(); @@ -466,94 +405,38 @@ public void normalAtomicRingBufferBackpressured() { assertThat(tp.getError()).as("getError").isNull(); } - @Test - public void state(){ - EmitterProcessor tp = EmitterProcessor.create(); - assertThat(tp.getPending()).isEqualTo(0); - assertThat(tp.getBufferSize()).isEqualTo(Queues.SMALL_BUFFER_SIZE); - assertThat(tp.isCancelled()).isFalse(); - - assertThat(tp.inners()).isEmpty(); - assertThat(tp.currentSubscriberCount()).as("has subscriber").isZero(); - - Disposable d1 = tp.subscribe(); - assertThat(tp.inners()).hasSize(1); - assertThat(tp.currentSubscriberCount()).isPositive(); - - FluxSink s = tp.sink(); - - s.next(2); - s.next(3); - s.next(4); - assertThat(tp.getPending()).isEqualTo(0); - AtomicReference d2 = new AtomicReference<>(); - tp.subscribe(new CoreSubscriber() { - @Override - public void onSubscribe(Subscription s) { - d2.set(s); - } - - @Override - public void onNext(Integer integer) { - - } - - @Override - public void onError(Throwable t) { - - } - - @Override - public void onComplete() { - - } - }); - s.next(5); - s.next(6); - s.next(7); - assertThat(tp.scan(BUFFERED)).isEqualTo(3); - assertThat(tp.isTerminated()).isFalse(); - s.complete(); - assertThat(tp.isTerminated()).isFalse(); - d1.dispose(); - d2.get().cancel(); - assertThat(tp.isTerminated()).isTrue(); - - StepVerifier.create(tp) - .verifyComplete(); - - tp.onNext(8); //noop - EmitterProcessor empty = EmitterProcessor.create(); - empty.onComplete(); - assertThat(empty.isTerminated()).isTrue(); + @Test + void failZeroBufferSize() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new SinkManyEmitterProcessor<>(true, 0)) + .withMessage("bufferSize must be strictly positive, was: 0"); } - @Test - public void failNullBufferSize() { - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> { - EmitterProcessor.create(0); - }); + void failNegativeBufferSize() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new SinkManyEmitterProcessor<>(true, -1)) + .withMessage("bufferSize must be strictly positive, was: -1"); } @Test - public void failNullNext() { + void failNullTryEmitNext() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - EmitterProcessor.create().onNext(null); - }); + new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE).tryEmitNext(null); + }) + .withMessage("tryEmitNext must be invoked with a non-null value"); } @Test - public void failNullError() { + void failNullTryEmitError() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { - EmitterProcessor.create().onError(null); - }); + new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE).tryEmitError(null); + }) + .withMessage("tryEmitError must be invoked with a non-null Throwable"); } @Test - public void failDoubleError() { - EmitterProcessor ep = EmitterProcessor.create(); + void failDoubleError() { + SinkManyEmitterProcessor ep = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); StepVerifier.create(ep) .then(() -> { assertThat(ep.getError()).isNull(); @@ -567,8 +450,8 @@ public void failDoubleError() { } @Test - public void failCompleteThenError() { - EmitterProcessor ep = EmitterProcessor.create(); + void failCompleteThenError() { + SinkManyEmitterProcessor ep = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); StepVerifier.create(ep) .then(() -> { ep.onComplete(); @@ -580,20 +463,6 @@ public void failCompleteThenError() { .hasDroppedErrorWithMessage("test"); } - @Test - public void ignoreDoubleOnSubscribe() { - EmitterProcessor ep = EmitterProcessor.create(); - ep.sink(); - assertThat(ep.sink().isCancelled()).isTrue(); - } - - @Test - public void failNegativeBufferSize() { - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> { - EmitterProcessor.create(-1); - }); - } - static final List DATA = new ArrayList<>(); static final int MAX_SIZE = 100; @@ -604,8 +473,8 @@ public void failNegativeBufferSize() { } @Test - public void testRed() { - EmitterProcessor processor = EmitterProcessor.create(); + void testRed() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); AssertSubscriber subscriber = AssertSubscriber.create(1); processor.subscribe(subscriber); @@ -617,8 +486,8 @@ public void testRed() { } @Test - public void testGreen() { - EmitterProcessor processor = EmitterProcessor.create(); + void testGreen() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); AssertSubscriber subscriber = AssertSubscriber.create(1); processor.subscribe(subscriber); @@ -631,8 +500,8 @@ public void testGreen() { } @Test - public void testHanging() { - EmitterProcessor processor = EmitterProcessor.create(2); + void testHanging() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, 2); AssertSubscriber first = AssertSubscriber.create(0); processor.log("after-1").subscribe(first); @@ -659,8 +528,8 @@ public void testHanging() { } @Test - public void testNPE() { - EmitterProcessor processor = EmitterProcessor.create(8); + void testNPE() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, 8); AssertSubscriber first = AssertSubscriber.create(1); processor.log().take(1, false).subscribe(first); @@ -697,7 +566,7 @@ public void uncaughtException(Thread t, Throwable e) { } - public MyThread(EmitterProcessor processor, CyclicBarrier barrier, int n, int index) { + public MyThread(SinkManyEmitterProcessor processor, CyclicBarrier barrier, int n, int index) { this.processor = processor.log("consuming."+index); this.barrier = barrier; this.n = n; @@ -737,11 +606,11 @@ public Throwable getLastException() { @Test @Disabled - public void testRacing() throws Exception { + void testRacing() throws Exception { int N_THREADS = 3; int N_ITEMS = 8; - EmitterProcessor processor = EmitterProcessor.create(4); + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, 4); List data = new ArrayList<>(); for (int i = 1; i <= N_ITEMS; i++) { data.add(String.valueOf(i)); @@ -771,7 +640,7 @@ public void testRacing() throws Exception { } @Test - public void testThreadAffinity() throws InterruptedException { + void testThreadAffinity() throws InterruptedException { int count = 10; Scheduler[] schedulers = new Scheduler[4]; CountDownLatch[] latches = new CountDownLatch[schedulers.length]; @@ -780,7 +649,7 @@ public void testThreadAffinity() throws InterruptedException { int expectedCount = i == 1 ? count * 2 : count; latches[i] = new CountDownLatch(expectedCount); } - EmitterProcessor processor = EmitterProcessor.create(); + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); processor.publishOn(schedulers[0]) .share(); processor.publishOn(schedulers[1]) @@ -812,36 +681,36 @@ public void testThreadAffinity() throws InterruptedException { } @Test - public void createDefault() { - EmitterProcessor processor = EmitterProcessor.create(); + void createDefault() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); assertProcessor(processor, null, null); } @Test - public void createOverrideBufferSize() { + void createOverrideBufferSize() { int bufferSize = 1024; - EmitterProcessor processor = EmitterProcessor.create(bufferSize); + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, bufferSize); assertProcessor(processor, bufferSize, null); } @Test - public void createOverrideAutoCancel() { + void createOverrideAutoCancel() { boolean autoCancel = false; - EmitterProcessor processor = EmitterProcessor.create(autoCancel); + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(autoCancel, Queues.SMALL_BUFFER_SIZE); assertProcessor(processor, null, autoCancel); } @Test - public void createOverrideAll() { + void createOverrideAll() { int bufferSize = 1024; boolean autoCancel = false; - EmitterProcessor processor = EmitterProcessor.create(bufferSize, autoCancel); + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(autoCancel, bufferSize); assertProcessor(processor, bufferSize, autoCancel); } - public void assertProcessor(EmitterProcessor processor, - @Nullable Integer bufferSize, - @Nullable Boolean autoCancel) { + void assertProcessor(SinkManyEmitterProcessor processor, + @Nullable Integer bufferSize, + @Nullable Boolean autoCancel) { int expectedBufferSize = bufferSize != null ? bufferSize : Queues.SMALL_BUFFER_SIZE; boolean expectedAutoCancel = autoCancel != null ? autoCancel : true; @@ -849,43 +718,20 @@ public void assertProcessor(EmitterProcessor processor, assertThat(processor.autoCancel).isEqualTo(expectedAutoCancel); } - /** - * Concurrent subtraction bound to 0 and Long.MAX_VALUE. - * Any concurrent write will "happen" before this operation. - * - * @param sequence current atomic to update - * @param toSub delta to sub - * @return value before subscription, 0 or Long.MAX_VALUE - */ - public static long getAndSub(AtomicLong sequence, long toSub) { - long r, u; - do { - r = sequence.get(); - if (r == 0 || r == Long.MAX_VALUE) { - return r; - } - u = Operators.subOrZero(r, toSub); - } while (!sequence.compareAndSet(r, u)); - - return r; - } - @Test - public void scanMain() { - EmitterProcessor test = EmitterProcessor.create(123); + void scanMain() { + SinkManyEmitterProcessor test = new SinkManyEmitterProcessor<>(true, 123); assertThat(test.scan(BUFFERED)).isEqualTo(0); assertThat(test.scan(CANCELLED)).isFalse(); assertThat(test.scan(PREFETCH)).isEqualTo(123); assertThat(test.scan(CAPACITY)).isEqualTo(123); - Disposable d1 = test.subscribe(); - FluxSink sink = test.sink(); + test.tryEmitNext(2).orThrow(); + test.tryEmitNext(3).orThrow(); + test.tryEmitNext(4).orThrow(); - sink.next(2); - sink.next(3); - sink.next(4); assertThat(test.scan(BUFFERED)).isEqualTo(0); AtomicReference d2 = new AtomicReference<>(); @@ -910,14 +756,15 @@ public void onComplete() { } }); - sink.next(5); - sink.next(6); - sink.next(7); + test.tryEmitNext(5).orThrow(); + test.tryEmitNext(6).orThrow(); + test.tryEmitNext(7).orThrow(); assertThat(test.scan(BUFFERED)).isEqualTo(3); assertThat(test.scan(TERMINATED)).isFalse(); - sink.complete(); + test.tryEmitComplete().orThrow(); + assertThat(test.scan(TERMINATED)).isFalse(); d1.dispose(); @@ -930,8 +777,8 @@ public void onComplete() { } @Test - public void scanMainCancelled() { - EmitterProcessor test = EmitterProcessor.create(true); + void scanMainCancelled() { + SinkManyEmitterProcessor test = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); test.subscribe().dispose(); assertThat(test.scan(CANCELLED)).isTrue(); @@ -939,17 +786,17 @@ public void scanMainCancelled() { } @Test - public void scanMainError() { - EmitterProcessor test = EmitterProcessor.create(); - test.sink().error(new IllegalStateException("boom")); + void scanMainError() { + SinkManyEmitterProcessor test = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); + test.tryEmitError(new IllegalStateException("boom")).orThrow(); assertThat(test.scan(TERMINATED)).as("terminated").isTrue(); assertThat(test.scan(Attr.ERROR)).hasMessage("boom"); } @Test - public void inners() { - Sinks.Many sink = EmitterProcessor.create(); + void inners() { + Sinks.Many sink = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); CoreSubscriber notScannable = new BaseSubscriber() {}; InnerConsumer scannable = new LambdaSubscriber<>(null, null, null, null); @@ -962,15 +809,15 @@ public void inners() { .asList() .as("after subscriptions") .hasSize(2) - .extracting(l -> (Object) ((EmitterProcessor.EmitterInner) l).actual) + .extracting(l -> (Object) ((SinkManyEmitterProcessor.EmitterInner) l).actual) .containsExactly(notScannable, scannable); } //see https://github.com/reactor/reactor-core/issues/1528 @Test - public void syncFusionFromInfiniteStream() { + void syncFusionFromInfiniteStream() { final Flux flux = Flux.fromStream(Stream.iterate(0, i -> i + 1)); - final EmitterProcessor processor = EmitterProcessor.create(); + final SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); StepVerifier.create(processor) .then(() -> flux.subscribe(processor)) @@ -982,10 +829,10 @@ public void syncFusionFromInfiniteStream() { //see https://github.com/reactor/reactor-core/issues/1528 @Test - public void syncFusionFromInfiniteStreamAndTake() { + void syncFusionFromInfiniteStreamAndTake() { final Flux flux = Flux.fromStream(Stream.iterate(0, i -> i + 1)) .take(10, false); - final EmitterProcessor processor = EmitterProcessor.create(); + final SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); flux.subscribe(processor); StepVerifier.create(processor) @@ -995,10 +842,10 @@ public void syncFusionFromInfiniteStreamAndTake() { } @Test - public void removeUnknownInnerIgnored() { - EmitterProcessor processor = EmitterProcessor.create(); - EmitterProcessor.EmitterInner inner = new EmitterProcessor.EmitterInner<>(null, processor); - EmitterProcessor.EmitterInner notInner = new EmitterProcessor.EmitterInner<>(null, processor); + void removeUnknownInnerIgnored() { + SinkManyEmitterProcessor processor = new SinkManyEmitterProcessor<>(true, Queues.SMALL_BUFFER_SIZE); + SinkManyEmitterProcessor.EmitterInner inner = new SinkManyEmitterProcessor.EmitterInner<>(null, processor); + SinkManyEmitterProcessor.EmitterInner notInner = new SinkManyEmitterProcessor.EmitterInner<>(null, processor); processor.add(inner); assertThat(processor.subscribers).as("adding inner").hasSize(1); @@ -1011,65 +858,65 @@ public void removeUnknownInnerIgnored() { } @Test - public void currentContextDelegatesToFirstSubscriber() { + void currentContextDelegatesToFirstSubscriber() { AssertSubscriber testSubscriber1 = new AssertSubscriber<>(Context.of("key", "value1")); AssertSubscriber testSubscriber2 = new AssertSubscriber<>(Context.of("key", "value2")); - EmitterProcessor emitterProcessor = new EmitterProcessor<>(false, 1); - emitterProcessor.subscribe(testSubscriber1); - emitterProcessor.subscribe(testSubscriber2); + SinkManyEmitterProcessor sinkManyEmitterProcessor = new SinkManyEmitterProcessor<>(false, 1); + sinkManyEmitterProcessor.subscribe(testSubscriber1); + sinkManyEmitterProcessor.subscribe(testSubscriber2); - Context processorContext = emitterProcessor.currentContext(); + Context processorContext = sinkManyEmitterProcessor.currentContext(); assertThat(processorContext.getOrDefault("key", "EMPTY")).isEqualTo("value1"); } @Test - public void tryEmitNextWithNoSubscriberFailsOnlyIfNoCapacity() { - EmitterProcessor emitterProcessor = EmitterProcessor.create(1); + void tryEmitNextWithNoSubscriberFailsOnlyIfNoCapacity() { + SinkManyEmitterProcessor sinkManyEmitterProcessor = new SinkManyEmitterProcessor<>(true, 1); - assertThat(emitterProcessor.tryEmitNext(1)).isEqualTo(Sinks.EmitResult.OK); - assertThat(emitterProcessor.tryEmitNext(2)).isEqualTo(Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER); + assertThat(sinkManyEmitterProcessor.tryEmitNext(1)).isEqualTo(Sinks.EmitResult.OK); + assertThat(sinkManyEmitterProcessor.tryEmitNext(2)).isEqualTo(Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER); - StepVerifier.create(emitterProcessor) + StepVerifier.create(sinkManyEmitterProcessor) .expectNext(1) - .then(() -> emitterProcessor.tryEmitComplete().orThrow()) + .then(() -> sinkManyEmitterProcessor.tryEmitComplete().orThrow()) .verifyComplete(); } @Test - public void tryEmitNextWithNoSubscriberFailsIfNoCapacityAndAllSubscribersCancelledAndNoAutoTermination() { + void tryEmitNextWithNoSubscriberFailsIfNoCapacityAndAllSubscribersCancelledAndNoAutoTermination() { //in case of autoCancel, removing all subscribers results in TERMINATED rather than EMPTY - EmitterProcessor emitterProcessor = EmitterProcessor.create(1, false); + SinkManyEmitterProcessor sinkManyEmitterProcessor = new SinkManyEmitterProcessor<>(false, 1); AssertSubscriber testSubscriber = AssertSubscriber.create(); - emitterProcessor.subscribe(testSubscriber); + sinkManyEmitterProcessor.subscribe(testSubscriber); - assertThat(emitterProcessor.tryEmitNext(1)).as("emit 1, with subscriber").isEqualTo( + assertThat(sinkManyEmitterProcessor.tryEmitNext(1)).as("emit 1, with subscriber").isEqualTo( Sinks.EmitResult.OK); - assertThat(emitterProcessor.tryEmitNext(2)).as("emit 2, with subscriber").isEqualTo( + assertThat(sinkManyEmitterProcessor.tryEmitNext(2)).as("emit 2, with subscriber").isEqualTo( Sinks.EmitResult.OK); - assertThat(emitterProcessor.tryEmitNext(3)).as("emit 3, with subscriber").isEqualTo( + assertThat(sinkManyEmitterProcessor.tryEmitNext(3)).as("emit 3, with subscriber").isEqualTo( Sinks.EmitResult.OK); testSubscriber.cancel(); - assertThat(emitterProcessor.tryEmitNext(4)).as("emit 4, without subscriber, buffered").isEqualTo( + assertThat(sinkManyEmitterProcessor.tryEmitNext(4)).as("emit 4, without subscriber, buffered").isEqualTo( Sinks.EmitResult.OK); - assertThat(emitterProcessor.tryEmitNext(5)).as("emit 5, without subscriber").isEqualTo( + assertThat(sinkManyEmitterProcessor.tryEmitNext(5)).as("emit 5, without subscriber").isEqualTo( Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER); } @Test - public void emitNextWithNoSubscriberNoCapacityKeepsSinkOpenWithBuffer() { - EmitterProcessor emitterProcessor = EmitterProcessor.create(1, false); + void emitNextWithNoSubscriberNoCapacityKeepsSinkOpenWithBuffer() { + SinkManyEmitterProcessor sinkManyEmitterProcessor = new SinkManyEmitterProcessor<>(false, 1); //fill the buffer - assertThat(emitterProcessor.tryEmitNext(1)).as("filling buffer").isEqualTo(Sinks.EmitResult.OK); + assertThat(sinkManyEmitterProcessor.tryEmitNext(1)).as("filling buffer").isEqualTo(Sinks.EmitResult.OK); //test proper //this is "discarded" but no hook can be invoked, so effectively dropped on the floor - emitterProcessor.emitNext(2, FAIL_FAST); + sinkManyEmitterProcessor.emitNext(2, FAIL_FAST); - StepVerifier.create(emitterProcessor) + StepVerifier.create(sinkManyEmitterProcessor) .expectNext(1) .expectTimeout(Duration.ofSeconds(1)) .verify(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/ReplayProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinkManyReplayProcessorTest.java similarity index 81% rename from reactor-core/src/test/java/reactor/core/publisher/ReplayProcessorTest.java rename to reactor-core/src/test/java/reactor/core/publisher/SinkManyReplayProcessorTest.java index 8e434399e9..3fb0a86a3b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ReplayProcessorTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SinkManyReplayProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,20 @@ import java.time.Duration; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.assertj.core.api.Assertions; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.Fuseable; import reactor.core.Scannable; -import reactor.test.util.LoggerUtils; import reactor.test.StepVerifier; import reactor.test.scheduler.VirtualTimeScheduler; import reactor.test.subscriber.AssertSubscriber; +import reactor.test.util.LoggerUtils; import reactor.test.util.TestLogger; import static org.assertj.core.api.Assertions.assertThat; @@ -39,7 +39,7 @@ // This is ok as this class tests the deprecated ReplayProcessor. Will be removed with it in 3.5. @SuppressWarnings("deprecation") -public class ReplayProcessorTest { +public class SinkManyReplayProcessorTest { @BeforeEach public void virtualTime() { @@ -53,7 +53,7 @@ public void teardownVirtualTime() { @Test public void currentSubscriberCount() { - Sinks.Many sink = ReplayProcessor.create(); + Sinks.Many sink = SinkManyReplayProcessor.create(); assertThat(sink.currentSubscriberCount()).isZero(); @@ -68,7 +68,7 @@ public void currentSubscriberCount() { @Test public void unbounded() { - ReplayProcessor rp = ReplayProcessor.create(16, true); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, true); AssertSubscriber ts = AssertSubscriber.create(0L); @@ -96,7 +96,7 @@ public void unbounded() { @Test public void bounded() { - ReplayProcessor rp = ReplayProcessor.create(16, false); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, false); AssertSubscriber ts = AssertSubscriber.create(0L); @@ -124,7 +124,7 @@ public void bounded() { @Test public void cancel() { - ReplayProcessor rp = ReplayProcessor.create(16, false); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, false); AssertSubscriber ts = AssertSubscriber.create(); @@ -137,7 +137,7 @@ public void cancel() { @Test public void unboundedAfter() { - ReplayProcessor rp = ReplayProcessor.create(16, true); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, true); AssertSubscriber ts = AssertSubscriber.create(0L); @@ -165,7 +165,7 @@ public void unboundedAfter() { @Test public void boundedAfter() { - ReplayProcessor rp = ReplayProcessor.create(16, false); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, false); AssertSubscriber ts = AssertSubscriber.create(0L); @@ -193,7 +193,7 @@ public void boundedAfter() { @Test public void unboundedLong() { - ReplayProcessor rp = ReplayProcessor.create(16, true); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, true); AssertSubscriber ts = AssertSubscriber.create(0L); @@ -217,7 +217,7 @@ public void unboundedLong() { @Test public void boundedLong() { - ReplayProcessor rp = ReplayProcessor.create(16, false); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, false); for (int i = 0; i < 256; i++) { rp.onNext(i); } @@ -229,7 +229,7 @@ public void boundedLong() { @Test public void boundedLongError() { - ReplayProcessor rp = ReplayProcessor.create(16, false); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, false); for (int i = 0; i < 256; i++) { rp.onNext(i); } @@ -241,7 +241,7 @@ public void boundedLongError() { @Test public void unboundedFused() { - ReplayProcessor rp = ReplayProcessor.create(16, true); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, true); for (int i = 0; i < 256; i++) { rp.onNext(i); } @@ -254,7 +254,7 @@ public void unboundedFused() { @Test public void unboundedFusedError() { - ReplayProcessor rp = ReplayProcessor.create(16, true); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, true); for (int i = 0; i < 256; i++) { rp.onNext(i); } @@ -267,7 +267,7 @@ public void unboundedFusedError() { @Test public void boundedFused() { - ReplayProcessor rp = ReplayProcessor.create(16, false); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, false); for (int i = 0; i < 256; i++) { rp.onNext(i); } @@ -280,7 +280,7 @@ public void boundedFused() { @Test public void boundedFusedError() { - ReplayProcessor rp = ReplayProcessor.create(16, false); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, false); for (int i = 0; i < 256; i++) { rp.onNext(i); } @@ -293,7 +293,7 @@ public void boundedFusedError() { @Test public void boundedFusedAfter() { - ReplayProcessor rp = ReplayProcessor.create(16, false); + SinkManyReplayProcessor rp = SinkManyReplayProcessor.create(16, false); StepVerifier.create(rp) .expectFusion(Fuseable.ASYNC) @@ -311,8 +311,8 @@ public void boundedFusedAfter() { public void timed() throws Exception { VirtualTimeScheduler.getOrSet(); - ReplayProcessor rp = - ReplayProcessor.createTimeout(Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createTimeout(Duration.ofSeconds(1)); for (int i = 0; i < 5; i++) { rp.onNext(i); @@ -335,8 +335,8 @@ public void timed() throws Exception { public void timedError() throws Exception { VirtualTimeScheduler.getOrSet(); - ReplayProcessor rp = - ReplayProcessor.createTimeout(Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createTimeout(Duration.ofSeconds(1)); for (int i = 0; i < 5; i++) { rp.onNext(i); @@ -358,8 +358,8 @@ public void timedError() throws Exception { @Test public void timedAfter() throws Exception { - ReplayProcessor rp = - ReplayProcessor.createTimeout(Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createTimeout(Duration.ofSeconds(1)); StepVerifier.create(rp.hide()) .expectFusion(Fuseable.NONE) @@ -383,8 +383,8 @@ public void timedAfter() throws Exception { public void timedFused() throws Exception { VirtualTimeScheduler.getOrSet(); - ReplayProcessor rp = - ReplayProcessor.createTimeout(Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createTimeout(Duration.ofSeconds(1)); for (int i = 0; i < 5; i++) { @@ -408,8 +408,8 @@ public void timedFused() throws Exception { public void timedFusedError() throws Exception { VirtualTimeScheduler.getOrSet(); - ReplayProcessor rp = - ReplayProcessor.createTimeout(Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createTimeout(Duration.ofSeconds(1)); for (int i = 0; i < 5; i++) { @@ -431,8 +431,8 @@ public void timedFusedError() throws Exception { @Test public void timedFusedAfter() throws Exception { - ReplayProcessor rp = - ReplayProcessor.createTimeout(Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createTimeout(Duration.ofSeconds(1)); StepVerifier.create(rp) .expectFusion(Fuseable.NONE) @@ -454,8 +454,8 @@ public void timedFusedAfter() throws Exception { @Test public void timedAndBound() throws Exception { - ReplayProcessor rp = - ReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); for (int i = 0; i < 10; i++) { @@ -479,8 +479,8 @@ public void timedAndBound() throws Exception { @Test public void timedAndBoundError() throws Exception { - ReplayProcessor rp = - ReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); for (int i = 0; i < 10; i++) { @@ -504,8 +504,8 @@ public void timedAndBoundError() throws Exception { @Test public void timedAndBoundAfter() throws Exception { - ReplayProcessor rp = - ReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); StepVerifier.create(rp.hide()) .expectFusion(Fuseable.NONE) @@ -529,8 +529,8 @@ public void timedAndBoundAfter() throws Exception { @Test public void timedAndBoundFused() throws Exception { - ReplayProcessor rp = - ReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); for (int i = 0; i < 10; i++) { @@ -554,8 +554,8 @@ public void timedAndBoundFused() throws Exception { @Test public void timedAndBoundFusedError() throws Exception { - ReplayProcessor rp = - ReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); + SinkManyReplayProcessor rp = + SinkManyReplayProcessor.createSizeAndTimeout(5, Duration.ofSeconds(1)); for (int i = 0; i < 10; i++) { @@ -579,26 +579,26 @@ public void timedAndBoundFusedError() throws Exception { @Test public void timedAndBoundedOnSubscribeAndState() { - testReplayProcessorState(ReplayProcessor.createSizeAndTimeout(1, Duration.ofSeconds(1))); + testReplayProcessorState(SinkManyReplayProcessor.createSizeAndTimeout(1, Duration.ofSeconds(1))); } @Test public void timedOnSubscribeAndState() { - testReplayProcessorState(ReplayProcessor.createTimeout(Duration.ofSeconds(1))); + testReplayProcessorState(SinkManyReplayProcessor.createTimeout(Duration.ofSeconds(1))); } @Test public void unboundedOnSubscribeAndState() { - testReplayProcessorState(ReplayProcessor.create(1, true)); + testReplayProcessorState(SinkManyReplayProcessor.create(1, true)); } @Test public void boundedOnSubscribeAndState() { - testReplayProcessorState(ReplayProcessor.cacheLast()); + testReplayProcessorState(SinkManyReplayProcessor.cacheLast()); } @SuppressWarnings("unchecked") - void testReplayProcessorState(ReplayProcessor rp) { + void testReplayProcessorState(SinkManyReplayProcessor rp) { TestLogger testLogger = new TestLogger(); LoggerUtils.enableCaptureWith(testLogger); try { @@ -606,7 +606,7 @@ void testReplayProcessorState(ReplayProcessor rp) { rp.subscribe(); - ReplayProcessor.ReplayInner s = ((ReplayProcessor.ReplayInner) rp.inners() + SinkManyReplayProcessor.ReplayInner s = ((SinkManyReplayProcessor.ReplayInner) rp.inners() .findFirst() .get()); @@ -617,11 +617,7 @@ void testReplayProcessorState(ReplayProcessor rp) { assertThat(s.isCancelled()).isFalse(); assertThat(rp.getPrefetch()).isEqualTo(Integer.MAX_VALUE); - if (rp.getBufferSize() != Integer.MAX_VALUE) { - assertThat(rp.getBufferSize()).isEqualTo(1); - } - FluxSink sink = rp.sink(); - sink.next("test"); + rp.tryEmitNext("test").orThrow(); rp.onComplete(); rp.onComplete(); @@ -640,20 +636,20 @@ void testReplayProcessorState(ReplayProcessor rp) { @Test public void failNegativeBufferSizeBounded() { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> { - ReplayProcessor.create(-1); + SinkManyReplayProcessor.create(-1); }); } @Test public void failNegativeBufferBoundedAndTimed() { assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> { - ReplayProcessor.createSizeAndTimeout(-1, Duration.ofSeconds(1)); + SinkManyReplayProcessor.createSizeAndTimeout(-1, Duration.ofSeconds(1)); }); } @Test public void scanProcessor() { - ReplayProcessor test = ReplayProcessor.create(16, false); + SinkManyReplayProcessor test = SinkManyReplayProcessor.create(16, false); Subscription subscription = Operators.emptySubscription(); test.onSubscribe(subscription); @@ -670,13 +666,13 @@ public void scanProcessor() { @Test public void scanProcessorUnboundedCapacity() { - ReplayProcessor test = ReplayProcessor.create(16, true); + SinkManyReplayProcessor test = SinkManyReplayProcessor.create(16, true); assertThat(test.scan(Scannable.Attr.CAPACITY)).isEqualTo(Integer.MAX_VALUE); } @Test public void inners() { - Sinks.Many sink = ReplayProcessor.create(1); + Sinks.Many sink = SinkManyReplayProcessor.create(1); CoreSubscriber notScannable = new BaseSubscriber() {}; InnerConsumer scannable = new LambdaSubscriber<>(null, null, null, null); @@ -689,7 +685,7 @@ public void inners() { .asList() .as("after subscriptions") .hasSize(2) - .extracting(l -> (Object) ((ReplayProcessor.ReplayInner) l).actual) + .extracting(l -> (Object) ((SinkManyReplayProcessor.ReplayInner) l).actual) .containsExactly(notScannable, scannable); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/UnicastManySinkNoBackpressureTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastNoBackpressureTest.java similarity index 82% rename from reactor-core/src/test/java/reactor/core/publisher/UnicastManySinkNoBackpressureTest.java rename to reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastNoBackpressureTest.java index 2df1ddd6b9..ce753fe8dc 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/UnicastManySinkNoBackpressureTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastNoBackpressureTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,11 +28,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST; -class UnicastManySinkNoBackpressureTest { +class SinkManyUnicastNoBackpressureTest { @Test void currentSubscriberCount() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); assertThat(sink.currentSubscriberCount()).isZero(); @@ -43,25 +43,25 @@ void currentSubscriberCount() { @Test void noSubscribers() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); assertThat(sink.tryEmitNext("hi")).isEqualTo(EmitResult.FAIL_ZERO_SUBSCRIBER); } @Test void noSubscribersTryError() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); assertThat(sink.tryEmitError(new NullPointerException())).isEqualTo(EmitResult.FAIL_ZERO_SUBSCRIBER); } @Test void noSubscribersTryComplete() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); assertThat(sink.tryEmitComplete()).isEqualTo(EmitResult.FAIL_ZERO_SUBSCRIBER); } @Test void noRequest() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); StepVerifier.create(sink.asFlux(), 0) .then(() -> { @@ -73,7 +73,7 @@ void noRequest() { @Test void singleRequest() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); StepVerifier.create(sink.asFlux(), 1) .then(() -> { @@ -90,7 +90,7 @@ void singleRequest() { @Test void cancelled() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); StepVerifier.create(sink.asFlux(), 0).thenCancel().verify(); @@ -99,7 +99,7 @@ void cancelled() { @Test void completed() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); sink.asFlux().subscribe(); sink.tryEmitComplete().orThrow(); @@ -108,7 +108,7 @@ void completed() { @Test void errored() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); sink.asFlux().subscribe(v -> {}, e -> {}); sink.tryEmitError(new IllegalArgumentException("boom")).orThrow(); @@ -117,7 +117,7 @@ void errored() { @Test void beforeSubscriberEmitNextIsIgnoredKeepsSinkOpen() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); sink.emitNext("hi", FAIL_FAST); @@ -134,7 +134,7 @@ void beforeSubscriberEmitNextIsIgnoredKeepsSinkOpen() { @Test void scanTerminatedCancelled() { - Sinks.Many sink = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink = SinkManyUnicastNoBackpressure.create(); sink.asFlux().subscribe(); assertThat(sink.scan(Scannable.Attr.TERMINATED)).as("not yet terminated").isFalse(); @@ -147,15 +147,15 @@ void scanTerminatedCancelled() { assertThat(sink.scan(Scannable.Attr.CANCELLED)).as("pre-cancellation").isFalse(); - ((UnicastManySinkNoBackpressure) sink).cancel(); + ((SinkManyUnicastNoBackpressure) sink).cancel(); assertThat(sink.scan(Scannable.Attr.CANCELLED)).as("cancelled").isTrue(); } @Test void inners() { - Sinks.Many sink1 = UnicastManySinkNoBackpressure.create(); - Sinks.Many sink2 = UnicastManySinkNoBackpressure.create(); + Sinks.Many sink1 = SinkManyUnicastNoBackpressure.create(); + Sinks.Many sink2 = SinkManyUnicastNoBackpressure.create(); CoreSubscriber notScannable = new BaseSubscriber() {}; InnerConsumer scannable = new LambdaSubscriber<>(null, null, null, null); diff --git a/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastTest.java new file mode 100644 index 0000000000..49e347e117 --- /dev/null +++ b/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastTest.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.locks.LockSupport; + +import org.junit.jupiter.api.Test; + +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.Exceptions; +import reactor.core.Scannable; +import reactor.core.Scannable.Attr; +import reactor.test.StepVerifier; +import reactor.test.subscriber.AssertSubscriber; +import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; + +import static org.assertj.core.api.Assertions.assertThat; +import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST; + +// This is ok as this class tests the deprecated UnicastProcessor. Will be removed with it in 3.5. +@SuppressWarnings("deprecation") +public class SinkManyUnicastTest { + + @Test + public void currentSubscriberCount() { + Sinks.Many sink = SinkManyUnicast.create(); + + assertThat(sink.currentSubscriberCount()).isZero(); + + sink.asFlux().subscribe(); + + assertThat(sink.currentSubscriberCount()).isOne(); + } + + @Test + public void secondSubscriberRejectedProperly() { + + SinkManyUnicast up = SinkManyUnicast.create(new ConcurrentLinkedQueue<>()); + + up.subscribe(); + + AssertSubscriber ts = AssertSubscriber.create(); + + up.subscribe(ts); + + ts.assertNoValues() + .assertError(IllegalStateException.class) + .assertNotComplete(); + + } + + @Test + public void multiThreadedProducer() { + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + int nThreads = 5; + int countPerThread = 10000; + ExecutorService executor = Executors.newFixedThreadPool(nThreads); + for (int i = 0; i < 5; i++) { + Runnable generator = () -> { + for (int j = 0; j < countPerThread; j++) { + while (sink.tryEmitNext(j).isFailure()) { + LockSupport.parkNanos(10); + } + } + }; + executor.submit(generator); + } + StepVerifier.create(sink.asFlux()) + .expectNextCount(nThreads * countPerThread) + .thenCancel() + .verify(); + executor.shutdownNow(); + } + + @Test + public void createDefault() { + SinkManyUnicast processor = SinkManyUnicast.create(); + assertProcessor(processor, null, null); + } + + @Test + public void createOverrideQueue() { + Queue queue = Queues.get(10).get(); + SinkManyUnicast processor = SinkManyUnicast.create(queue); + assertProcessor(processor, queue, null); + } + + @Test + public void createOverrideQueueOnTerminate() { + Disposable onTerminate = () -> {}; + Queue queue = Queues.get(10).get(); + SinkManyUnicast processor = SinkManyUnicast.create(queue, onTerminate); + assertProcessor(processor, queue, onTerminate); + } + + void assertProcessor(SinkManyUnicast processor, + @Nullable Queue queue, + @Nullable Disposable onTerminate) { + Queue expectedQueue = queue != null ? queue : Queues.unbounded().get(); + Disposable expectedOnTerminate = onTerminate; + assertThat(processor.queue.getClass()).isEqualTo(expectedQueue.getClass()); + assertThat(processor.onTerminate).isEqualTo(expectedOnTerminate); + } + + @Test + void scanCapacityReactorUnboundedQueue() { + SinkManyUnicast processor = SinkManyUnicast.create( + Queues.unbounded(2).get()); + + assertThat(processor.scan(Attr.CAPACITY)).isEqualTo(Integer.MAX_VALUE); + } + + @Test + void scanCapacityReactorBoundedQueue() { + //the bounded queue floors at 8 and rounds to the next power of 2 + + assertThat(SinkManyUnicast.create(Queues.get(2).get()).scan(Attr.CAPACITY)) + .isEqualTo(8); + + assertThat(SinkManyUnicast.create(Queues.get(8).get()).scan(Attr.CAPACITY)) + .isEqualTo(8); + + assertThat(SinkManyUnicast.create(Queues.get(9).get()).scan(Attr.CAPACITY)) + .isEqualTo(16); + } + + @Test + void scanCapacityBoundedBlockingQueue() { + SinkManyUnicast processor = SinkManyUnicast.create( + new LinkedBlockingQueue<>(10)); + + assertThat(processor.scan(Attr.CAPACITY)).isEqualTo(10); + } + + @Test + void scanCapacityUnboundedBlockingQueue() { + SinkManyUnicast processor = SinkManyUnicast.create(new LinkedBlockingQueue<>()); + + assertThat(processor.scan(Attr.CAPACITY)).isEqualTo(Integer.MAX_VALUE); + + } + + @Test + void scanCapacityOtherQueue() { + SinkManyUnicast processor = SinkManyUnicast.create(new PriorityQueue<>(10)); + + assertThat(processor.scan(Attr.CAPACITY)) + .isEqualTo(Integer.MIN_VALUE) + .isEqualTo(Queues.CAPACITY_UNSURE); + } + + + @Test + public void contextTest() { + SinkManyUnicast p = SinkManyUnicast.create(); + p.contextWrite(ctx -> ctx.put("foo", "bar")).subscribe(); + + assertThat(p.currentContext().get("foo").toString()).isEqualTo("bar"); + } + + @Test + public void subscriptionCancelUpdatesDownstreamCount() { + SinkManyUnicast processor = SinkManyUnicast.create(); + + assertThat(processor.currentSubscriberCount()) + .as("before subscribe") + .isZero(); + + LambdaSubscriber subscriber = new LambdaSubscriber<>(null, null, null, null); + Disposable subscription = processor.subscribeWith(subscriber); + + assertThat(processor.currentSubscriberCount()) + .as("after subscribe") + .isPositive(); + assertThat(processor.actual) + .as("after subscribe has actual") + .isSameAs(subscriber); + + subscription.dispose(); + + assertThat(processor.currentSubscriberCount()) + .as("after subscription cancel") + .isZero(); + } + + @Test + public void shouldNotThrowFromTryEmitNext() { + SinkManyUnicast processor = new SinkManyUnicast<>(Queues.empty().get()); + + StepVerifier.create(processor, 0) + .expectSubscription() + .then(() -> { + assertThat(processor.tryEmitNext("boom")) + .as("emission") + .isEqualTo(Sinks.EmitResult.FAIL_OVERFLOW); + }) + .then(() -> processor.tryEmitComplete().orThrow()) + .verifyComplete(); + } + + @Test + public void shouldSignalErrorOnOverflow() { + SinkManyUnicast processor = new SinkManyUnicast<>(Queues.empty().get()); + + StepVerifier.create(processor, 0) + .expectSubscription() + .then(() -> processor.emitNext("boom", FAIL_FAST)) + .verifyErrorMatches(Exceptions::isOverflow); + } + + @Test + public void tryEmitNextWithNoSubscriberAndBoundedQueueFailsZeroSubscriber() { + SinkManyUnicast sinkManyUnicast = SinkManyUnicast.create(Queues.one().get()); + + assertThat(sinkManyUnicast.tryEmitNext(1)).isEqualTo(Sinks.EmitResult.OK); + assertThat(sinkManyUnicast.tryEmitNext(2)).isEqualTo(Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER); + + StepVerifier.create(sinkManyUnicast) + .expectNext(1) + .then(() -> sinkManyUnicast.tryEmitComplete().orThrow()) + .verifyComplete(); + } + + @Test + public void tryEmitNextWithBoundedQueueAndNoRequestFailsWithOverflow() { + SinkManyUnicast sinkManyUnicast = SinkManyUnicast.create(Queues.one().get()); + + StepVerifier.create(sinkManyUnicast, 0) //important to make no initial request + .expectSubscription() + .then(() -> { + assertThat(sinkManyUnicast.tryEmitNext(1)).isEqualTo(Sinks.EmitResult.OK); + assertThat(sinkManyUnicast.tryEmitNext(2)).isEqualTo(Sinks.EmitResult.FAIL_OVERFLOW); + assertThat(sinkManyUnicast.tryEmitComplete()).isEqualTo(Sinks.EmitResult.OK); + }) + .thenRequest(1) + .expectNext(1) + .verifyComplete(); + } + + @Test + public void emitNextWithNoSubscriberAndBoundedQueueIgnoresValueAndKeepsSinkOpen() { + SinkManyUnicast sinkManyUnicast = SinkManyUnicast.create(Queues.one().get()); + //fill the buffer + sinkManyUnicast.tryEmitNext(1); + //this "overflows" but keeps the sink open. since there's no subscriber, there's no Context so no real discarding + sinkManyUnicast.emitNext(2, FAIL_FAST); + + //let's verify we get the buffer's content + StepVerifier.create(sinkManyUnicast) + .expectNext(1) //from the buffer + .expectNoEvent(Duration.ofMillis(500)) + .then(() -> sinkManyUnicast.tryEmitComplete().orThrow()) + .verifyComplete(); + } + + @Test + public void scanTerminatedCancelled() { + Sinks.Many sink = SinkManyUnicast.create(); + + assertThat(sink.scan(Attr.TERMINATED)).as("not yet terminated").isFalse(); + + sink.tryEmitError(new IllegalStateException("boom")).orThrow(); + + assertThat(sink.scan(Attr.TERMINATED)).as("terminated with error").isTrue(); + assertThat(sink.scan(Attr.ERROR)).as("error").hasMessage("boom"); + + assertThat(sink.scan(Attr.CANCELLED)).as("pre-cancellation").isFalse(); + + ((SinkManyUnicast) sink).cancel(); + + assertThat(sink.scan(Attr.CANCELLED)).as("cancelled").isTrue(); + } + + @Test + public void inners() { + Sinks.Many sink1 = SinkManyUnicast.create(); + Sinks.Many sink2 = SinkManyUnicast.create(); + CoreSubscriber notScannable = new BaseSubscriber() {}; + InnerConsumer scannable = new LambdaSubscriber<>(null, null, null, null); + + assertThat(sink1.inners()).as("before subscription notScannable").isEmpty(); + assertThat(sink2.inners()).as("before subscription notScannable").isEmpty(); + + sink1.asFlux().subscribe(notScannable); + sink2.asFlux().subscribe(scannable); + + assertThat(sink1.inners()) + .asList() + .as("after notScannable subscription") + .containsExactly(Scannable.from("NOT SCANNABLE")); + + assertThat(sink2.inners()) + .asList() + .as("after scannable subscription") + .containsExactly(scannable); + } +} diff --git a/reactor-core/src/test/java/reactor/core/publisher/SinksTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinksTest.java index c53b2b64af..c66659bdfd 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/SinksTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SinksTest.java @@ -623,7 +623,8 @@ DynamicContainer expectUnicast(Supplier> sinkSupplier) { assertThatCode(flux::subscribe).doesNotThrowAnyException(); StepVerifier.create(flux) - .verifyErrorSatisfies(e -> assertThat(e).hasMessageEndingWith("allows only a single Subscriber")); + .verifyErrorSatisfies(e -> assertThat(e) + .hasMessage("Sinks.many().unicast() sinks only allow a single Subscriber")); }), dynamicTest("honorsSubscriberBackpressure", () -> { diff --git a/reactor-core/src/test/java/reactor/core/publisher/UnicastProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/UnicastProcessorTest.java deleted file mode 100644 index 3399796cff..0000000000 --- a/reactor-core/src/test/java/reactor/core/publisher/UnicastProcessorTest.java +++ /dev/null @@ -1,411 +0,0 @@ -/* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.publisher; - -import java.time.Duration; -import java.util.List; -import java.util.PriorityQueue; -import java.util.Queue; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.locks.LockSupport; -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; - -import reactor.core.CoreSubscriber; -import reactor.core.Disposable; -import reactor.core.Disposables; -import reactor.core.Exceptions; -import reactor.core.Scannable; -import reactor.test.MemoryUtils; -import reactor.test.StepVerifier; -import reactor.test.publisher.TestPublisher; -import reactor.test.subscriber.AssertSubscriber; -import reactor.test.util.RaceTestUtils; -import reactor.util.annotation.Nullable; -import reactor.util.concurrent.Queues; - -import static org.assertj.core.api.Assertions.assertThat; -import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST; - -// This is ok as this class tests the deprecated UnicastProcessor. Will be removed with it in 3.5. -@SuppressWarnings("deprecation") -public class UnicastProcessorTest { - - @Test - public void currentSubscriberCount() { - Sinks.Many sink = UnicastProcessor.create(); - - assertThat(sink.currentSubscriberCount()).isZero(); - - sink.asFlux().subscribe(); - - assertThat(sink.currentSubscriberCount()).isOne(); - } - - @Test - public void secondSubscriberRejectedProperly() { - - UnicastProcessor up = UnicastProcessor.create(new ConcurrentLinkedQueue<>()); - - up.subscribe(); - - AssertSubscriber ts = AssertSubscriber.create(); - - up.subscribe(ts); - - ts.assertNoValues() - .assertError(IllegalStateException.class) - .assertNotComplete(); - - } - - @Test - public void multiThreadedProducer() { - Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); - int nThreads = 5; - int countPerThread = 10000; - ExecutorService executor = Executors.newFixedThreadPool(nThreads); - for (int i = 0; i < 5; i++) { - Runnable generator = () -> { - for (int j = 0; j < countPerThread; j++) { - while (sink.tryEmitNext(j).isFailure()) { - LockSupport.parkNanos(10); - } - } - }; - executor.submit(generator); - } - StepVerifier.create(sink.asFlux()) - .expectNextCount(nThreads * countPerThread) - .thenCancel() - .verify(); - executor.shutdownNow(); - } - - @Test - public void createDefault() { - UnicastProcessor processor = UnicastProcessor.create(); - assertProcessor(processor, null, null, null); - } - - @Test - public void createOverrideQueue() { - Queue queue = Queues.get(10).get(); - UnicastProcessor processor = UnicastProcessor.create(queue); - assertProcessor(processor, queue, null, null); - } - - @Test - public void createOverrideQueueOnTerminate() { - Disposable onTerminate = () -> {}; - Queue queue = Queues.get(10).get(); - UnicastProcessor processor = UnicastProcessor.create(queue, onTerminate); - assertProcessor(processor, queue, null, onTerminate); - } - - @Test - public void createOverrideAll() { - Disposable onTerminate = () -> {}; - Consumer onOverflow = t -> {}; - Queue queue = Queues.get(10).get(); - UnicastProcessor processor = UnicastProcessor.create(queue, onOverflow, onTerminate); - assertProcessor(processor, queue, onOverflow, onTerminate); - } - - public void assertProcessor(UnicastProcessor processor, - @Nullable Queue queue, - @Nullable Consumer onOverflow, - @Nullable Disposable onTerminate) { - Queue expectedQueue = queue != null ? queue : Queues.unbounded().get(); - Disposable expectedOnTerminate = onTerminate; - assertThat(processor.queue.getClass()).isEqualTo(expectedQueue.getClass()); - assertThat(processor.onTerminate).isEqualTo(expectedOnTerminate); - if (onOverflow != null) - assertThat(processor.onOverflow).isEqualTo(onOverflow); - } - - @Test - public void bufferSizeReactorUnboundedQueue() { - UnicastProcessor processor = UnicastProcessor.create( - Queues.unbounded(2).get()); - - assertThat(processor.getBufferSize()).isEqualTo(Integer.MAX_VALUE); - } - - @Test - public void bufferSizeReactorBoundedQueue() { - //the bounded queue floors at 8 and rounds to the next power of 2 - - assertThat(UnicastProcessor.create(Queues.get(2).get()) - .getBufferSize()) - .isEqualTo(8); - - assertThat(UnicastProcessor.create(Queues.get(8).get()) - .getBufferSize()) - .isEqualTo(8); - - assertThat(UnicastProcessor.create(Queues.get(9).get()) - .getBufferSize()) - .isEqualTo(16); - } - - @Test - public void bufferSizeBoundedBlockingQueue() { - UnicastProcessor processor = UnicastProcessor.create( - new LinkedBlockingQueue<>(10)); - - assertThat(processor.getBufferSize()).isEqualTo(10); - } - - @Test - public void bufferSizeUnboundedBlockingQueue() { - UnicastProcessor processor = UnicastProcessor.create( - new LinkedBlockingQueue<>()); - - assertThat(processor.getBufferSize()).isEqualTo(Integer.MAX_VALUE); - - } - - @Test - public void bufferSizeOtherQueue() { - Sinks.Many processor = Sinks.many().unicast().onBackpressureBuffer( - new PriorityQueue<>(10)); - - assertThat(Scannable.from(processor).scan(Scannable.Attr.CAPACITY)) - .isEqualTo(Integer.MIN_VALUE) - .isEqualTo(Queues.CAPACITY_UNSURE); - } - - - @Test - public void contextTest() { - UnicastProcessor p = UnicastProcessor.create(); - p.contextWrite(ctx -> ctx.put("foo", "bar")).subscribe(); - - assertThat(p.sink().currentContext().get("foo").toString()).isEqualTo("bar"); - } - - @Test - public void subscriptionCancelUpdatesDownstreamCount() { - UnicastProcessor processor = UnicastProcessor.create(); - - assertThat(processor.currentSubscriberCount()) - .as("before subscribe") - .isZero(); - - LambdaSubscriber subscriber = new LambdaSubscriber<>(null, null, null, null); - Disposable subscription = processor.subscribeWith(subscriber); - - assertThat(processor.currentSubscriberCount()) - .as("after subscribe") - .isPositive(); - assertThat(processor.actual()) - .as("after subscribe has actual") - .isSameAs(subscriber); - - subscription.dispose(); - - assertThat(processor.currentSubscriberCount()) - .as("after subscription cancel") - .isZero(); - } - - @Test - public void ensureNoLeaksIfRacingDisposeAndOnNext() { - Hooks.onNextDropped(MemoryUtils.Tracked::safeRelease); - try { - MemoryUtils.OffHeapDetector tracker = new MemoryUtils.OffHeapDetector(); - for (int i = 0; i < 10000; i++) { - tracker.reset(); - TestPublisher testPublisher = TestPublisher.createNoncompliant(TestPublisher.Violation.DEFER_CANCELLATION, - TestPublisher.Violation.REQUEST_OVERFLOW); - UnicastProcessor unicastProcessor = UnicastProcessor.create(); - - testPublisher.subscribe(unicastProcessor); - - AssertSubscriber assertSubscriber = - new AssertSubscriber<>(Operators.enableOnDiscard(null, MemoryUtils.Tracked::safeRelease)); - - unicastProcessor.subscribe(assertSubscriber); - - testPublisher.next(tracker.track(1)); - testPublisher.next(tracker.track(2)); - - MemoryUtils.Tracked value3 = tracker.track(3); - MemoryUtils.Tracked value4 = tracker.track(4); - MemoryUtils.Tracked value5 = tracker.track(5); - - RaceTestUtils.race(unicastProcessor::dispose, () -> { - testPublisher.next(value3); - testPublisher.next(value4); - testPublisher.next(value5); - }); - - assertSubscriber.assertTerminated() - .assertError(CancellationException.class) - .assertErrorMessage("Disposed"); - - List values = assertSubscriber.values(); - values.forEach(MemoryUtils.Tracked::release); - - tracker.assertNoLeaks(); - } - } finally { - Hooks.resetOnNextDropped(); - } - } - - @Test - public void shouldNotThrowFromTryEmitNext() { - UnicastProcessor processor = new UnicastProcessor<>(Queues.empty().get()); - - StepVerifier.create(processor, 0) - .expectSubscription() - .then(() -> { - assertThat(processor.tryEmitNext("boom")) - .as("emission") - .isEqualTo(Sinks.EmitResult.FAIL_OVERFLOW); - }) - .then(() -> processor.tryEmitComplete().orThrow()) - .verifyComplete(); - } - - @Test - public void shouldSignalErrorOnOverflow() { - UnicastProcessor processor = new UnicastProcessor<>(Queues.empty().get()); - - StepVerifier.create(processor, 0) - .expectSubscription() - .then(() -> processor.emitNext("boom", FAIL_FAST)) - .verifyErrorMatches(Exceptions::isOverflow); - } - - @Test - public void tryEmitNextWithNoSubscriberAndBoundedQueueFailsZeroSubscriber() { - UnicastProcessor unicastProcessor = UnicastProcessor.create(Queues.one().get()); - - assertThat(unicastProcessor.tryEmitNext(1)).isEqualTo(Sinks.EmitResult.OK); - assertThat(unicastProcessor.tryEmitNext(2)).isEqualTo(Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER); - - StepVerifier.create(unicastProcessor) - .expectNext(1) - .then(() -> unicastProcessor.tryEmitComplete().orThrow()) - .verifyComplete(); - } - - @Test - public void tryEmitNextWithBoundedQueueAndNoRequestFailsWithOverflow() { - UnicastProcessor unicastProcessor = UnicastProcessor.create(Queues.one().get()); - - StepVerifier.create(unicastProcessor, 0) //important to make no initial request - .expectSubscription() - .then(() -> { - assertThat(unicastProcessor.tryEmitNext(1)).isEqualTo(Sinks.EmitResult.OK); - assertThat(unicastProcessor.tryEmitNext(2)).isEqualTo(Sinks.EmitResult.FAIL_OVERFLOW); - assertThat(unicastProcessor.tryEmitComplete()).isEqualTo(Sinks.EmitResult.OK); - }) - .thenRequest(1) - .expectNext(1) - .verifyComplete(); - } - - @Test - public void emitNextWithNoSubscriberAndBoundedQueueIgnoresValueAndKeepsSinkOpen() { - UnicastProcessor unicastProcessor = UnicastProcessor.create(Queues.one().get()); - //fill the buffer - unicastProcessor.tryEmitNext(1); - //this "overflows" but keeps the sink open. since there's no subscriber, there's no Context so no real discarding - unicastProcessor.emitNext(2, FAIL_FAST); - - //let's verify we get the buffer's content - StepVerifier.create(unicastProcessor) - .expectNext(1) //from the buffer - .expectNoEvent(Duration.ofMillis(500)) - .then(() -> unicastProcessor.tryEmitComplete().orThrow()) - .verifyComplete(); - } - - @Test //TODO that onOverflow API isn't exposed via Sinks. But maybe it should be generalized? - public void emitNextWithNoSubscriberAndBoundedQueueAndHandlerHandlesValueAndKeepsSinkOpen() { - Disposable sinkDisposed = Disposables.single(); - List discarded = new CopyOnWriteArrayList<>(); - UnicastProcessor unicastProcessor = UnicastProcessor.create(Queues.one().get(), - discarded::add, sinkDisposed); - //fill the buffer - unicastProcessor.tryEmitNext(1); - //this "overflows" but keeps the sink open - unicastProcessor.emitNext(2, FAIL_FAST); - - assertThat(discarded).containsExactly(2); - assertThat(sinkDisposed.isDisposed()).as("sinkDisposed").isFalse(); - - unicastProcessor.emitComplete(FAIL_FAST); - - //let's verify we get the buffer's content - StepVerifier.create(unicastProcessor) - .expectNext(1) //from the buffer - .verifyComplete(); - } - - @Test - public void scanTerminatedCancelled() { - Sinks.Many sink = UnicastProcessor.create(); - - assertThat(sink.scan(Scannable.Attr.TERMINATED)).as("not yet terminated").isFalse(); - - sink.tryEmitError(new IllegalStateException("boom")).orThrow(); - - assertThat(sink.scan(Scannable.Attr.TERMINATED)).as("terminated with error").isTrue(); - assertThat(sink.scan(Scannable.Attr.ERROR)).as("error").hasMessage("boom"); - - assertThat(sink.scan(Scannable.Attr.CANCELLED)).as("pre-cancellation").isFalse(); - - ((UnicastProcessor) sink).cancel(); - - assertThat(sink.scan(Scannable.Attr.CANCELLED)).as("cancelled").isTrue(); - } - - @Test - public void inners() { - Sinks.Many sink1 = UnicastProcessor.create(); - Sinks.Many sink2 = UnicastProcessor.create(); - CoreSubscriber notScannable = new BaseSubscriber() {}; - InnerConsumer scannable = new LambdaSubscriber<>(null, null, null, null); - - assertThat(sink1.inners()).as("before subscription notScannable").isEmpty(); - assertThat(sink2.inners()).as("before subscription notScannable").isEmpty(); - - sink1.asFlux().subscribe(notScannable); - sink2.asFlux().subscribe(scannable); - - assertThat(sink1.inners()) - .asList() - .as("after notScannable subscription") - .containsExactly(Scannable.from("NOT SCANNABLE")); - - assertThat(sink2.inners()) - .asList() - .as("after scannable subscription") - .containsExactly(scannable); - } -} diff --git a/reactor-tools/src/test/java/reactor/tools/agent/ReactorDebugAgentTest.java b/reactor-tools/src/test/java/reactor/tools/agent/ReactorDebugAgentTest.java index fe27ab85a6..a63479218e 100644 --- a/reactor-tools/src/test/java/reactor/tools/agent/ReactorDebugAgentTest.java +++ b/reactor-tools/src/test/java/reactor/tools/agent/ReactorDebugAgentTest.java @@ -30,7 +30,6 @@ import reactor.core.Scannable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import static org.assertj.core.api.Assertions.assertThat; @@ -86,16 +85,6 @@ public void shouldWorkWithGroupedFlux() { .startsWith("GroupedFlux.map ⇢ at reactor.tools.agent.ReactorDebugAgentTest.shouldWorkWithGroupedFlux(ReactorDebugAgentTest.java:" + (baseline + 1)); } - @Test - @Deprecated - void shouldIgnoreMonoProcessor() { - Mono mono = Mono.just(1); - MonoProcessor monoProcessor = mono.toProcessor(); - assertThat(Scannable.from(monoProcessor).stepName()) - .doesNotContain(" ⇢ at") - .isEqualTo("nextProcessor"); - } - @Test public void shouldHandleNullReturnValue() { Mono mono = methodReturningNullMono(); From 686b2b3bc65981704976d9f7169e92a462cfd9d0 Mon Sep 17 00:00:00 2001 From: Jarred H Date: Tue, 7 Jun 2022 05:10:22 -0400 Subject: [PATCH 027/312] Simplify list of JVM metrisc in metrics.adoc (#3047) This commit removes the list of `ExecutorService`-related metrics, some of which were outdated, to favor referring the user to the Micrometer documentation directly. --- docs/asciidoc/metrics.adoc | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/asciidoc/metrics.adoc b/docs/asciidoc/metrics.adoc index 3f0ffef7e7..09d6b14d47 100644 --- a/docs/asciidoc/metrics.adoc +++ b/docs/asciidoc/metrics.adoc @@ -27,13 +27,7 @@ TIP: If you're using Spring Boot, it is a good idea to place the invocation befo Once scheduler metrics are enabled and provided it is on the classpath, Reactor will use Micrometer's support for instrumenting the executors that back most schedulers. -Please refer to https://micrometer.io/docs/ref/jvm[Micrometer's documentation] for the exposed metrics, such as: - -- executor_active_threads -- executor_completed_tasks_total -- executor_pool_size_threads -- executor_queued_tasks -- executor_secounds_{count, max, sum} +Please refer to https://micrometer.io/docs/ref/jvm[Micrometer's documentation] for the exposed metrics. Since one scheduler may have multiple executors, every executor metric has a `reactor_scheduler_id` tag. From bb9219dd94d1e1610176eeae202bd68590bb8fe2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Sat, 11 Jun 2022 13:19:35 +0300 Subject: [PATCH 028/312] Fix windowTimeout stress test to use sinks (#3074) Signed-off-by: Oleh Dokuka odokuka@vmware.com --- .../FluxWindowTimeoutStressTest.java | 23 ++++++++++--------- .../core/publisher/FluxWindowTimeoutTest.java | 1 - 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxWindowTimeoutStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxWindowTimeoutStressTest.java index 2e5ee0607a..76c1a9c25e 100644 --- a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxWindowTimeoutStressTest.java +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxWindowTimeoutStressTest.java @@ -331,7 +331,7 @@ public static class FluxWindowTimoutStressTest1_3 { - final UnicastProcessor proxy = new UnicastProcessor<>(Queues.get(8).get()); + final Sinks.Many proxy = Sinks.many().unicast().onBackpressureBuffer(Queues.get(8).get()); final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); StressSubscriber subscriber1 = new StressSubscriber<>(); StressSubscriber subscriber2 = new StressSubscriber<>(); @@ -397,21 +397,22 @@ public void onNext(Flux window) { final AtomicLong requested = new AtomicLong(); { - proxy.doOnRequest(requested::addAndGet) + proxy.asFlux() + .doOnRequest(requested::addAndGet) .subscribe(windowTimeoutSubscriber); } @Actor public void next() { - proxy.onNext(0L); - proxy.onNext(1L); - proxy.onNext(2L); - proxy.onNext(3L); - proxy.onNext(4L); - proxy.onNext(5L); - proxy.onNext(6L); - proxy.onNext(7L); - proxy.onComplete(); + proxy.emitNext(0L, Sinks.EmitFailureHandler.FAIL_FAST); + proxy.emitNext(1L, Sinks.EmitFailureHandler.FAIL_FAST); + proxy.emitNext(2L, Sinks.EmitFailureHandler.FAIL_FAST); + proxy.emitNext(3L, Sinks.EmitFailureHandler.FAIL_FAST); + proxy.emitNext(4L, Sinks.EmitFailureHandler.FAIL_FAST); + proxy.emitNext(5L, Sinks.EmitFailureHandler.FAIL_FAST); + proxy.emitNext(6L, Sinks.EmitFailureHandler.FAIL_FAST); + proxy.emitNext(7L, Sinks.EmitFailureHandler.FAIL_FAST); + proxy.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST); } @Actor diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTimeoutTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTimeoutTest.java index 17cea4eebf..08737ea734 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTimeoutTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTimeoutTest.java @@ -248,7 +248,6 @@ public void noDelayMultipleOfSize() { .concatMap(Flux::collectList)) .assertNext(l -> assertThat(l).containsExactly(1, 2, 3, 4, 5)) .assertNext(l -> assertThat(l).containsExactly(6, 7, 8, 9, 10)) - .assertNext(l -> assertThat(l).isEmpty()) .verifyComplete(); } From 33767d0edb263ae274ecec62192d3d88a65cb6bd Mon Sep 17 00:00:00 2001 From: Eduard Neagoe Date: Mon, 13 Jun 2022 11:59:02 +0300 Subject: [PATCH 029/312] Fix doc typo: missing word in reactiveProgramming.adoc (#3075) --- docs/asciidoc/reactiveProgramming.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/asciidoc/reactiveProgramming.adoc b/docs/asciidoc/reactiveProgramming.adoc index 198f217d43..11fa48d555 100644 --- a/docs/asciidoc/reactiveProgramming.adoc +++ b/docs/asciidoc/reactiveProgramming.adoc @@ -373,7 +373,7 @@ behavior to a `Publisher` and wraps the previous step's `Publisher` into a new i The whole chain is thus linked, such that data originates from the first `Publisher` and moves down the chain, transformed by each link. Eventually, a `Subscriber` finishes the process. Remember that nothing happens until a `Subscriber` subscribes to a `Publisher`, -as we see shortly. +as we will see shortly. TIP: Understanding that operators create new instances can help you avoid a common mistake that would lead you to believe that an operator you used in your chain is not From 0b66e5225f4101360bd136a1f6b494f842d89088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 21 Jun 2022 10:08:46 +0200 Subject: [PATCH 030/312] [release] Prepare and release 3.5.0-M3 --- README.md | 6 +++--- gradle.properties | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 898332614f..778f59c024 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.0-M2" - testCompile "io.projectreactor:reactor-test:3.5.0-M2" + compile "io.projectreactor:reactor-core:3.5.0-M3" + testCompile "io.projectreactor:reactor-test:3.5.0-M3" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.5.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.5.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.0-M2" + // implementation "io.projectreactor:reactor-tools:3.5.0-M3" } ``` diff --git a/gradle.properties b/gradle.properties index cdb0599c8e..56773194fc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-SNAPSHOT -bomVersion=2022.0.0-M2 -metricsMicrometerVersion=1.0.0-SNAPSHOT \ No newline at end of file +version=3.5.0-M3 +bomVersion=2022.0.0-M3 +metricsMicrometerVersion=1.0.0-M3 \ No newline at end of file From ef67c7f6441e1dc33ffd160f4d20bdbdbf6067cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 21 Jun 2022 10:54:34 +0200 Subject: [PATCH 031/312] [release] Next development version 3.5.0-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 56773194fc..09436d6018 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-M3 +version=3.5.0-SNAPSHOT bomVersion=2022.0.0-M3 -metricsMicrometerVersion=1.0.0-M3 \ No newline at end of file +metricsMicrometerVersion=1.0.0-SNAPSHOT \ No newline at end of file From 56077aa3b6beef1b2b916ab21a7792b4a1a5f347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 1 Jul 2022 09:58:38 +0200 Subject: [PATCH 032/312] Add context-propagation-api dependency + ReactorContextAccessor (#3098) This commit adds a dependency to `micrometer:context-propagation-api`, in snapshot version. Temporarily, it switches switches the micrometer BOM to the snapshot version as well (for compatibility with the above new dependency). Most importantly, this new dependency is used to implement a context accessor, `ReactorContextAccessor`, to be picked up by ServiceLoader mechanism (META-INF/services/ file also included). --- gradle/libs.versions.toml | 4 +- reactor-core/build.gradle | 3 + .../util/context/ReactorContextAccessor.java | 55 +++++++++++++++++++ .../io.micrometer.context.ContextAccessor | 1 + 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java create mode 100644 reactor-core/src/main/resources/META-INF/services/io.micrometer.context.ContextAccessor diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index adebf68d1d..a3ee27bf0a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,8 @@ asciidoctor = "3.3.2" bytebuddy = "1.12.10" jmh = "1.35" junit = "5.8.2" -micrometer = "1.10.0-M1" +#note that context-propagation-api has a different version directly set in libraries +micrometer = "1.10.0-SNAPSHOT" reactiveStreams = "1.0.4" [libraries] @@ -30,6 +31,7 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } +micrometer-contextPropagationApi = "io.micrometer:context-propagation-api:1.0.0-SNAPSHOT" mockito = "org.mockito:mockito-core:4.6.1" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } reactiveStreams-tck = { module = "org.reactivestreams:reactive-streams-tck", version.ref = "reactiveStreams" } diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index d4e0334d51..df98f007db 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -74,6 +74,9 @@ dependencies { } tckTestImplementation libs.testNg + // context-propagation-api + implementation libs.micrometer.contextPropagationApi + // JSR-305 annotations compileOnly libs.jsr305 testCompileOnly libs.jsr305 diff --git a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java new file mode 100644 index 0000000000..73a62d7b48 --- /dev/null +++ b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.util.context; + +import java.util.Map; +import java.util.function.Predicate; + +import io.micrometer.context.ContextAccessor; + +/** + * {@code ContextAccessor} to enable reading values from a Reactor + * {@link ContextView} and writing values to {@link Context}. + * + * @author Rossen Stoyanchev + * @since 3.5.0 + */ +public final class ReactorContextAccessor implements ContextAccessor { + + @Override + public boolean canReadFrom(Class contextType) { + return ContextView.class.isAssignableFrom(contextType); + } + + @Override + public void readValues(ContextView source, Predicate keyPredicate, Map target) { + source.stream() + .filter(entry -> keyPredicate.test(entry.getKey())) + .forEach(entry -> target.put(entry.getKey(), entry.getValue())); + } + + @Override + public boolean canWriteTo(Class contextType) { + return Context.class.isAssignableFrom(contextType); + } + + @Override + public Context writeValues(Map source, Context target) { + return target.putAll(Context.of(source).readOnly()); + } + +} diff --git a/reactor-core/src/main/resources/META-INF/services/io.micrometer.context.ContextAccessor b/reactor-core/src/main/resources/META-INF/services/io.micrometer.context.ContextAccessor new file mode 100644 index 0000000000..929fa6612d --- /dev/null +++ b/reactor-core/src/main/resources/META-INF/services/io.micrometer.context.ContextAccessor @@ -0,0 +1 @@ +reactor.util.context.ReactorContextAccessor \ No newline at end of file From 35f49d5c324e7c9edb06e80327478f9e181673af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 1 Jul 2022 17:18:55 +0200 Subject: [PATCH 033/312] Make context-propagation-api dependency optional + documentation (#3100) Polishing of #3098 : - make context-propagation-api dependency optional - mention the optional SPI in the accessor's javadoc - mention support of the SPI in the Context section of reference guide --- docs/asciidoc/advancedFeatures.adoc | 9 +++++++++ reactor-core/build.gradle | 6 +++--- .../reactor/util/context/ReactorContextAccessor.java | 5 ++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/asciidoc/advancedFeatures.adoc b/docs/asciidoc/advancedFeatures.adoc index 8b43bcec71..56716dc1ac 100644 --- a/docs/asciidoc/advancedFeatures.adoc +++ b/docs/asciidoc/advancedFeatures.adoc @@ -839,7 +839,16 @@ its `ContextView`: TIP: In order to read from the `Context` without misleading users into thinking one can write to it while data is running through the pipeline, only the `ContextView` is exposed by the operators above. +In case one needs to use one of the remaining APIs that still require a `Context`, one can use `Context.of(contextView)` for conversion. +[[context.propagation]] +=== Micrometer Context-Propagation Support +Since 3.5.0, Reactor-Core embeds basic support for the `io.micrometer:context-propagation-api` SPI. +This library is intended as a mean to easily adapt between various implementations of the concept of a Context, of which +`ContextView`/`Context` is an example, and between `ThreadLocal` variables as well. + +`ReactorContextAccessor` is one implementation of this SPI that is loaded via `ServiceLoader`. No user action is required, +other than depending on reactor-core and `context-propagation-api` (the class is public but shouldn't generally be accessed by user code). === Simple `Context` Examples diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index b9a3357c38..4982b1850f 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -74,9 +74,6 @@ dependencies { } tckTestImplementation libs.testNg - // context-propagation-api - implementation libs.micrometer.contextPropagationApi - // JSR-305 annotations compileOnly libs.jsr305 testCompileOnly libs.jsr305 @@ -90,6 +87,9 @@ dependencies { compileOnly libs.micrometer.commons compileOnly libs.micrometer.core + // Optional context-propagation-api + compileOnly libs.micrometer.contextPropagationApi + // Optional BlockHound support compileOnly libs.blockhound // Also make BlockHound visible in the CP of dedicated testset diff --git a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java index 73a62d7b48..c2c21d0d97 100644 --- a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java +++ b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java @@ -22,8 +22,11 @@ import io.micrometer.context.ContextAccessor; /** - * {@code ContextAccessor} to enable reading values from a Reactor + * A {@code ContextAccessor} to enable reading values from a Reactor * {@link ContextView} and writing values to {@link Context}. + *

    + * Please note that this public class implements the {@code libs.micrometer.contextPropagationApi} + * SPI library, which is an optional dependency. * * @author Rossen Stoyanchev * @since 3.5.0 From 3ba5dd7b9fb7799d0ce161741f42224e293544b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 7 Jul 2022 16:55:10 +0200 Subject: [PATCH 034/312] Improve ReactorContextAccessor, add tests (#3107) This commit improves the ReactorContextAccessor implementation to make use of newly introduced `forEach` and `putAllMap` methods, leading to a less wasteful implementation of `readValues` and `writeValues`. This is tested inside the `withMicrometerTest` testSet. --- reactor-core/build.gradle | 2 + .../util/context/ReactorContextAccessor.java | 12 +- .../context/ReactorContextAccessorTest.java | 107 ++++++++++++++++++ 3 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index e051692a5c..01b9bd6e1b 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -45,6 +45,7 @@ ext { testSets { blockHoundTest + //TODO once can probably be removed in 3.6.0, but MUST keep the ReactorContextAccessorTest in that case withMicrometerTest tckTest } @@ -120,6 +121,7 @@ dependencies { withMicrometerTestImplementation platform(libs.micrometer.bom) withMicrometerTestImplementation libs.micrometer.commons withMicrometerTestImplementation libs.micrometer.core + withMicrometerTestImplementation libs.micrometer.contextPropagationApi withMicrometerTestImplementation sourceSets.test.output jcstressImplementation(project(":reactor-test")) { diff --git a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java index c2c21d0d97..051b3c4b57 100644 --- a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java +++ b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java @@ -29,6 +29,7 @@ * SPI library, which is an optional dependency. * * @author Rossen Stoyanchev + * @author Simon Baslé * @since 3.5.0 */ public final class ReactorContextAccessor implements ContextAccessor { @@ -40,9 +41,11 @@ public boolean canReadFrom(Class contextType) { @Override public void readValues(ContextView source, Predicate keyPredicate, Map target) { - source.stream() - .filter(entry -> keyPredicate.test(entry.getKey())) - .forEach(entry -> target.put(entry.getKey(), entry.getValue())); + source.forEach((k, v) -> { + if (keyPredicate.test(k)) { + target.put(k, v); + } + }); } @Override @@ -52,7 +55,6 @@ public boolean canWriteTo(Class contextType) { @Override public Context writeValues(Map source, Context target) { - return target.putAll(Context.of(source).readOnly()); + return target.putAllMap(source); } - } diff --git a/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java b/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java new file mode 100644 index 0000000000..d7f23492ad --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.util.context; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +/** + * @author Simon Baslé + */ +class ReactorContextAccessorTest { + + ReactorContextAccessor accessor = new ReactorContextAccessor(); + + @Test + void canReadFromContext() { + assertThat(accessor.canReadFrom(Context.class)).isTrue(); + } + + @Test + void canReadFromContextView() { + assertThat(accessor.canReadFrom(ContextView.class)).isTrue(); + } + + @Test + void canWriteToContext() { + assertThat(accessor.canWriteTo(Context.class)).isTrue(); + } + + @Test + void cannotWriteToContextView() { + assertThat(accessor.canWriteTo(ContextView.class)).isFalse(); + } + + @Test + void readValuesUsesPredicateToIncludeValues() { + Map target = new HashMap<>(); + ContextView source = Context.of(1, "A", 2, "B", "3", "C"); + + accessor.readValues(source, Number.class::isInstance, target); + + assertThat(target) + .containsOnlyKeys(1, 2) + .containsEntry(1, "A") + .containsEntry(2, "B"); + } + + @Test + void readValuesDoesntUseStream() { + //we create a ForeignContext, which is a test suite non-final Context implementation + //so that Mockito will be able to spy it and assert reads don't rely on stream() + ContextView trueSource = new ContextTest.ForeignContext(1, "A"); + ContextView source = Mockito.spy(trueSource); + Map target = new HashMap<>(); + + accessor.readValues(source, v -> true, target); + + Mockito.verify(source, never()).stream(); + Mockito.verify(source, times(1)).forEach(any()); + } + + @Test + void writeValuesWithPutAllMap() { + //we create a ForeignContext, which is a test suite non-final Context implementation + //so that Mockito will be able to spy it and assert writes don't rely on putAll() + Context trueTarget = new ContextTest.ForeignContext(1, "A"); + Context target = Mockito.spy(trueTarget); + Map map = Collections.singletonMap(2, "B"); + + Context result = accessor.writeValues(map, target); + + assertThat(result) + .isNotSameAs(trueTarget) + .isNotSameAs(target); + + assertThat(result.hasKey(2)).as("contains key 2").isTrue(); + Object value2 = result.get(2); + assertThat(value2).as("value for key 2").isEqualTo("B"); + + Mockito.verify(target, never()).putAll((ContextView) any()); + Mockito.verify(target, times(1)).putAllMap(anyMap()); + } +} From 9de500c961e23ba1d64505d8cc438edfccf25742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 7 Jul 2022 17:13:17 +0200 Subject: [PATCH 035/312] Micrometer module: add observation(), deprecate global registry (#3104) This commit introduces `Micrometer.observation(ObservationRegistry)`, adds Context support to tap/`SignalListener` and deprecates a couple of methods. `.observation()` is a new tap-oriented listener similar to `.metrics()` which uses the Micrometer Observation API under the cover. An explicit registry is required to instantiate the listener. The MicrometerObservationListener: - covers the entire lifecycle of a Flux/Mono subscription with an Observation and associated Scope - uses context-propagation-api to ensure the Scope hierarchy is correct Compared to `.metrics()`, `.observation()` only concerns itself with subscription and termination events (cancel, onComplete, onError). It uses the `Observation#error(e)` mechanism to report onError cause. Spans names exactly reflect the one defined by the `.name()` operator (making use of the `Observation#contextualName` for that purpose). One big change in this commit is the ability for the `tap` operator to extract a redefined `Context` from its `SignalListener`, by calling new method `addToContext` after having invoked `doFirst()`. This gives the `.observation` an opportunity to update the context with a Scope it just opened in `doFirst`. The third part is the deprecation of `Micrometer#useRegistry` and `Micrometer#getRegistry` methods, in order to enforce explicit passing of a `MeterRegistry` (just like `ObservationRegistry` is required for `.observation()`). Note that `io.micrometer:context-propagation-api` is added as a direct dependency of the module. --- gradle/libs.versions.toml | 3 + reactor-core-micrometer/build.gradle | 4 + .../observability/micrometer/Micrometer.java | 56 ++- ...ener.java => MicrometerMeterListener.java} | 24 +- ...MicrometerMeterListenerConfiguration.java} | 28 +- ...va => MicrometerMeterListenerFactory.java} | 20 +- .../MicrometerObservationListener.java | 230 +++++++++++ ...meterObservationListenerConfiguration.java | 97 +++++ .../MicrometerObservationListenerFactory.java | 59 +++ ...ometerMeterListenerConfigurationTest.java} | 26 +- ...> MicrometerMeterListenerFactoryTest.java} | 25 +- ....java => MicrometerMeterListenerTest.java} | 74 ++-- .../MicrometerObservationIntegrationTest.java | 132 +++++++ ...rObservationListenerConfigurationTest.java | 209 ++++++++++ ...rometerObservationListenerFactoryTest.java | 82 ++++ .../MicrometerObservationListenerTest.java | 360 ++++++++++++++++++ .../micrometer/MicrometerTest.java | 4 +- .../core/observability/SignalListener.java | 14 + .../java/reactor/core/publisher/FluxTap.java | 19 +- 19 files changed, 1360 insertions(+), 106 deletions(-) rename reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/{MicrometerListener.java => MicrometerMeterListener.java} (92%) rename reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/{MicrometerListenerConfiguration.java => MicrometerMeterListenerConfiguration.java} (75%) rename reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/{MicrometerListenerFactory.java => MicrometerMeterListenerFactory.java} (65%) create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactory.java rename reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/{MicrometerListenerConfigurationTest.java => MicrometerMeterListenerConfigurationTest.java} (84%) rename reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/{MicrometerListenerFactoryTest.java => MicrometerMeterListenerFactoryTest.java} (74%) rename reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/{MicrometerListenerTest.java => MicrometerMeterListenerTest.java} (78%) create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3ee27bf0a..f2a6630a73 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,9 @@ micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micro micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagationApi = "io.micrometer:context-propagation-api:1.0.0-SNAPSHOT" +micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" +micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.6.1" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } reactiveStreams-tck = { module = "org.reactivestreams:reactive-streams-tck", version.ref = "reactiveStreams" } diff --git a/reactor-core-micrometer/build.gradle b/reactor-core-micrometer/build.gradle index 363ab47888..4c81a0a9d7 100644 --- a/reactor-core-micrometer/build.gradle +++ b/reactor-core-micrometer/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation platform(libs.micrometer.bom) api libs.micrometer.core + implementation libs.micrometer.contextPropagationApi testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" @@ -55,6 +56,9 @@ dependencies { testImplementation platform(libs.micrometer.bom) testImplementation libs.micrometer.core + testImplementation libs.micrometer.test + testImplementation libs.micrometer.observation.test + testImplementation libs.micrometer.tracing.test testImplementation(project(":reactor-test")) { exclude module: 'reactor-core' diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index f50a96e11e..bb117c900b 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -19,14 +19,18 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; -import reactor.core.observability.SignalListenerFactory; public final class Micrometer { @@ -34,15 +38,18 @@ public final class Micrometer { private static MeterRegistry registry = Metrics.globalRegistry; /** - * The default "name" to use as a prefix for meter IDs if the instrumented sequence doesn't + * The default "name" to use as a prefix for meter or observation IDs if the instrumented sequence doesn't * define a {@link reactor.core.publisher.Flux#name(String) name}. */ public static final String DEFAULT_METER_PREFIX = "reactor"; /** - * Set the registry to use in reactor for metrics related purposes. + * Set the registry to use in reactor-core-micrometer for metrics related purposes. * @return the previously configured registry. + * @deprecated in M4, will be removed in M5 / RC1. prefer your own singleton and explicitly + * passing the registry to {@link #metrics(MeterRegistry, Clock)} */ + @Deprecated public static MeterRegistry useRegistry(MeterRegistry newRegistry) { MeterRegistry previous = registry; registry = newRegistry; @@ -50,8 +57,12 @@ public static MeterRegistry useRegistry(MeterRegistry newRegistry) { } /** - * Get the registry used in reactor for metrics related purposes. + * Get the registry used in reactor-core-micrometer for metrics related purposes. + * + * @deprecated in M4, will be removed in M5 / RC1. prefer your own singleton and explicitly + * passing the registry to {@link #metrics(MeterRegistry, Clock)} */ + @Deprecated public static MeterRegistry getRegistry() { return registry; } @@ -72,9 +83,11 @@ public static MeterRegistry getRegistry() { * * @param the type of onNext in the target publisher * @return a {@link SignalListenerFactory} to record metrics + * @deprecated in M4, will be removed in M5 / RC1. prefer explicitly passing a registry via {@link #metrics(MeterRegistry, Clock)} */ + @Deprecated public static SignalListenerFactory metrics() { - return new MicrometerListenerFactory<>(); + return new MicrometerMeterListenerFactory<>(); } /** @@ -95,7 +108,7 @@ public static MeterRegistry getRegistry() { * @return a {@link SignalListenerFactory} to record metrics */ public static SignalListenerFactory metrics(MeterRegistry registry, Clock clock) { - return new MicrometerListenerFactory() { + return new MicrometerMeterListenerFactory() { @Override protected Clock useClock() { return clock; @@ -108,6 +121,37 @@ protected MeterRegistry useRegistry() { }; } + /** + * A {@link SignalListener} factory that will ultimately produce a Micrometer {@link Observation} + * representing the runtime of the publisher to the provided {@link ObservationRegistry}. + * To be used with either the {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} or + * {@link reactor.core.publisher.Mono#tap(SignalListenerFactory)} operator. + *

    + * The {@code NAME.observation.flow} {@link Observation} covers the entire length of the sequence, + * from subscription to termination. Said termination can be a cancellation, a completion with or without values + * or an error. This is denoted by the low cardinality {@code status} {@link KeyValue}. + * In case of an exception, a high cardinality {@code exception} KeyValue with the exception class name is also added. + * Finally, the low cardinality {@code type} KeyValue informs whether we're observing a {@code Flux} + * or a {@code Mono}. + *

    + * Note that the Micrometer {@code context-propagation-api} is used to populate thread locals + * around the opening of the observation (upon {@code onSubscribe(Subscription)}). + *

    + * Observation names are prefixed by the {@link reactor.core.publisher.Flux#name(String)} defined upstream + * of the tap if applicable or by the default prefix {@link #DEFAULT_METER_PREFIX}. + * Similarly, Reactor tags defined upstream via eg. {@link reactor.core.publisher.Flux#tag(String, String)}) + * are gathered and added to the default set of {@link io.micrometer.common.KeyValues} used by the Observation + * as {@link Observation#lowCardinalityKeyValues(KeyValues) low cardinality keyValues}. + * + * @param the type of onNext in the target publisher + * @return a {@link SignalListenerFactory} to record observations + */ + public static SignalListenerFactory observation(ObservationRegistry registry) { + return new MicrometerObservationListenerFactory<>(registry); + } + + //FIXME: remove these and replace with an option to decorate an arbitrary Scheduler + /** * Set-up a decorator that will instrument any {@link ExecutorService} that backs a reactor-core {@link Scheduler} * (or scheduler implementations which use {@link Schedulers#decorateExecutorService(Scheduler, ScheduledExecutorService)}). diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java similarity index 92% rename from reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListener.java rename to reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java index 576a9a1082..68ad175c2b 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java @@ -25,21 +25,21 @@ import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; +import reactor.core.observability.SignalListener; import reactor.core.publisher.Flux; import reactor.core.publisher.SignalType; import reactor.util.annotation.Nullable; -import reactor.core.observability.SignalListener; /** * A {@link SignalListener} that activates metrics gathering using Micrometer 1.x. * * @author Simon Baslé */ -final class MicrometerListener implements SignalListener { +final class MicrometerMeterListener implements SignalListener { - final MicrometerListenerConfiguration configuration; + final MicrometerMeterListenerConfiguration configuration; @Nullable - final DistributionSummary requestedCounter; + final DistributionSummary requestedCounter; @Nullable final Timer onNextIntervalTimer; @@ -47,7 +47,7 @@ final class MicrometerListener implements SignalListener { long lastNextEventNanos = -1L; boolean valued; - MicrometerListener(MicrometerListenerConfiguration configuration) { + MicrometerMeterListener(MicrometerMeterListenerConfiguration configuration) { this.configuration = configuration; this.valued = false; @@ -215,10 +215,16 @@ public void handleListenerError(Throwable listenerError) { static final Tags DEFAULT_TAGS_MONO = Tags.of("type", "Mono"); // === Operator === - static final Tag TAG_ON_ERROR = Tag.of("status", "error"); - static final Tags TAG_ON_COMPLETE = Tags.of("status", "completed", TAG_KEY_EXCEPTION, ""); - static final Tags TAG_ON_COMPLETE_EMPTY = Tags.of("status", "completedEmpty", TAG_KEY_EXCEPTION, ""); - static final Tags TAG_CANCEL = Tags.of("status", "cancelled", TAG_KEY_EXCEPTION, ""); + static final String TAG_KEY_STATUS = "status"; + static final String TAG_STATUS_CANCELLED = "cancelled"; + static final String TAG_STATUS_COMPLETED = "completed"; + static final String TAG_STATUS_COMPLETED_EMPTY = "completedEmpty"; + static final String TAG_STATUS_ERROR = "error"; + + static final Tag TAG_ON_ERROR = Tag.of(TAG_KEY_STATUS, TAG_STATUS_ERROR); + static final Tags TAG_ON_COMPLETE = Tags.of(TAG_KEY_STATUS, TAG_STATUS_COMPLETED, TAG_KEY_EXCEPTION, ""); + static final Tags TAG_ON_COMPLETE_EMPTY = Tags.of(TAG_KEY_STATUS, TAG_STATUS_COMPLETED_EMPTY, TAG_KEY_EXCEPTION, ""); + static final Tags TAG_CANCEL = Tags.of(TAG_KEY_STATUS, TAG_STATUS_CANCELLED, TAG_KEY_EXCEPTION, ""); /* * This method calls the registry, which can be costly. However the cancel signal is only expected diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java similarity index 75% rename from reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java rename to reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java index d3397df082..363e90686d 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerConfiguration.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java @@ -16,10 +16,7 @@ package reactor.core.observability.micrometer; -import java.util.LinkedList; import java.util.List; -import java.util.function.BiFunction; -import java.util.function.BinaryOperator; import java.util.stream.Collectors; import io.micrometer.core.instrument.Clock; @@ -33,32 +30,31 @@ import reactor.core.publisher.Mono; import reactor.util.Logger; import reactor.util.Loggers; -import reactor.util.function.Tuple2; /** - * A companion configuration object for {@link MicrometerListener} that serves as the state created by - * {@link MicrometerListenerFactory}. + * A companion configuration object for {@link MicrometerMeterListener} that serves as the state created by + * {@link MicrometerMeterListenerFactory}. * * @author Simon Baslé */ -final class MicrometerListenerConfiguration { +final class MicrometerMeterListenerConfiguration { - private static final Logger LOGGER = Loggers.getLogger(MicrometerListenerConfiguration.class); + private static final Logger LOGGER = Loggers.getLogger(MicrometerMeterListenerConfiguration.class); - static MicrometerListenerConfiguration fromFlux(Flux source, MeterRegistry meterRegistry, Clock clock) { - Tags defaultTags = MicrometerListener.DEFAULT_TAGS_FLUX; + static MicrometerMeterListenerConfiguration fromFlux(Flux source, MeterRegistry meterRegistry, Clock clock) { + Tags defaultTags = MicrometerMeterListener.DEFAULT_TAGS_FLUX; final String name = resolveName(source, LOGGER); final Tags tags = resolveTags(source, defaultTags); - return new MicrometerListenerConfiguration(name, tags, meterRegistry, clock, false); + return new MicrometerMeterListenerConfiguration(name, tags, meterRegistry, clock, false); } - static MicrometerListenerConfiguration fromMono(Mono source, MeterRegistry meterRegistry, Clock clock) { - Tags defaultTags = MicrometerListener.DEFAULT_TAGS_MONO; + static MicrometerMeterListenerConfiguration fromMono(Mono source, MeterRegistry meterRegistry, Clock clock) { + Tags defaultTags = MicrometerMeterListener.DEFAULT_TAGS_MONO; final String name = resolveName(source, LOGGER); final Tags tags = resolveTags(source, defaultTags); - return new MicrometerListenerConfiguration(name, tags, meterRegistry, clock, true); + return new MicrometerMeterListenerConfiguration(name, tags, meterRegistry, clock, true); } /** @@ -116,8 +112,8 @@ static Tags resolveTags(Publisher source, Tags tags) { // separator is the dot, not camelCase... final MeterRegistry registry; - MicrometerListenerConfiguration(String sequenceName, Tags tags, MeterRegistry registryCandidate, Clock clock, - boolean isMono) { + MicrometerMeterListenerConfiguration(String sequenceName, Tags tags, MeterRegistry registryCandidate, Clock clock, + boolean isMono) { this.clock = clock; this.commonTags = tags; this.isMono = isMono; diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerFactory.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java similarity index 65% rename from reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerFactory.java rename to reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java index 808c803ea1..4c9722ca7b 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerListenerFactory.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java @@ -20,18 +20,18 @@ import io.micrometer.core.instrument.MeterRegistry; import org.reactivestreams.Publisher; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.context.ContextView; -import reactor.core.observability.SignalListener; -import reactor.core.observability.SignalListenerFactory; /** - * A {@link SignalListenerFactory} for {@link MicrometerListener}. + * A {@link SignalListenerFactory} for {@link MicrometerMeterListener}. * * @author Simon Baslé */ -class MicrometerListenerFactory implements SignalListenerFactory { +class MicrometerMeterListenerFactory implements SignalListenerFactory { protected Clock useClock() { return Clock.SYSTEM; @@ -42,21 +42,21 @@ protected MeterRegistry useRegistry() { } @Override - public MicrometerListenerConfiguration initializePublisherState(Publisher source) { + public MicrometerMeterListenerConfiguration initializePublisherState(Publisher source) { if (source instanceof Mono) { - return MicrometerListenerConfiguration.fromMono((Mono) source, useRegistry(), useClock()); + return MicrometerMeterListenerConfiguration.fromMono((Mono) source, useRegistry(), useClock()); } else if (source instanceof Flux) { - return MicrometerListenerConfiguration.fromFlux((Flux) source, useRegistry(), useClock()); + return MicrometerMeterListenerConfiguration.fromFlux((Flux) source, useRegistry(), useClock()); } else { - throw new IllegalArgumentException("MicrometerListenerFactory must only be used via the tap operator / with a Flux or Mono"); + throw new IllegalArgumentException("MicrometerMeterListenerFactory must only be used via the tap operator / with a Flux or Mono"); } } @Override public SignalListener createListener(Publisher source, ContextView listenerContext, - MicrometerListenerConfiguration publisherContext) { - return new MicrometerListener<>(publisherContext); + MicrometerMeterListenerConfiguration publisherContext) { + return new MicrometerMeterListener<>(publisherContext); } } diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java new file mode 100644 index 0000000000..2a4e01b2e2 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.context.ContextSnapshot; +import io.micrometer.observation.Observation; + +import reactor.core.observability.SignalListener; +import reactor.core.publisher.SignalType; +import reactor.util.Logger; +import reactor.util.Loggers; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +/** + * A {@link SignalListener} that makes timings using the {@link io.micrometer.observation.Observation} API from Micrometer 1.10. + *

    + * This is a bare-bone version compared to {@link MicrometerMeterListener}, as it only opens a single Observation for the + * duration of the Flux/Mono. + * + * @author Simon Baslé + */ +final class MicrometerObservationListener implements SignalListener { + + private static final Logger LOGGER = Loggers.getLogger(MicrometerObservationListener.class); + + static final String OBSERVATION_FLOW = ".observation.flow"; + + /** + * A value for the status tag, to be used when a Mono completes from onNext. + * In production, this is set to {@link MicrometerMeterListener#TAG_STATUS_COMPLETED}. + * In some tests, this can be overridden as a way to assert {@link #doOnComplete()} is no-op. + */ + final String completedOnNextStatus; + final MicrometerObservationListenerConfiguration configuration; + final ContextView originalContext; + final Observation subscribeToTerminalObservation; + + @Nullable + Context contextWithScope; + @Nullable + Observation.Scope scope = null; + + boolean valued; + + MicrometerObservationListener(ContextView subscriberContext, MicrometerObservationListenerConfiguration configuration) { + this(subscriberContext, configuration, MicrometerMeterListener.TAG_STATUS_COMPLETED); + } + + //for test purposes, we can pass in a value for the status tag, to be used when a Mono completes from onNext + MicrometerObservationListener(ContextView subscriberContext, MicrometerObservationListenerConfiguration configuration, String completedOnNextStatus) { + this.configuration = configuration; + this.originalContext = subscriberContext; + this.completedOnNextStatus = completedOnNextStatus; + + this.valued = false; + + //creation of the listener matches subscription (Publisher.subscribe(Subscriber) / doFirst) + //while doOnSubscription matches the moment where the Publisher acknowledges said subscription + subscribeToTerminalObservation = Observation.createNotStarted( + configuration.sequenceName + OBSERVATION_FLOW, + configuration.registry + ) + .contextualName(configuration.sequenceName) + .lowCardinalityKeyValues(configuration.commonKeyValues); + } + + @Override + public void doFirst() { + ContextSnapshot contextSnapshot = ContextSnapshot.forContextAndThreadLocalValues(this.originalContext); + + try (ContextSnapshot.Scope ignored = contextSnapshot.setThreadLocalValues()) { + this.scope = this.subscribeToTerminalObservation + .start() + .openScope(); + //reacquire the scope from ThreadLocal + //tap context hasn't been initialized yet, so addToContext can now use the Scope + ContextSnapshot contextSnapshot2 = ContextSnapshot.forContextAndThreadLocalValues(this.originalContext); + this.contextWithScope = contextSnapshot2.updateContext(Context.of(this.originalContext)); + } + } + + @Override + public Context addToContext(Context originalContext) { + if (this.originalContext != originalContext) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("addToContext call on Observation {} with unexpected originalContext {}", + this.subscribeToTerminalObservation, originalContext); + } + return originalContext; + } + if (this.contextWithScope == null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("addToContext call on Observation {} before contextWithScope is set", + this.subscribeToTerminalObservation); + } + return originalContext; + } + return contextWithScope; + } + + @Override + public void doOnCancel() { + Observation observation = subscribeToTerminalObservation + .lowCardinalityKeyValue(MicrometerMeterListener.TAG_KEY_STATUS, MicrometerMeterListener.TAG_STATUS_CANCELLED); + + observation.stop(); + if (scope != null) { + scope.close(); + } + } + + @Override + public void doOnComplete() { + // We differentiate between empty completion and value completion via tags. + String status = null; + if (!valued) { + status = MicrometerMeterListener.TAG_STATUS_COMPLETED_EMPTY; + } + else if (!configuration.isMono) { + status = MicrometerMeterListener.TAG_STATUS_COMPLETED; + } + + // if status == null, recording with OnComplete tag is done directly in onNext for the Mono(valued) case + if (status != null) { + Observation completeObservation = subscribeToTerminalObservation + .lowCardinalityKeyValue(MicrometerMeterListener.TAG_KEY_STATUS, status); + + completeObservation.stop(); + if (scope != null) { + scope.close(); + } + } + } + + @Override + public void doOnError(Throwable e) { + Observation errorObservation = subscribeToTerminalObservation + .lowCardinalityKeyValue(MicrometerMeterListener.TAG_KEY_STATUS, MicrometerMeterListener.TAG_STATUS_ERROR) + .error(e); + + errorObservation.stop(); + if (scope != null) { + scope.close(); + } + } + + @Override + public void doOnNext(T t) { + valued = true; + if (configuration.isMono) { + //record valued completion directly + Observation completeObservation = subscribeToTerminalObservation + .lowCardinalityKeyValue(MicrometerMeterListener.TAG_KEY_STATUS, completedOnNextStatus); + + completeObservation.stop(); + if (scope != null) { + scope.close(); + } + } + } + + @Override + public void handleListenerError(Throwable listenerError) { + LOGGER.error("unhandled listener error", listenerError); + } + + //unused hooks + + @Override + public void doOnSubscription() { + // NO-OP. We rather initialize everything in `doFirst`, as it is closer to actual Publisher.subscriber call + // and gives us a chance to store the Scope in the SignalListener's context. + } + + @Override + public void doOnMalformedOnComplete() { + //NO-OP + } + + @Override + public void doOnMalformedOnError(Throwable e) { + // NO-OP + } + + @Override + public void doOnMalformedOnNext(T value) { + // NO-OP + } + + @Override + public void doOnRequest(long l) { + // NO-OP + } + + @Override + public void doOnFusion(int negotiatedFusion) { + // NO-OP + } + + @Override + public void doFinally(SignalType terminationType) { + // NO-OP + } + + @Override + public void doAfterComplete() { + // NO-OP + } + + @Override + public void doAfterError(Throwable error) { + // NO-OP + } +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java new file mode 100644 index 0000000000..34013e4475 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.util.List; +import java.util.stream.Collectors; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.ObservationRegistry; +import org.reactivestreams.Publisher; + +import reactor.core.Scannable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; + +/** + * A companion configuration object for {@link MicrometerObservationListener} that serves as the state created by + * {@link MicrometerObservationListenerFactory}. + * + * @author Simon Baslé + */ +final class MicrometerObservationListenerConfiguration { + + static final KeyValues DEFAULT_KV_FLUX = KeyValues.of("type", "Flux"); + static final KeyValues DEFAULT_KV_MONO = KeyValues.of("type", "Mono"); + + private static final Logger LOGGER = Loggers.getLogger(MicrometerObservationListenerConfiguration.class); + + static MicrometerObservationListenerConfiguration fromFlux(Flux source, ObservationRegistry observationRegistry) { + KeyValues defaultKeyValues = DEFAULT_KV_FLUX; + final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER); + final KeyValues keyValues = resolveKeyValues(source, defaultKeyValues); + + return new MicrometerObservationListenerConfiguration(name, keyValues, observationRegistry, false); + } + + static MicrometerObservationListenerConfiguration fromMono(Mono source, ObservationRegistry observationRegistry) { + KeyValues defaultKeyValues = DEFAULT_KV_MONO; + final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER); + final KeyValues keyValues = resolveKeyValues(source, defaultKeyValues); + + return new MicrometerObservationListenerConfiguration(name, keyValues, observationRegistry, true); + } + + /** + * Extract the "tags" from the upstream as {@link KeyValues}. + * + * @param source the upstream + * + * @return a {@link KeyValues} collection + */ + static KeyValues resolveKeyValues(Publisher source, KeyValues tags) { + Scannable scannable = Scannable.from(source); + + if (scannable.isScanAvailable()) { + List discoveredTags = scannable.tagsDeduplicated() + .entrySet().stream() + .map(e -> KeyValue.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + return tags.and(discoveredTags); + } + + return tags; + } + + final KeyValues commonKeyValues; + final boolean isMono; + final String sequenceName; + + final ObservationRegistry registry; + + MicrometerObservationListenerConfiguration(String sequenceName, KeyValues commonKeyValues, + ObservationRegistry registryCandidate, + boolean isMono) { + this.commonKeyValues = commonKeyValues; + this.isMono = isMono; + this.sequenceName = sequenceName; + this.registry = registryCandidate; + } +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactory.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactory.java new file mode 100644 index 0000000000..06213da981 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.observation.ObservationRegistry; +import org.reactivestreams.Publisher; + +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** + * A {@link SignalListenerFactory} for {@link MicrometerObservationListener}. + * + * @author Simon Baslé + */ +class MicrometerObservationListenerFactory implements SignalListenerFactory { + + final ObservationRegistry registry; + + public MicrometerObservationListenerFactory(ObservationRegistry registry) { + this.registry = registry; + } + + @Override + public MicrometerObservationListenerConfiguration initializePublisherState(Publisher source) { + if (source instanceof Mono) { + return MicrometerObservationListenerConfiguration.fromMono((Mono) source, this.registry); + } + else if (source instanceof Flux) { + return MicrometerObservationListenerConfiguration.fromFlux((Flux) source, this.registry); + } + else { + throw new IllegalArgumentException("MicrometerObservationListenerFactory must only be used via the tap operator / with a Flux or Mono"); + } + } + + @Override + public SignalListener createListener(Publisher source, ContextView listenerContext, + MicrometerObservationListenerConfiguration publisherContext) { + return new MicrometerObservationListener<>(listenerContext, publisherContext); + } +} diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java similarity index 84% rename from reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java rename to reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java index 83acffaa68..f858b0962f 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerConfigurationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java @@ -36,7 +36,7 @@ /** * @author Simon Baslé */ -class MicrometerListenerConfigurationTest { +class MicrometerMeterListenerConfigurationTest { @ParameterizedTestWithName @CsvSource(value = { @@ -58,7 +58,7 @@ void fromFlux(@Nullable String name, @Nullable String tag) { flux = flux.tag("tag", tag); } - MicrometerListenerConfiguration configuration = MicrometerListenerConfiguration.fromFlux(flux, expectedRegistry, expectedClock); + MicrometerMeterListenerConfiguration configuration = MicrometerMeterListenerConfiguration.fromFlux(flux, expectedRegistry, expectedClock); assertThat(configuration.clock).as("clock").isSameAs(expectedClock); assertThat(configuration.registry).as("registry").isSameAs(expectedRegistry); @@ -100,7 +100,7 @@ void fromMono(@Nullable String name, @Nullable String tag) { mono = mono.tag("tag", tag); } - MicrometerListenerConfiguration configuration = MicrometerListenerConfiguration.fromMono(mono, expectedRegistry, expectedClock); + MicrometerMeterListenerConfiguration configuration = MicrometerMeterListenerConfiguration.fromMono(mono, expectedRegistry, expectedClock); assertThat(configuration.clock).as("clock").isSameAs(expectedClock); assertThat(configuration.registry).as("registry").isSameAs(expectedRegistry); @@ -127,7 +127,7 @@ void resolveName_notSet() { TestLogger logger = new TestLogger(false); Flux flux = Flux.just(1); - String resolvedName = MicrometerListenerConfiguration.resolveName(flux, logger); + String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger); assertThat(resolvedName).isEqualTo(Micrometer.DEFAULT_METER_PREFIX); assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); @@ -138,7 +138,7 @@ void resolveName_setRightAbove() { TestLogger logger = new TestLogger(false); Flux flux = Flux.just(1).name("someName"); - String resolvedName = MicrometerListenerConfiguration.resolveName(flux, logger); + String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger); assertThat(resolvedName).isEqualTo("someName"); assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); @@ -149,7 +149,7 @@ void resolveName_setHigherAbove() { TestLogger logger = new TestLogger(false); Flux flux = Flux.just(1).name("someName").filter(i -> i % 2 == 0).map(i -> i + 10); - String resolvedName = MicrometerListenerConfiguration.resolveName(flux, logger); + String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger); assertThat(resolvedName).isEqualTo("someName"); assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); @@ -160,7 +160,7 @@ void resolveName_notScannable() { TestLogger testLogger = new TestLogger(false); Publisher publisher = Operators::complete; - String resolvedName = MicrometerListenerConfiguration.resolveName(publisher, testLogger); + String resolvedName = MicrometerMeterListenerConfiguration.resolveName(publisher, testLogger); assertThat(resolvedName).as("resolved name").isEqualTo(Micrometer.DEFAULT_METER_PREFIX); assertThat(testLogger.getErrContent()).contains("Attempting to activate metrics but the upstream is not Scannable. You might want to use `name()` (and optionally `tags()`) right before this listener"); @@ -171,7 +171,7 @@ void resolveTags_notSet() { Tags defaultTags = Tags.of("common1", "commonValue1"); Flux flux = Flux.just(1); - Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + Tags resolvedTags = MicrometerMeterListenerConfiguration.resolveTags(flux, defaultTags); assertThat(resolvedTags.stream().map(Object::toString)) .containsExactly("tag(common1=commonValue1)"); @@ -184,7 +184,7 @@ void resolveTags_setRightAbove() { .just(1) .tag("k1", "v1"); - Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + Tags resolvedTags = MicrometerMeterListenerConfiguration.resolveTags(flux, defaultTags); assertThat(resolvedTags.stream().map(Object::toString)).containsExactlyInAnyOrder( "tag(common1=commonValue1)", @@ -201,7 +201,7 @@ void resolveTags_setHigherAbove() { .filter(i -> i % 2 == 0) .map(i -> i + 10); - Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + Tags resolvedTags = MicrometerMeterListenerConfiguration.resolveTags(flux, defaultTags); assertThat(resolvedTags.stream().map(Object::toString)).containsExactlyInAnyOrder( "tag(common1=commonValue1)", @@ -218,7 +218,7 @@ void resolveTags_multipleScatteredTagsSetAbove() { .tag("k2", "v2") .map(i -> i + 10); - Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + Tags resolvedTags = MicrometerMeterListenerConfiguration.resolveTags(flux, defaultTags); assertThat(resolvedTags.stream().map(Object::toString)).containsExactlyInAnyOrder( "tag(common1=commonValue1)", @@ -237,7 +237,7 @@ void resolveTags_multipleScatteredTagsSetAboveWithDeduplication() { .tag("k2", "v2") .map(i -> i + 10); - Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(flux, defaultTags); + Tags resolvedTags = MicrometerMeterListenerConfiguration.resolveTags(flux, defaultTags); assertThat(resolvedTags.stream().map(Object::toString)).containsExactly( "tag(common1=commonValue1)", @@ -251,7 +251,7 @@ void resolveTags_notScannable() { Tags defaultTags = Tags.of("common1", "commonValue1"); Publisher publisher = Operators::complete; - Tags resolvedTags = MicrometerListenerConfiguration.resolveTags(publisher, defaultTags); + Tags resolvedTags = MicrometerMeterListenerConfiguration.resolveTags(publisher, defaultTags); assertThat(resolvedTags.stream().map(Object::toString)).containsExactly("tag(common1=commonValue1)"); } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerFactoryTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactoryTest.java similarity index 74% rename from reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerFactoryTest.java rename to reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactoryTest.java index f2680069f3..e3a10926d5 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerFactoryTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactoryTest.java @@ -22,11 +22,11 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; +import reactor.core.observability.SignalListener; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; import reactor.util.context.Context; -import reactor.core.observability.SignalListener; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -34,11 +34,11 @@ /** * @author Simon Baslé */ -class MicrometerListenerFactoryTest { +class MicrometerMeterListenerFactoryTest { @Test void useClockDefaultsToSystemClock() { - MicrometerListenerFactory factory = new MicrometerListenerFactory<>(); + MicrometerMeterListenerFactory factory = new MicrometerMeterListenerFactory<>(); assertThat(factory.useClock()).isSameAs(Clock.SYSTEM); } @@ -48,7 +48,7 @@ void useRegistryDefaultsToCommonRegistry() { SimpleMeterRegistry commonRegistry = new SimpleMeterRegistry(); MeterRegistry defaultCommon = Micrometer.useRegistry(commonRegistry); try { - MicrometerListenerFactory factory = new MicrometerListenerFactory<>(); + MicrometerMeterListenerFactory factory = new MicrometerMeterListenerFactory<>(); assertThat(factory.useRegistry()).isSameAs(Micrometer.getRegistry()) .isSameAs(commonRegistry); @@ -60,7 +60,7 @@ void useRegistryDefaultsToCommonRegistry() { @Test void configurationFromMono() { - MicrometerListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Mono.just(1)); + MicrometerMeterListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Mono.just(1)); assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); assertThat(configuration.clock).as("clock").isSameAs(CUSTOM_CLOCK); @@ -70,7 +70,7 @@ void configurationFromMono() { @Test void configurationFromFlux() { - MicrometerListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Flux.just(1, 2)); + MicrometerMeterListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Flux.just(1, 2)); assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); assertThat(configuration.clock).as("clock").isSameAs(CUSTOM_CLOCK); @@ -82,17 +82,17 @@ void configurationFromFlux() { void configurationFromGenericPublisherIsRejected() { assertThatIllegalArgumentException() .isThrownBy(() -> CUSTOM_FACTORY.initializePublisherState(Operators::complete)) - .withMessage("MicrometerListenerFactory must only be used via the tap operator / with a Flux or Mono"); + .withMessage("MicrometerMeterListenerFactory must only be used via the tap operator / with a Flux or Mono"); } @Test void createListenerOfTypeMicrometer() { Publisher source = Mono.just(1); - MicrometerListenerConfiguration conf = CUSTOM_FACTORY.initializePublisherState(source); + MicrometerMeterListenerConfiguration conf = CUSTOM_FACTORY.initializePublisherState(source); SignalListener signalListener = CUSTOM_FACTORY.createListener(source, Context.empty(), conf); - assertThat(signalListener).isInstanceOf(MicrometerListener.class); - assertThat(((MicrometerListener) signalListener).configuration).as("configuration").isSameAs(conf); + assertThat(signalListener).isInstanceOf(MicrometerMeterListener.class); + assertThat(((MicrometerMeterListener) signalListener).configuration).as("configuration").isSameAs(conf); } protected static final Clock CUSTOM_CLOCK = new Clock() { @@ -106,8 +106,9 @@ public long monotonicTime() { return 0; } }; - protected static final SimpleMeterRegistry CUSTOM_REGISTRY = new SimpleMeterRegistry(); - protected static final MicrometerListenerFactory CUSTOM_FACTORY = new MicrometerListenerFactory() { + protected static final SimpleMeterRegistry CUSTOM_REGISTRY = new SimpleMeterRegistry(); + protected static final MicrometerMeterListenerFactory + CUSTOM_FACTORY = new MicrometerMeterListenerFactory() { @Override protected Clock useClock() { return CUSTOM_CLOCK; diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java similarity index 78% rename from reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerTest.java rename to reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java index a5d6812039..3410668065 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java @@ -33,12 +33,12 @@ /** * @author Simon Baslé */ -class MicrometerListenerTest { +class MicrometerMeterListenerTest { SimpleMeterRegistry registry; AtomicLong virtualClockTime; - Clock virtualClock; - MicrometerListenerConfiguration configuration; + Clock virtualClock; + MicrometerMeterListenerConfiguration configuration; @BeforeEach void initRegistry() { @@ -55,7 +55,7 @@ public long monotonicTime() { return virtualClockTime.get(); } }; - configuration = new MicrometerListenerConfiguration( + configuration = new MicrometerMeterListenerConfiguration( "testName", Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -65,14 +65,14 @@ public long monotonicTime() { @Test void initialStateFluxWithDefaultName() { - configuration = new MicrometerListenerConfiguration( + configuration = new MicrometerMeterListenerConfiguration( Micrometer.DEFAULT_METER_PREFIX, Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, virtualClock, false); - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); assertThat(listener.valued).as("valued").isFalse(); assertThat(listener.requestedCounter).as("requestedCounter disabled").isNull(); @@ -91,14 +91,14 @@ void initialStateFluxWithDefaultName() { @Test void initialStateFluxWithCustomName() { - configuration = new MicrometerListenerConfiguration( + configuration = new MicrometerMeterListenerConfiguration( "testName", Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, virtualClock, false); - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); assertThat(listener.valued).as("valued").isFalse(); assertThat(listener.requestedCounter).as("requestedCounter").isNotNull(); @@ -122,14 +122,14 @@ void initialStateFluxWithCustomName() { @Test void initialStateMono() { - configuration = new MicrometerListenerConfiguration( + configuration = new MicrometerMeterListenerConfiguration( Micrometer.DEFAULT_METER_PREFIX, Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, virtualClock, true); - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); assertThat(listener.valued).as("valued").isFalse(); assertThat(listener.requestedCounter).as("requestedCounter disabled").isNull(); @@ -142,7 +142,7 @@ void initialStateMono() { @Test void timerSampleInitializedInSubscription() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); assertThat(listener.subscribeToTerminateSample) .as("subscribeToTerminateSample pre subscription") @@ -166,7 +166,7 @@ void timerSampleInitializedInSubscription() { assertThat(registry.getMeters()) .as("meters post subscription") .hasSize(3); - assertThat(registry.find("testName" + MicrometerListener.METER_SUBSCRIBED).counter()) + assertThat(registry.find("testName" + MicrometerMeterListener.METER_SUBSCRIBED).counter()) .as("meter .subscribed") .isNotNull() .satisfies(meter -> assertThat(meter.count()).isEqualTo(1d)); @@ -174,14 +174,14 @@ void timerSampleInitializedInSubscription() { @Test void doOnCancelTimesFlowDurationMeter() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); assertThat(registry.getMeters()).hasSize(3); virtualClockTime.set(100); listener.doOnCancel(); - Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) .timer(); assertThat(registry.getMeters()).hasSize(4); @@ -195,14 +195,14 @@ void doOnCancelTimesFlowDurationMeter() { @Test void doOnCompleteTimesFlowDurationMeter_completeEmpty() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); assertThat(registry.getMeters()).hasSize(3); virtualClockTime.set(100); listener.doOnComplete(); - Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) .timer(); assertThat(registry.getMeters()).hasSize(4); @@ -216,7 +216,7 @@ void doOnCompleteTimesFlowDurationMeter_completeEmpty() { @Test void doOnCompleteTimesFlowDurationMeter_completeValued() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); assertThat(registry.getMeters()).hasSize(3); @@ -224,7 +224,7 @@ void doOnCompleteTimesFlowDurationMeter_completeValued() { listener.valued = true; listener.doOnComplete(); - Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) .timer(); assertThat(registry.getMeters()).hasSize(4); @@ -238,14 +238,14 @@ void doOnCompleteTimesFlowDurationMeter_completeValued() { @Test void doOnErrorTimesFlowDurationMeter() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); assertThat(registry.getMeters()).hasSize(3); virtualClockTime.set(100); listener.doOnError(new IllegalStateException("expected")); - Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) .timer(); assertThat(registry.getMeters()).hasSize(4); @@ -259,7 +259,7 @@ void doOnErrorTimesFlowDurationMeter() { @Test void doOnNextRecordsInterval() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); virtualClockTime.set(100); @@ -268,7 +268,7 @@ void doOnNextRecordsInterval() { assertThat(listener.valued).as("valued").isTrue(); assertThat(listener.lastNextEventNanos).as("lastEventNanos recorded").isEqualTo(100); - assertThat(registry.find("testName" + MicrometerListener.METER_FLOW_DURATION).meters()) + assertThat(registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION).meters()) .as("no flow.duration meter yet") .isEmpty(); @@ -283,9 +283,9 @@ void doOnNextRecordsInterval() { @Test void doOnNextRecordsInterval_defaultName() { - configuration = new MicrometerListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), + configuration = new MicrometerMeterListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), registry, virtualClock, false); - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); virtualClockTime.set(100); @@ -294,7 +294,7 @@ void doOnNextRecordsInterval_defaultName() { assertThat(listener.valued).as("valued").isTrue(); assertThat(listener.lastNextEventNanos).as("lastEventNanos recorded").isEqualTo(100); - assertThat(registry.find(Micrometer.DEFAULT_METER_PREFIX + MicrometerListener.METER_FLOW_DURATION).meters()) + assertThat(registry.find(Micrometer.DEFAULT_METER_PREFIX + MicrometerMeterListener.METER_FLOW_DURATION).meters()) .as("no flow.duration meter yet") .isEmpty(); @@ -308,9 +308,9 @@ void doOnNextRecordsInterval_defaultName() { @Test void doOnNext_monoRecordsCompletionOnly() { - configuration = new MicrometerListenerConfiguration("testName", Tags.empty(), + configuration = new MicrometerMeterListenerConfiguration("testName", Tags.empty(), registry, virtualClock, true); - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); @@ -320,7 +320,7 @@ void doOnNext_monoRecordsCompletionOnly() { assertThat(listener.valued).as("valued").isTrue(); assertThat(listener.lastNextEventNanos).as("no lastEventNanos recorded").isZero(); - Timer timer = registry.find("testName" + MicrometerListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) .timer(); assertThat(timer.getId().toString()) @@ -333,7 +333,7 @@ void doOnNext_monoRecordsCompletionOnly() { @Test void doOnNextMultipleRecords() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); virtualClockTime.set(100); @@ -354,7 +354,7 @@ void doOnNextMultipleRecords() { @Test void doOnRequestRecordsTotalDemand() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnRequest(100L); assertThat(listener.requestedCounter.count()).as("1 request calls").isEqualTo(1); @@ -369,30 +369,30 @@ void doOnRequestRecordsTotalDemand() { @Test void doOnRequestMonoIgnoresRequest() { - configuration = new MicrometerListenerConfiguration("testName", Tags.empty(), registry, virtualClock, true); - MicrometerListener listener = new MicrometerListener<>(configuration); + configuration = new MicrometerMeterListenerConfiguration("testName", Tags.empty(), registry, virtualClock, true); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); assertThatCode(() -> listener.doOnRequest(100L)).doesNotThrowAnyException(); assertThat(listener.requestedCounter).isNull(); } @Test void doOnRequestDefaultNameIgnoresRequest() { - configuration = new MicrometerListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), registry, virtualClock, false); - MicrometerListener listener = new MicrometerListener<>(configuration); + configuration = new MicrometerMeterListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), registry, virtualClock, false); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); assertThatCode(() -> listener.doOnRequest(100L)).doesNotThrowAnyException(); assertThat(listener.requestedCounter).isNull(); } @Test void malformedCounterCapturesNextCompleteError() { - MicrometerListener listener = new MicrometerListener<>(configuration); + MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); - Counter malformedCounter = registry.find("testName" + MicrometerListener.METER_MALFORMED).counter(); + Counter malformedCounter = registry.find("testName" + MicrometerMeterListener.METER_MALFORMED).counter(); assertThat(malformedCounter).as("counter not registered").isNull(); listener.doOnMalformedOnNext(123); - malformedCounter = registry.find("testName" + MicrometerListener.METER_MALFORMED).counter(); + malformedCounter = registry.find("testName" + MicrometerMeterListener.METER_MALFORMED).counter(); assertThat(malformedCounter).as("lazy counter registration").isNotNull(); assertThat(malformedCounter.count()).as("onNext malformed").isOne(); diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java new file mode 100644 index 0000000000..f2c09d2f43 --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import io.micrometer.tracing.Span; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.simple.SpansAssert; +import org.junit.jupiter.api.Tag; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import static org.assertj.core.api.Assertions.assertThat; +import static reactor.core.observability.micrometer.MicrometerMeterListener.TAG_KEY_STATUS; +import static reactor.core.observability.micrometer.MicrometerMeterListener.TAG_STATUS_ERROR; + +/** + * @author Simon Baslé + */ +@Tag("slow") +public class MicrometerObservationIntegrationTest extends SampleTestRunner { + + MicrometerObservationIntegrationTest() { + super(SampleTestRunner.SampleRunnerConfig.builder() + .build()); + } + + @Override + public SampleTestRunnerConsumer yourCode() throws Exception { + final Scheduler delayScheduler = Schedulers.newSingle("test"); + final IllegalStateException EXCEPTION = new IllegalStateException("expected error"); + return (bb, meterRegistry) -> { + Span beforeStart = bb.getTracer().currentSpan(); + + Function> querySimulator = id -> + Mono.delay(Duration.ofMillis(500), delayScheduler) + .tag("endpoint", "simulated/" + id) + .map(ignored -> "query for id " + id) + .doOnNext(v -> { + if (id == 2L) throw EXCEPTION; + }) + //FIXME enforce providing a name via String or ObservationConvention + //FIXME Micrometer taps should ignore name() + .name("query" + id) + .tap(Micrometer.observation(getObservationRegistry())); + + Flux.range(0, 100) + .name("testFlux") + .tag("interval", "500ms") + .take(3) + .tag("size", "3") + .concatMap(querySimulator) + .tap(Micrometer.observation(getObservationRegistry())) + .onErrorReturn("ended with error") // prevent error throwing. the tap should still get notified + .blockLast(); + + SpansAssert spansAssert = SpansAssert.assertThat(bb.getFinishedSpans()); + SpansAssert.SpansAssertReturningAssert assertThatMain = spansAssert.assertThatASpanWithNameEqualTo("test-flux"); + SpansAssert.SpansAssertReturningAssert assertThatQuery2 = spansAssert.assertThatASpanWithNameEqualTo("query2"); + + spansAssert.hasSize(4); + + assertThatMain + //FIXME reactor-defined tags should have a reactor. prefix + //FIXME reactor-defined Tags and KeyValues should be Documented + .hasTag(TAG_KEY_STATUS, TAG_STATUS_ERROR) + .hasTag("type", "Flux") + .hasTag("interval", "500ms") + .hasTag("size", "3") + //TODO propose new duration assertion? span's timestamps should return Instant, not long. OTel is using nanos, Brave is storing long microsecond +// .satisfies(span -> assertThat(Duration.ofNanos(span.getEndTimestamp() - span.getStartTimestamp())) +// .as("duration") +// .isGreaterThanOrEqualTo(Duration.ofMillis(1500)) +// ) + //OTEL doesn't really capture the exception type, only the message + .thenThrowable().hasMessage(EXCEPTION.getMessage()); + + //query2 span + assertThatQuery2 + .hasTag("endpoint", "simulated/2") + .thenThrowable().hasMessage(EXCEPTION.getMessage()); + + //quick assert query0 and query1 + spansAssert + .thenASpanWithNameEqualTo("query0") + .doesNotHaveEventWithNameEqualTo("exception") + .hasTag("endpoint", "simulated/0") + .backToSpans() + .hasASpanWithName("query1"); + + assertThat(bb.getTracer().currentSpan()) + .as("no leftover span in main thread") + .isNotSameAs(beforeStart) //something happened + .isEqualTo(beforeStart); //original span was restored + + //finally, assert that the delay thread was not polluted either + CountDownLatch latch = new CountDownLatch(1); + delayScheduler.schedule(() -> { + try { + assertThat(bb.getTracer().currentSpan()) + .as("no leftover span in delay thread") + .isNull(); + } + finally { + latch.countDown(); + } + }); + latch.await(10, TimeUnit.SECONDS); + }; + } +} diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java new file mode 100644 index 0000000000..9c92f492af --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.CsvSource; +import org.reactivestreams.Publisher; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.test.ParameterizedTestWithName; +import reactor.util.annotation.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Simon Baslé + */ +class MicrometerObservationListenerConfigurationTest { + + @ParameterizedTestWithName + @CsvSource(value = { + ",", + "someName,", + ",someTag", + "someName,someTag" + }) + void fromFlux(@Nullable String name, @Nullable String tag) { + ObservationRegistry expectedRegistry = ObservationRegistry.create(); + + Flux flux = Flux.just(1, 2, 3); + + if (name != null) { + flux = flux.name(name); + } + if (tag != null) { + flux = flux.tag("tag", tag); + } + + MicrometerObservationListenerConfiguration configuration = MicrometerObservationListenerConfiguration.fromFlux(flux, expectedRegistry); + + assertThat(configuration.registry).as("registry").isSameAs(expectedRegistry); + assertThat(configuration.isMono).as("isMono").isFalse(); + + assertThat(configuration.sequenceName) + .as("sequenceName") + .isEqualTo(name == null ? Micrometer.DEFAULT_METER_PREFIX : name); + + if (tag == null) { + assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) + .as("commonKeyValues without additional KeyValue") + .containsExactly("type=Flux"); + } + else { + assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) + .as("commonKeyValues") + .containsExactlyInAnyOrder("type=Flux", "tag="+tag); + } + } + + @ParameterizedTestWithName + @CsvSource(value = { + ",", + "someName,", + ",someTag", + "someName,someTag" + }) + void fromMono(@Nullable String name, @Nullable String tag) { + ObservationRegistry expectedRegistry = ObservationRegistry.create(); + + Mono mono = Mono.just(1); + + if (name != null) { + mono = mono.name(name); + } + if (tag != null) { + mono = mono.tag("tag", tag); + } + + MicrometerObservationListenerConfiguration configuration = MicrometerObservationListenerConfiguration.fromMono(mono, expectedRegistry); + + assertThat(configuration.registry).as("registry").isSameAs(expectedRegistry); + assertThat(configuration.isMono).as("isMono").isTrue(); + + assertThat(configuration.sequenceName) + .as("sequenceName") + .isEqualTo(name == null ? Micrometer.DEFAULT_METER_PREFIX : name); + + if (tag == null) { + assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) + .as("commonKeyValues without additional KeyValue") + .containsExactly("type=Mono"); + } + else { + assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) + .as("commonKeyValues") + .containsExactlyInAnyOrder("type=Mono", "tag="+tag); + } + } + + // == NB: this configuration reuses the MicrometerObservationListenerConfiguration#resolveName method, not tested here + + @Test + void resolveKeyValues_notSet() { + KeyValues defaultKeyValues = KeyValues.of("common1", "commonValue1"); + Flux flux = Flux.just(1); + + KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); + + assertThat(resolvedKeyValues.stream().map(Object::toString)) + .containsExactly("tag(common1=commonValue1)"); + } + + @Test + void resolveKeyValues_setRightAbove() { + KeyValues defaultKeyValues = KeyValues.of("common1", "commonValue1"); + Flux flux = Flux + .just(1) + .tag("k1", "v1"); + + KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); + + assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactlyInAnyOrder( + "tag(common1=commonValue1)", + "tag(k1=v1)" + ); + } + + @Test + void resolveKeyValues_setHigherAbove() { + KeyValues defaultKeyValues = KeyValues.of("common1", "commonValue1"); + Flux flux = Flux + .just(1) + .tag("k1", "v1") + .filter(i -> i % 2 == 0) + .map(i -> i + 10); + + KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); + + assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactlyInAnyOrder( + "tag(common1=commonValue1)", + "tag(k1=v1)" + ); + } + + @Test + void resolveKeyValues_multipleScatteredKeyValuesSetAbove() { + KeyValues defaultKeyValues = KeyValues.of("common1", "commonValue1"); + Flux flux = Flux.just(1) + .tag("k1", "v1") + .filter(i -> i % 2 == 0) + .tag("k2", "v2") + .map(i -> i + 10); + + KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); + + assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactlyInAnyOrder( + "tag(common1=commonValue1)", + "tag(k1=v1)", + "tag(k2=v2)" + ); + } + + @Test + void resolveKeyValues_multipleScatteredKeyValuesSetAboveWithDeduplication() { + KeyValues defaultKeyValues = KeyValues.of("common1", "commonValue1"); + Flux flux = Flux.just(1) + .tag("k1", "v1") + .tag("k2", "oldV2") + .filter(i -> i % 2 == 0) + .tag("k2", "v2") + .map(i -> i + 10); + + KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); + + assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactly( + "tag(common1=commonValue1)", + "tag(k1=v1)", + "tag(k2=v2)" + ); + } + + @Test + void resolveKeyValues_notScannable() { + KeyValues defaultKeyValues = KeyValues.of("common1", "commonValue1"); + Publisher publisher = Operators::complete; + + KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(publisher, defaultKeyValues); + + assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactly("tag(common1=commonValue1)"); + } +} \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java new file mode 100644 index 0000000000..7889d3df38 --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import reactor.core.observability.SignalListener; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Simon Baslé + */ +class MicrometerObservationListenerFactoryTest { + + protected static final ObservationRegistry CUSTOM_REGISTRY = ObservationRegistry.create(); + + protected static final MicrometerObservationListenerFactory CUSTOM_FACTORY = + new MicrometerObservationListenerFactory<>(CUSTOM_REGISTRY); + + @Test + void configurationFromMono() { + MicrometerObservationListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Mono.just(1)); + + assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); + assertThat(configuration.isMono).as("isMono").isTrue(); + assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("tag(type=Mono)"); + } + + @Test + void configurationFromFlux() { + MicrometerObservationListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Flux.just(1, 2)); + + assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); + assertThat(configuration.isMono).as("isMono").isFalse(); + assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("tag(type=Flux)"); + } + + @Test + void configurationFromGenericPublisherIsRejected() { + assertThatIllegalArgumentException() + .isThrownBy(() -> CUSTOM_FACTORY.initializePublisherState(Operators::complete)) + .withMessage("MicrometerObservationListenerFactory must only be used via the tap operator / with a Flux or Mono"); + } + + @Test + void createListenerOfTypeMicrometer() { + Publisher source = Mono.just(1); + ContextView expectedContext = Context.of(1, "A"); + MicrometerObservationListenerConfiguration conf = CUSTOM_FACTORY.initializePublisherState(source); + SignalListener signalListener = CUSTOM_FACTORY.createListener(source, expectedContext, conf); + + assertThat(signalListener).isInstanceOf(MicrometerObservationListener.class); + MicrometerObservationListener observationListener = (MicrometerObservationListener) signalListener; + + assertThat(observationListener.configuration).as("configuration").isSameAs(conf); + assertThat(observationListener.originalContext).as("context").isSameAs(expectedContext); + } + +} \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java new file mode 100644 index 0000000000..2d94efdd26 --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java @@ -0,0 +1,360 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.util.concurrent.atomic.AtomicLong; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.Clock; +import io.micrometer.observation.tck.TestObservationRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +import static io.micrometer.observation.tck.TestObservationRegistryAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Simon Baslé + */ +class MicrometerObservationListenerTest { + + TestObservationRegistry registry; + AtomicLong virtualClockTime; + Clock virtualClock; + MicrometerObservationListenerConfiguration configuration; + ContextView subscriberContext; + + @BeforeEach + void initRegistry() { + registry = TestObservationRegistry.create(); + virtualClockTime = new AtomicLong(); + virtualClock = new Clock() { + @Override + public long wallTime() { + return virtualClockTime.get(); + } + + @Override + public long monotonicTime() { + return virtualClockTime.get(); + } + }; + configuration = new MicrometerObservationListenerConfiguration( + "testName", + KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + false); + subscriberContext = Context.of("contextKey", "contextValue"); + } + + @Test + void whenStartedFluxWithDefaultName() { + configuration = new MicrometerObservationListenerConfiguration( + Micrometer.DEFAULT_METER_PREFIX, + //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) + KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + false); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); + + assertThat(listener.valued).as("valued").isFalse(); + assertThat(listener.subscribeToTerminalObservation) + .as("subscribeToTerminalObservation field") + .isNotNull(); + assertThat(registry).as("before start").doesNotHaveAnyObservation(); + + listener.doFirst(); // forces observation start + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("reactor.observation.flow") + .as("subscribeToTerminalObservation") + .hasBeenStarted() + .isNotStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2"); + } + + @Test + void whenStartedFluxWithCustomName() { + configuration = new MicrometerObservationListenerConfiguration( + "testName", + //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) + KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + false); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); + + assertThat(listener.valued).as("valued").isFalse(); + assertThat(listener.subscribeToTerminalObservation) + .as("subscribeToTerminalObservation field") + .isNotNull(); + assertThat(registry).as("no observation started").doesNotHaveAnyObservation(); + + listener.doFirst(); // forces observation start + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("testName.observation.flow") + .as("subscribeToTerminalObservation") + .hasBeenStarted() + .isNotStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2"); + } + + @Test + void whenStartedMono() { + configuration = new MicrometerObservationListenerConfiguration( + Micrometer.DEFAULT_METER_PREFIX, + //note: "type" key is added by MicrometerObservationListenerConfiguration#fromMono (which is tested separately) + KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + true); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); + + assertThat(listener.valued).as("valued").isFalse(); + assertThat(listener.subscribeToTerminalObservation) + .as("subscribeToTerminalObservation field") + .isNotNull(); + assertThat(registry).as("no observation started").doesNotHaveAnyObservation(); + + listener.doFirst(); // forces observation start + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("reactor.observation.flow") + .as("subscribeToTerminalObservation") + .hasBeenStarted() + .isNotStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2"); + } + + @Test + void tapFromFluxWithTags() { + Flux flux = Flux.just(1) + .name("testFlux") + .tag("testTag1", "testTagValue1") + .tag("testTag2", "testTagValue2") + .tap(new MicrometerObservationListenerFactory<>(registry)); + + assertThat(registry).as("before subscription").doesNotHaveAnyObservation(); + + flux.blockLast(); + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("testFlux.observation.flow") + .as("subscribeToTerminalObservation") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2") + .hasLowCardinalityKeyValue("type", "Flux") + .hasLowCardinalityKeyValue("status", "completed") + .hasKeyValuesCount(4); + } + + @Test + void tapFromMonoWithTags() { + Mono mono = Mono.just(1) + .name("testMono") + .tag("testTag1", "testTagValue1") + .tag("testTag2", "testTagValue2") + .tap(new MicrometerObservationListenerFactory<>(registry)); + + assertThat(registry).as("before subscription").doesNotHaveAnyObservation(); + + mono.block(); + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("testMono.observation.flow") + .as("subscribeToTerminalObservation") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2") + .hasLowCardinalityKeyValue("type", "Mono") + .hasLowCardinalityKeyValue("status", "completed") + .hasKeyValuesCount(4); + } + + @Test + void observationStoppedByCancellation() { + configuration = new MicrometerObservationListenerConfiguration( + "flux", + KeyValues.of("forcedType", "Flux"), + registry, + false); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); + + listener.doFirst(); // forces observation start + listener.doOnCancel(); // stops via cancellation + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("flux.observation.flow") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("forcedType", "Flux") + .hasLowCardinalityKeyValue("status", "cancelled") + .hasKeyValuesCount(2) + .doesNotHaveError(); + } + + @Test + void observationStoppedByCompleteEmpty() { + configuration = new MicrometerObservationListenerConfiguration( + "emptyFlux", + KeyValues.of("forcedType", "Flux"), + registry, + false); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); + + listener.doFirst(); // forces observation start + listener.doOnComplete(); // stops via completion (empty) + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("emptyFlux.observation.flow") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("forcedType", "Flux") + .hasLowCardinalityKeyValue("status", "completedEmpty") + .hasKeyValuesCount(2) + .doesNotHaveError(); + } + + @Test + void observationStoppedByCompleteWithValues() { + configuration = new MicrometerObservationListenerConfiguration( + "flux", + KeyValues.of("forcedType", "Flux"), + registry, + false); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); + + listener.doFirst(); // forces observation start + listener.doOnNext(1); + listener.doOnComplete(); // stops via completion + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("flux.observation.flow") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("forcedType", "Flux") + .hasLowCardinalityKeyValue("status", "completed") + .hasKeyValuesCount(2) + .doesNotHaveError(); + } + + @Test + void observationMonoStoppedByOnNext() { + configuration = new MicrometerObservationListenerConfiguration( + "valuedMono", + KeyValues.of("forcedType", "Mono"), + registry, + true); + + final String expectedStatus = "completedOnNext"; + + //we use a test-oriented constructor to force the onNext completion case to have a different tag value + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration, expectedStatus); + + listener.doFirst(); // forces observation start + listener.doOnNext(1); // emulates onNext, should stop observation + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("valuedMono.observation.flow") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("forcedType", "Mono") + .hasLowCardinalityKeyValue("status", expectedStatus) + .doesNotHaveError(); + + listener.doOnComplete(); + + //if the test in doOnComplete was bugged, the status key would be associated with "completed" now + assertThat(registry) + .hasSingleObservationThat() + .as("post-doOnComplete") + .hasLowCardinalityKeyValue("status", expectedStatus); + } + + @Test + void observationEmptyMonoStoppedByOnComplete() { + configuration = new MicrometerObservationListenerConfiguration( + "emptyMono", + KeyValues.of("forcedType", "Mono"), + registry, + true); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); + + listener.doFirst(); // forces observation start + listener.doOnComplete(); // stops via completion (empty) + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("emptyMono.observation.flow") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("forcedType", "Mono") + .hasLowCardinalityKeyValue("status", "completedEmpty") + .doesNotHaveError(); + } + + @Test + void observationStoppedByError() { + configuration = new MicrometerObservationListenerConfiguration( + "errorFlux", + KeyValues.of("forcedType", "Flux"), + registry, + false); + IllegalStateException exception = new IllegalStateException("observationStoppedByError"); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); + + listener.doFirst(); // forces observation start + listener.doOnError(exception); // stops via onError + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("errorFlux.observation.flow") + .hasBeenStarted() + .hasBeenStopped() + .hasOnlyKeys("forcedType", "status") + .hasLowCardinalityKeyValue("forcedType", "Flux") + .hasLowCardinalityKeyValue("status", "error") + .hasError(exception); + } +} \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java index dd27963a5e..595ee6b5ca 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java @@ -64,7 +64,7 @@ void defaultRegistryCanBeChanged() { void metricsUsesCommonRegistry() { SimpleMeterRegistry customCommonRegistry = new SimpleMeterRegistry(); Micrometer.useRegistry(customCommonRegistry); - MicrometerListenerFactory factory = (MicrometerListenerFactory) Micrometer.metrics(); + MicrometerMeterListenerFactory factory = (MicrometerMeterListenerFactory) Micrometer.metrics(); assertThat(factory.useClock()).as("clock").isSameAs(Clock.SYSTEM); assertThat(factory.useRegistry()).as("registry").isSameAs(customCommonRegistry); @@ -87,7 +87,7 @@ public long monotonicTime() { } }; - MicrometerListenerFactory factory = (MicrometerListenerFactory) Micrometer.metrics(customLocalRegistry, customLocalClock); + MicrometerMeterListenerFactory factory = (MicrometerMeterListenerFactory) Micrometer.metrics(customLocalRegistry, customLocalClock); assertThat(factory.useClock()).as("clock").isSameAs(customLocalClock).isNotSameAs(Clock.SYSTEM); assertThat(factory.useRegistry()).as("registry").isSameAs(customLocalRegistry).isNotSameAs(customCommonRegistry); diff --git a/reactor-core/src/main/java/reactor/core/observability/SignalListener.java b/reactor-core/src/main/java/reactor/core/observability/SignalListener.java index 8df017d1be..927a281326 100644 --- a/reactor-core/src/main/java/reactor/core/observability/SignalListener.java +++ b/reactor-core/src/main/java/reactor/core/observability/SignalListener.java @@ -200,4 +200,18 @@ public interface SignalListener { * @param listenerError the exception thrown from a {@link SignalListener} handler method */ void handleListenerError(Throwable listenerError); + + /** + * In some cases, the tap operation should alter the {@link Context} exposed by the operator in order to store additional + * data. This method is invoked when the tap subscriber is created, which is between the invocation of {@link #doFirst()} + * and the invocation of {@link #doOnSubscription()}. Generally, only addition of new keys should be performed on + * the downstream original {@link Context}. Extra care should be exercised if any pre-existing key is to be removed + * or replaced. + * + * @param originalContext the original downstream operator's {@link Context} + * @return the {@link Context} to use and expose upstream + */ + default Context addToContext(Context originalContext) { + return originalContext; + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java index 6bd4d1fcc5..2cd1715eb3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java @@ -21,9 +21,10 @@ import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Fuseable.ConditionalSubscriber; -import reactor.util.annotation.Nullable; import reactor.core.observability.SignalListener; import reactor.core.observability.SignalListenerFactory; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; /** * A generic per-Subscription side effect {@link Flux} that notifies a {@link SignalListener} of most events. @@ -84,6 +85,7 @@ public Object scanUnsafe(Attr key) { static class TapSubscriber implements InnerOperator { final CoreSubscriber actual; + final Context context; final SignalListener listener; boolean done; @@ -92,6 +94,16 @@ static class TapSubscriber implements InnerOperator { TapSubscriber(CoreSubscriber actual, SignalListener signalListener) { this.actual = actual; this.listener = signalListener; + //note that since we're in the subscriber, this is technically invoked AFTER doFirst + Context ctx; + try { + ctx = signalListener.addToContext(actual.currentContext()); + } + catch (Throwable e) { + signalListener.handleListenerError(new IllegalStateException("Unable to augment tap Context at construction via addToContext", e)); + ctx = actual.currentContext(); + } + this.context = ctx; } @Override @@ -99,6 +111,11 @@ public CoreSubscriber actual() { return this.actual; } + @Override + public Context currentContext() { + return this.context; + } + @Override @Nullable public Object scanUnsafe(Attr key) { From acdfb31b2b07eda18d9e6c67bc668deb4fb94d2d Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Fri, 8 Jul 2022 19:40:52 +0300 Subject: [PATCH 036/312] Adapt to the changes in the context propagation API (#3113) The artifactId has been shortened to `context-propagation`. The `ContextSnapshot` API has renamed its capture methods. --- docs/asciidoc/advancedFeatures.adoc | 4 ++-- gradle/libs.versions.toml | 4 ++-- reactor-core-micrometer/build.gradle | 2 +- .../reactor/core/observability/micrometer/Micrometer.java | 2 +- .../micrometer/MicrometerObservationListener.java | 4 ++-- reactor-core/build.gradle | 6 +++--- .../java/reactor/util/context/ReactorContextAccessor.java | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/asciidoc/advancedFeatures.adoc b/docs/asciidoc/advancedFeatures.adoc index 56716dc1ac..8fcbf74fba 100644 --- a/docs/asciidoc/advancedFeatures.adoc +++ b/docs/asciidoc/advancedFeatures.adoc @@ -843,12 +843,12 @@ In case one needs to use one of the remaining APIs that still require a `Context [[context.propagation]] === Micrometer Context-Propagation Support -Since 3.5.0, Reactor-Core embeds basic support for the `io.micrometer:context-propagation-api` SPI. +Since 3.5.0, Reactor-Core embeds basic support for the `io.micrometer:context-propagation` SPI. This library is intended as a mean to easily adapt between various implementations of the concept of a Context, of which `ContextView`/`Context` is an example, and between `ThreadLocal` variables as well. `ReactorContextAccessor` is one implementation of this SPI that is loaded via `ServiceLoader`. No user action is required, -other than depending on reactor-core and `context-propagation-api` (the class is public but shouldn't generally be accessed by user code). +other than depending on reactor-core and `context-propagation` (the class is public but shouldn't generally be accessed by user code). === Simple `Context` Examples diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2a6630a73..19b2fec200 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ asciidoctor = "3.3.2" bytebuddy = "1.12.10" jmh = "1.35" junit = "5.8.2" -#note that context-propagation-api has a different version directly set in libraries +#note that context-propagation has a different version directly set in libraries micrometer = "1.10.0-SNAPSHOT" reactiveStreams = "1.0.4" @@ -31,7 +31,7 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagationApi = "io.micrometer:context-propagation-api:1.0.0-SNAPSHOT" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" micrometer-test = { module = "io.micrometer:micrometer-test" } diff --git a/reactor-core-micrometer/build.gradle b/reactor-core-micrometer/build.gradle index 4c81a0a9d7..f815e7e03c 100644 --- a/reactor-core-micrometer/build.gradle +++ b/reactor-core-micrometer/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation platform(libs.micrometer.bom) api libs.micrometer.core - implementation libs.micrometer.contextPropagationApi + implementation libs.micrometer.contextPropagation testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index bb117c900b..f34892f08a 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -134,7 +134,7 @@ protected MeterRegistry useRegistry() { * Finally, the low cardinality {@code type} KeyValue informs whether we're observing a {@code Flux} * or a {@code Mono}. *

    - * Note that the Micrometer {@code context-propagation-api} is used to populate thread locals + * Note that the Micrometer {@code context-propagation} is used to populate thread locals * around the opening of the observation (upon {@code onSubscribe(Subscription)}). *

    * Observation names are prefixed by the {@link reactor.core.publisher.Flux#name(String)} defined upstream diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java index 2a4e01b2e2..f4b477d3f8 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java @@ -82,7 +82,7 @@ final class MicrometerObservationListener implements SignalListener { @Override public void doFirst() { - ContextSnapshot contextSnapshot = ContextSnapshot.forContextAndThreadLocalValues(this.originalContext); + ContextSnapshot contextSnapshot = ContextSnapshot.capture(this.originalContext); try (ContextSnapshot.Scope ignored = contextSnapshot.setThreadLocalValues()) { this.scope = this.subscribeToTerminalObservation @@ -90,7 +90,7 @@ public void doFirst() { .openScope(); //reacquire the scope from ThreadLocal //tap context hasn't been initialized yet, so addToContext can now use the Scope - ContextSnapshot contextSnapshot2 = ContextSnapshot.forContextAndThreadLocalValues(this.originalContext); + ContextSnapshot contextSnapshot2 = ContextSnapshot.capture(this.originalContext); this.contextWithScope = contextSnapshot2.updateContext(Context.of(this.originalContext)); } } diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 01b9bd6e1b..7f3af21789 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -88,8 +88,8 @@ dependencies { compileOnly libs.micrometer.commons compileOnly libs.micrometer.core - // Optional context-propagation-api - compileOnly libs.micrometer.contextPropagationApi + // Optional context-propagation + compileOnly libs.micrometer.contextPropagation // Optional BlockHound support compileOnly libs.blockhound @@ -121,7 +121,7 @@ dependencies { withMicrometerTestImplementation platform(libs.micrometer.bom) withMicrometerTestImplementation libs.micrometer.commons withMicrometerTestImplementation libs.micrometer.core - withMicrometerTestImplementation libs.micrometer.contextPropagationApi + withMicrometerTestImplementation libs.micrometer.contextPropagation withMicrometerTestImplementation sourceSets.test.output jcstressImplementation(project(":reactor-test")) { diff --git a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java index 051b3c4b57..370e3b0e09 100644 --- a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java +++ b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java @@ -25,7 +25,7 @@ * A {@code ContextAccessor} to enable reading values from a Reactor * {@link ContextView} and writing values to {@link Context}. *

    - * Please note that this public class implements the {@code libs.micrometer.contextPropagationApi} + * Please note that this public class implements the {@code libs.micrometer.contextPropagation} * SPI library, which is an optional dependency. * * @author Rossen Stoyanchev From 7e999a2c04c1b85337b897ed3fd95e4d9bbe537a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 11 Jul 2022 12:07:18 +0200 Subject: [PATCH 037/312] Reintroduction of Processors as deprecated (#3112) This mostly reverts #3051, by reintroducing Flux and Mono Processor implementation as they were in 3.4.x (still deprecated). This was done by copying the classes and making a few small adaptations to DirectProcessor and SinkManyBestEffort (reintroducing the interface DirectInnerContainer, as a package-private mean of mutualizing the DirectInner code). This soft-revert doesn't reintroduce tests, Flux|Mono-level APIs or mentions in the documentation / reference guide. --- .../core/publisher/DelegateProcessor.java | 127 +++ .../core/publisher/DirectInnerContainer.java | 45 ++ .../core/publisher/DirectProcessor.java | 359 +++++++++ .../core/publisher/EmitterProcessor.java | 723 ++++++++++++++++++ .../reactor/core/publisher/FluxProcessor.java | 279 +++++++ .../reactor/core/publisher/MonoProcessor.java | 239 ++++++ .../reactor/core/publisher/NextProcessor.java | 85 +- .../core/publisher/ReplayProcessor.java | 686 +++++++++++++++++ .../core/publisher/SinkManyBestEffort.java | 10 +- .../core/publisher/UnicastProcessor.java | 652 ++++++++++++++++ 10 files changed, 3168 insertions(+), 37 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/publisher/DelegateProcessor.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/DirectInnerContainer.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/DirectProcessor.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/EmitterProcessor.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/FluxProcessor.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/MonoProcessor.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/ReplayProcessor.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/UnicastProcessor.java diff --git a/reactor-core/src/main/java/reactor/core/publisher/DelegateProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/DelegateProcessor.java new file mode 100644 index 0000000000..588d825fc2 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/DelegateProcessor.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.stream.Stream; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Scannable; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +/** + * @author Stephane Maldini + */ +@Deprecated +final class DelegateProcessor extends FluxProcessor { + + final Publisher downstream; + final Subscriber upstream; + + DelegateProcessor(Publisher downstream, + Subscriber upstream) { + this.downstream = Objects.requireNonNull(downstream, "Downstream must not be null"); + this.upstream = Objects.requireNonNull(upstream, "Upstream must not be null"); + } + + @Override + public Context currentContext() { + if(upstream instanceof CoreSubscriber){ + return ((CoreSubscriber)upstream).currentContext(); + } + return Context.empty(); + } + + @Override + public void onComplete() { + upstream.onComplete(); + } + + @Override + public void onError(Throwable t) { + upstream.onError(t); + } + + @Override + public void onNext(IN in) { + upstream.onNext(in); + } + + @Override + public void onSubscribe(Subscription s) { + upstream.onSubscribe(s); + } + + @Override + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + downstream.subscribe(actual); + } + + @Override + @SuppressWarnings("unchecked") + public boolean isSerialized() { + return upstream instanceof SerializedSubscriber || + (upstream instanceof FluxProcessor && + ((FluxProcessor)upstream).isSerialized()); + } + + @Override + public Stream inners() { + //noinspection ConstantConditions + return Scannable.from(upstream) + .inners(); + } + + @Override + public int getBufferSize() { + //noinspection ConstantConditions + return Scannable.from(upstream) + .scanOrDefault(Attr.CAPACITY, super.getBufferSize()); + } + + @Override + @Nullable + public Throwable getError() { + //noinspection ConstantConditions + return Scannable.from(upstream) + .scanOrDefault(Attr.ERROR, super.getError()); + } + + @Override + public boolean isTerminated() { + //noinspection ConstantConditions + return Scannable.from(upstream) + .scanOrDefault(Attr.TERMINATED, super.isTerminated()); + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) { + return downstream; + } + //noinspection ConstantConditions + return Scannable.from(upstream) + .scanUnsafe(key); + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/DirectInnerContainer.java b/reactor-core/src/main/java/reactor/core/publisher/DirectInnerContainer.java new file mode 100644 index 0000000000..7182773213 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/DirectInnerContainer.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +/** + * A package-private interface allowing to mutualize logic between {@link DirectProcessor} + * and {@link SinkManyBestEffort}. + * + * @author Simon Baslé + * @deprecated remove again once DirectProcessor is removed + */ +@Deprecated +interface DirectInnerContainer { + + /** + * Add a new {@link SinkManyBestEffort.DirectInner} to this publisher. + * + * @param s the new {@link SinkManyBestEffort.DirectInner} to add + * + * @return {@code true} if the inner could be added, {@code false} if the publisher cannot accept new subscribers + */ + boolean add(SinkManyBestEffort.DirectInner s); + + /** + * Remove an {@link SinkManyBestEffort.DirectInner} from this publisher. Does nothing if the inner is not currently managed + * by the publisher. + * + * @param s the {@link SinkManyBestEffort.DirectInner} to remove + */ + void remove(SinkManyBestEffort.DirectInner s); +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/DirectProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/DirectProcessor.java new file mode 100644 index 0000000000..fcbde1bf9b --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/DirectProcessor.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.stream.Stream; + +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Scannable; +import reactor.core.publisher.SinkManyBestEffort.DirectInner; +import reactor.core.publisher.Sinks.EmitResult; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +/** + * Dispatches onNext, onError and onComplete signals to zero-to-many Subscribers. + * Please note, that along with multiple consumers, current implementation of + * DirectProcessor supports multiple producers. However, all producers must produce + * messages on the same Thread, otherwise + * Reactive Streams Spec contract is + * violated. + *

    + * + *

    + * + *
    + *
    + * + *

    + * Note: DirectProcessor does not coordinate backpressure between its + * Subscribers and the upstream, but consumes its upstream in an + * unbounded manner. + * In the case where a downstream Subscriber is not ready to receive items (hasn't + * requested yet or enough), it will be terminated with an + * {@link IllegalStateException}. + * Hence in terms of interaction model, DirectProcessor only supports PUSH from the + * source through the processor to the Subscribers. + * + *

    + * + *

    + *

    + * + *
    + *
    + * + *

    + * Note: If there are no Subscribers, upstream items are dropped and only + * the terminal events are retained. A terminated DirectProcessor will emit the + * terminal signal to late subscribers. + * + *

    + * + *

    + *

    + * + *
    + *
    + * + *

    + * Note: The implementation ignores Subscriptions set via onSubscribe. + *

    + * + * @param the input and output value type + * @deprecated To be removed in 3.5, prefer clear cut usage of {@link Sinks}. Closest sink + * is {@link Sinks.MulticastSpec#directBestEffort() Sinks.many().multicast().directBestEffort()}, + * except it doesn't terminate overflowing downstreams. + */ +@Deprecated +public final class DirectProcessor extends FluxProcessor + implements DirectInnerContainer { + + /** + * Create a new {@link DirectProcessor} + * + * @param Type of processed signals + * + * @return a fresh processor + * @deprecated To be removed in 3.5. Closest sink is {@link Sinks.MulticastSpec#directBestEffort() Sinks.many().multicast().directBestEffort()}, + * except it doesn't terminate overflowing downstreams. + */ + @Deprecated + public static DirectProcessor create() { + return new DirectProcessor<>(); + } + + @SuppressWarnings("unchecked") + private volatile DirectInner[] subscribers = SinkManyBestEffort.EMPTY; + @SuppressWarnings("rawtypes") + private static final AtomicReferenceFieldUpdaterSUBSCRIBERS = + AtomicReferenceFieldUpdater.newUpdater(DirectProcessor.class, DirectInner[].class, "subscribers"); + + Throwable error; + + DirectProcessor() { + } + + @Override + public int getPrefetch() { + return Integer.MAX_VALUE; + } + + @Override + public Context currentContext() { + return Operators.multiSubscribersContext(subscribers); + } + + @Override + public void onSubscribe(Subscription s) { + Objects.requireNonNull(s, "s"); + if (subscribers != SinkManyBestEffort.TERMINATED) { + s.request(Long.MAX_VALUE); + } + else { + s.cancel(); + } + } + + @Override + public void onComplete() { + //no particular error condition handling for onComplete + @SuppressWarnings("unused") Sinks.EmitResult emitResult = tryEmitComplete(); + } + + private void emitComplete() { + //no particular error condition handling for onComplete + @SuppressWarnings("unused") EmitResult emitResult = tryEmitComplete(); + } + + private EmitResult tryEmitComplete() { + @SuppressWarnings("unchecked") + DirectInner[] inners = SUBSCRIBERS.getAndSet(this, SinkManyBestEffort.TERMINATED); + + if (inners == SinkManyBestEffort.TERMINATED) { + return EmitResult.FAIL_TERMINATED; + } + + for (DirectInner s : inners) { + s.emitComplete(); + } + return EmitResult.OK; + } + + @Override + public void onError(Throwable throwable) { + emitError(throwable); + } + + private void emitError(Throwable error) { + Sinks.EmitResult result = tryEmitError(error); + if (result == EmitResult.FAIL_TERMINATED) { + Operators.onErrorDroppedMulticast(error, subscribers); + } + } + + private Sinks.EmitResult tryEmitError(Throwable t) { + Objects.requireNonNull(t, "t"); + + @SuppressWarnings("unchecked") + DirectInner[] inners = SUBSCRIBERS.getAndSet(this, SinkManyBestEffort.TERMINATED); + + if (inners == SinkManyBestEffort.TERMINATED) { + return EmitResult.FAIL_TERMINATED; + } + + error = t; + for (DirectInner s : inners) { + s.emitError(t); + } + return Sinks.EmitResult.OK; + } + + private void emitNext(T value) { + switch (tryEmitNext(value)) { + case FAIL_ZERO_SUBSCRIBER: + //we want to "discard" without rendering the sink terminated. + // effectively NO-OP cause there's no subscriber, so no context :( + break; + case FAIL_OVERFLOW: + Operators.onDiscard(value, currentContext()); + //the emitError will onErrorDropped if already terminated + emitError(Exceptions.failWithOverflow("Backpressure overflow during Sinks.Many#emitNext")); + break; + case FAIL_CANCELLED: + Operators.onDiscard(value, currentContext()); + break; + case FAIL_TERMINATED: + Operators.onNextDroppedMulticast(value, subscribers); + break; + case OK: + break; + default: + throw new IllegalStateException("unexpected return code"); + } + } + + @Override + public void onNext(T t) { + emitNext(t); + } + + private EmitResult tryEmitNext(T t) { + Objects.requireNonNull(t, "t"); + + DirectInner[] inners = subscribers; + + if (inners == SinkManyBestEffort.TERMINATED) { + return EmitResult.FAIL_TERMINATED; + } + if (inners == SinkManyBestEffort.EMPTY) { + return Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER; + } + + for (DirectInner s : inners) { + s.directEmitNext(t); + } + return Sinks.EmitResult.OK; + } + + @Override + protected boolean isIdentityProcessor() { + return true; + } + + @Override + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + + DirectInner p = new DirectInner<>(actual, this); + actual.onSubscribe(p); + + if (add(p)) { + if (p.isCancelled()) { + remove(p); + } + } + else { + Throwable e = error; + if (e != null) { + actual.onError(e); + } + else { + actual.onComplete(); + } + } + } + + @Override + public Stream inners() { + return Stream.of(subscribers); + } + + @Override + public boolean isTerminated() { + return SinkManyBestEffort.TERMINATED == subscribers; + } + + @Override + public long downstreamCount() { + return subscribers.length; + } + + @Override + public boolean add(DirectInner s) { + DirectInner[] a = subscribers; + if (a == SinkManyBestEffort.TERMINATED) { + return false; + } + + synchronized (this) { + a = subscribers; + if (a == SinkManyBestEffort.TERMINATED) { + return false; + } + int len = a.length; + + @SuppressWarnings("unchecked") DirectInner[] b = new DirectInner[len + 1]; + System.arraycopy(a, 0, b, 0, len); + b[len] = s; + + subscribers = b; + + return true; + } + } + + @Override + @SuppressWarnings("unchecked") + public void remove(DirectInner s) { + DirectInner[] a = subscribers; + if (a == SinkManyBestEffort.TERMINATED || a == SinkManyBestEffort.EMPTY) { + return; + } + + synchronized (this) { + a = subscribers; + if (a == SinkManyBestEffort.TERMINATED || a == SinkManyBestEffort.EMPTY) { + return; + } + int len = a.length; + + int j = -1; + + for (int i = 0; i < len; i++) { + if (a[i] == s) { + j = i; + break; + } + } + if (j < 0) { + return; + } + if (len == 1) { + subscribers = SinkManyBestEffort.EMPTY; + return; + } + + DirectInner[] b = new DirectInner[len - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, len - j - 1); + + subscribers = b; + } + } + + @Override + public boolean hasDownstreams() { + DirectInner[] s = subscribers; + return s != SinkManyBestEffort.EMPTY && s != SinkManyBestEffort.TERMINATED; + } + + @Override + @Nullable + public Throwable getError() { + if (subscribers == SinkManyBestEffort.TERMINATED) { + return error; + } + return null; + } + +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/EmitterProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/EmitterProcessor.java new file mode 100644 index 0000000000..cedbf7d68f --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/EmitterProcessor.java @@ -0,0 +1,723 @@ +/* + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.stream.Stream; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.Exceptions; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.core.publisher.Sinks.EmitResult; +import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; +import reactor.util.context.Context; + +import static reactor.core.publisher.FluxPublish.PublishSubscriber.TERMINATED; + +/** + * An implementation of a message-passing Processor implementing + * publish-subscribe with synchronous (thread-stealing and happen-before interactions) + * drain loops. + *

    + * The default {@link #create} factories will only produce the new elements observed in + * the parent sequence after a given {@link Subscriber} is subscribed. + *

    + *

    + * + *

    + * + * @param the input and output value type + * + * @author Stephane Maldini + * @deprecated To be removed in 3.5. Prefer clear cut usage of {@link Sinks} through + * variations of {@link Sinks.MulticastSpec#onBackpressureBuffer() Sinks.many().multicast().onBackpressureBuffer()}. + * If you really need the subscribe-to-upstream functionality of a {@link org.reactivestreams.Processor}, switch + * to {@link Sinks.ManyWithUpstream} with {@link Sinks#unsafe()} variants of {@link Sinks.RootSpec#manyWithUpstream() Sinks.unsafe().manyWithUpstream()}. + *

    This processor was blocking in {@link EmitterProcessor#onNext(Object)}. This behaviour can be implemented with the {@link Sinks} API by calling + * {@link Sinks.Many#tryEmitNext(Object)} and retrying, e.g.: + *

    {@code while (sink.tryEmitNext(v).hasFailed()) {
    + *     LockSupport.parkNanos(10);
    + * }
    + * }
    + */ +@Deprecated +public final class EmitterProcessor extends FluxProcessor implements InternalManySink, + Sinks.ManyWithUpstream { + + @SuppressWarnings("rawtypes") + static final FluxPublish.PubSubInner[] EMPTY = new FluxPublish.PublishInner[0]; + + /** + * Create a new {@link EmitterProcessor} using {@link Queues#SMALL_BUFFER_SIZE} + * backlog size and auto-cancel. + * + * @param Type of processed signals + * + * @return a fresh processor + * @deprecated use {@link Sinks.MulticastSpec#onBackpressureBuffer() Sinks.many().multicast().onBackpressureBuffer()} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static EmitterProcessor create() { + return create(Queues.SMALL_BUFFER_SIZE, true); + } + + /** + * Create a new {@link EmitterProcessor} using {@link Queues#SMALL_BUFFER_SIZE} + * backlog size and the provided auto-cancel. + * + * @param Type of processed signals + * @param autoCancel automatically cancel + * + * @return a fresh processor + * @deprecated use {@link Sinks.MulticastSpec#onBackpressureBuffer(int, boolean) Sinks.many().multicast().onBackpressureBuffer(bufferSize, boolean)} + * using the old default of {@link Queues#SMALL_BUFFER_SIZE} for the {@code bufferSize} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static EmitterProcessor create(boolean autoCancel) { + return create(Queues.SMALL_BUFFER_SIZE, autoCancel); + } + + /** + * Create a new {@link EmitterProcessor} using the provided backlog size, with auto-cancel. + * + * @param Type of processed signals + * @param bufferSize the internal buffer size to hold signals + * + * @return a fresh processor + * @deprecated use {@link Sinks.MulticastSpec#onBackpressureBuffer(int) Sinks.many().multicast().onBackpressureBuffer(bufferSize)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static EmitterProcessor create(int bufferSize) { + return create(bufferSize, true); + } + + /** + * Create a new {@link EmitterProcessor} using the provided backlog size and auto-cancellation. + * + * @param Type of processed signals + * @param bufferSize the internal buffer size to hold signals + * @param autoCancel automatically cancel + * + * @return a fresh processor + * @deprecated use {@link Sinks.MulticastSpec#onBackpressureBuffer(int, boolean) Sinks.many().multicast().onBackpressureBuffer(bufferSize, autoCancel)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static EmitterProcessor create(int bufferSize, boolean autoCancel) { + return new EmitterProcessor<>(autoCancel, bufferSize); + } + + final int prefetch; + + final boolean autoCancel; + + volatile Subscription s; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater(EmitterProcessor.class, + Subscription.class, + "s"); + + volatile FluxPublish.PubSubInner[] subscribers; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater + SUBSCRIBERS = AtomicReferenceFieldUpdater.newUpdater(EmitterProcessor.class, + FluxPublish.PubSubInner[].class, + "subscribers"); + + volatile EmitterDisposable upstreamDisposable; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater UPSTREAM_DISPOSABLE = + AtomicReferenceFieldUpdater.newUpdater(EmitterProcessor.class, EmitterDisposable.class, "upstreamDisposable"); + + + @SuppressWarnings("unused") + volatile int wip; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(EmitterProcessor.class, "wip"); + + volatile Queue queue; + + int sourceMode; + + volatile boolean done; + + volatile Throwable error; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater ERROR = + AtomicReferenceFieldUpdater.newUpdater(EmitterProcessor.class, + Throwable.class, + "error"); + + EmitterProcessor(boolean autoCancel, int prefetch) { + if (prefetch < 1) { + throw new IllegalArgumentException("bufferSize must be strictly positive, " + "was: " + prefetch); + } + this.autoCancel = autoCancel; + this.prefetch = prefetch; + //doesn't use INIT/CANCELLED distinction, contrary to FluxPublish) + //see remove() + SUBSCRIBERS.lazySet(this, EMPTY); + } + + @Override + public Stream inners() { + return Stream.of(subscribers); + } + + @Override + public Context currentContext() { + return Operators.multiSubscribersContext(subscribers); + } + + + private boolean isDetached() { + return s == Operators.cancelledSubscription() && done && error instanceof CancellationException; + } + + private boolean detach() { + if (Operators.terminate(S, this)) { + done = true; + CancellationException detachException = new CancellationException("the ManyWithUpstream sink had a Subscription to an upstream which has been manually cancelled"); + if (ERROR.compareAndSet(EmitterProcessor.this, null, detachException)) { + Queue q = queue; + if (q != null) { + q.clear(); + } + for (FluxPublish.PubSubInner inner : terminate()) { + inner.actual.onError(detachException); + } + return true; + } + } + return false; + } + + @Override + public Disposable subscribeTo(Publisher upstream) { + EmitterDisposable ed = new EmitterDisposable(this); + if (UPSTREAM_DISPOSABLE.compareAndSet(this, null, ed)) { + upstream.subscribe(this); + return ed; + } + throw new IllegalStateException("A Sinks.ManyWithUpstream must be subscribed to a source only once"); + } + + @Override + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + EmitterInner inner = new EmitterInner<>(actual, this); + actual.onSubscribe(inner); + + if (inner.isCancelled()) { + return; + } + + if (add(inner)) { + if (inner.isCancelled()) { + remove(inner); + } + drain(); + } + else { + Throwable e = error; + if (e != null) { + inner.actual.onError(e); + } + else { + inner.actual.onComplete(); + } + } + } + + @Override + public void onComplete() { + //no particular error condition handling for onComplete + @SuppressWarnings("unused") EmitResult emitResult = tryEmitComplete(); + } + + @Override + public EmitResult tryEmitComplete() { + if (done) { + return EmitResult.FAIL_TERMINATED; + } + done = true; + drain(); + return EmitResult.OK; + } + + @Override + public void onError(Throwable throwable) { + emitError(throwable, Sinks.EmitFailureHandler.FAIL_FAST); + } + + @Override + public EmitResult tryEmitError(Throwable t) { + Objects.requireNonNull(t, "onError"); + if (done) { + return EmitResult.FAIL_TERMINATED; + } + if (Exceptions.addThrowable(ERROR, this, t)) { + done = true; + drain(); + return EmitResult.OK; + } + else { + return EmitResult.FAIL_TERMINATED; + } + } + + @Override + public void onNext(T t) { + if (sourceMode == Fuseable.ASYNC) { + drain(); + return; + } + emitNext(t, Sinks.EmitFailureHandler.FAIL_FAST); + } + + @Override + public EmitResult tryEmitNext(T t) { + if (done) { + return EmitResult.FAIL_TERMINATED; + } + + Objects.requireNonNull(t, "onNext"); + + Queue q = queue; + + if (q == null) { + if (Operators.setOnce(S, this, Operators.emptySubscription())) { + q = Queues.get(prefetch).get(); + queue = q; + } + else { + for (; ; ) { + if (isCancelled()) { + return EmitResult.FAIL_CANCELLED; + } + q = queue; + if (q != null) { + break; + } + } + } + } + + if (!q.offer(t)) { + return subscribers == EMPTY ? EmitResult.FAIL_ZERO_SUBSCRIBER : EmitResult.FAIL_OVERFLOW; + } + drain(); + return EmitResult.OK; + } + + @Override + public int currentSubscriberCount() { + return subscribers.length; + } + + @Override + public Flux asFlux() { + return this; + } + + @Override + protected boolean isIdentityProcessor() { + return true; + } + + /** + * Return the number of parked elements in the emitter backlog. + * + * @return the number of parked elements in the emitter backlog. + */ + public int getPending() { + Queue q = queue; + return q != null ? q.size() : 0; + } + + @Override + public boolean isDisposed() { + return isTerminated() || isCancelled(); + } + + @Override + public void onSubscribe(final Subscription s) { + //since the CoreSubscriber nature isn't exposed to the user, the only path to onSubscribe is + //already guarded by UPSTREAM_DISPOSABLE. just in case the publisher misbehaves we still use setOnce + if (Operators.setOnce(S, this, s)) { + if (s instanceof Fuseable.QueueSubscription) { + @SuppressWarnings("unchecked") Fuseable.QueueSubscription f = + (Fuseable.QueueSubscription) s; + + int m = f.requestFusion(Fuseable.ANY); + if (m == Fuseable.SYNC) { + sourceMode = m; + queue = f; + drain(); + return; + } + else if (m == Fuseable.ASYNC) { + sourceMode = m; + queue = f; + s.request(Operators.unboundedOrPrefetch(prefetch)); + return; + } + } + + queue = Queues.get(prefetch).get(); + + s.request(Operators.unboundedOrPrefetch(prefetch)); + } + } + + @Override + @Nullable + public Throwable getError() { + return error; + } + + /** + * @return true if all subscribers have actually been cancelled and the processor auto shut down + */ + public boolean isCancelled() { + return Operators.cancelledSubscription() == s; + } + + @Override + final public int getBufferSize() { + return prefetch; + } + + @Override + public boolean isTerminated() { + return done && getPending() == 0; + } + + @Override + public int getPrefetch() { + return prefetch; + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return s; + if (key == Attr.BUFFERED) return getPending(); + if (key == Attr.CANCELLED) return isCancelled(); + if (key == Attr.PREFETCH) return getPrefetch(); + + return super.scanUnsafe(key); + } + + final void drain() { + if (WIP.getAndIncrement(this) != 0) { + return; + } + + int missed = 1; + + for (; ; ) { + + boolean d = done; + + Queue q = queue; + + boolean empty = q == null || q.isEmpty(); + + if (checkTerminated(d, empty)) { + return; + } + + FluxPublish.PubSubInner[] a = subscribers; + + if (a != EMPTY && !empty) { + long maxRequested = Long.MAX_VALUE; + + int len = a.length; + int cancel = 0; + + for (FluxPublish.PubSubInner inner : a) { + long r = inner.requested; + if (r >= 0L) { + maxRequested = Math.min(maxRequested, r); + } + else { //Long.MIN == PublishInner.CANCEL_REQUEST + cancel++; + } + } + + if (len == cancel) { + T v; + + try { + v = q.poll(); + } + catch (Throwable ex) { + Exceptions.addThrowable(ERROR, + this, Operators.onOperatorError(s, ex, currentContext())); + d = true; + v = null; + } + if (checkTerminated(d, v == null)) { + return; + } + if (sourceMode != Fuseable.SYNC) { + s.request(1); + } + continue; + } + + int e = 0; + + while (e < maxRequested && cancel != Integer.MIN_VALUE) { + d = done; + T v; + + try { + v = q.poll(); + } + catch (Throwable ex) { + Exceptions.addThrowable(ERROR, + this, Operators.onOperatorError(s, ex, currentContext())); + d = true; + v = null; + } + + empty = v == null; + + if (checkTerminated(d, empty)) { + return; + } + + if (empty) { + //async mode only needs to break but SYNC mode needs to perform terminal cleanup here... + if (sourceMode == Fuseable.SYNC) { + //the q is empty + done = true; + checkTerminated(true, true); + } + break; + } + + for (FluxPublish.PubSubInner inner : a) { + inner.actual.onNext(v); + if (Operators.producedCancellable(FluxPublish + .PublishInner.REQUESTED, inner, + 1) == Long.MIN_VALUE) { + cancel = Integer.MIN_VALUE; + } + } + + e++; + } + + if (e != 0 && sourceMode != Fuseable.SYNC) { + s.request(e); + } + + if (maxRequested != 0L && !empty) { + continue; + } + } + else if ( sourceMode == Fuseable.SYNC ) { + done = true; + if (checkTerminated(true, empty)) { //empty can be true if no subscriber + break; + } + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + } + + @SuppressWarnings("unchecked") + FluxPublish.PubSubInner[] terminate() { + return SUBSCRIBERS.getAndSet(this, TERMINATED); + } + + boolean checkTerminated(boolean d, boolean empty) { + if (s == Operators.cancelledSubscription()) { + if (autoCancel) { + terminate(); + Queue q = queue; + if (q != null) { + q.clear(); + } + } + return true; + } + if (d) { + Throwable e = error; + if (e != null && e != Exceptions.TERMINATED) { + Queue q = queue; + if (q != null) { + q.clear(); + } + for (FluxPublish.PubSubInner inner : terminate()) { + inner.actual.onError(e); + } + return true; + } + else if (empty) { + for (FluxPublish.PubSubInner inner : terminate()) { + inner.actual.onComplete(); + } + return true; + } + } + return false; + } + + final boolean add(EmitterInner inner) { + for (; ; ) { + FluxPublish.PubSubInner[] a = subscribers; + if (a == TERMINATED) { + return false; + } + int n = a.length; + FluxPublish.PubSubInner[] b = new FluxPublish.PubSubInner[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (SUBSCRIBERS.compareAndSet(this, a, b)) { + return true; + } + } + } + + final void remove(FluxPublish.PubSubInner inner) { + for (; ; ) { + FluxPublish.PubSubInner[] a = subscribers; + if (a == TERMINATED || a == EMPTY) { + return; + } + int n = a.length; + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + FluxPublish.PubSubInner[] b; + if (n == 1) { + b = EMPTY; + } + else { + b = new FluxPublish.PubSubInner[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (SUBSCRIBERS.compareAndSet(this, a, b)) { + //contrary to FluxPublish, there is a possibility of auto-cancel, which + //happens when the removed inner makes the subscribers array EMPTY + if (autoCancel && b == EMPTY && Operators.terminate(S, this)) { + if (WIP.getAndIncrement(this) != 0) { + return; + } + terminate(); + Queue q = queue; + if (q != null) { + q.clear(); + } + } + return; + } + } + } + + @Override + public long downstreamCount() { + return subscribers.length; + } + + static final class EmitterInner extends FluxPublish.PubSubInner { + + final EmitterProcessor parent; + + EmitterInner(CoreSubscriber actual, EmitterProcessor parent) { + super(actual); + this.parent = parent; + } + + @Override + void drainParent() { + parent.drain(); + } + + @Override + void removeAndDrainParent() { + parent.remove(this); + parent.drain(); + } + } + + static final class EmitterDisposable implements Disposable { + + @Nullable + EmitterProcessor target; + + public EmitterDisposable(EmitterProcessor emitterProcessor) { + this.target = emitterProcessor; + } + + @Override + public boolean isDisposed() { + return target == null || target.isDetached(); + } + + @Override + public void dispose() { + EmitterProcessor t = target; + if (t == null) { + return; + } + if (t.detach() || t.isDetached()) { + target = null; + } + } + } + + +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/FluxProcessor.java new file mode 100644 index 0000000000..ff5aab0ef6 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxProcessor.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.concurrent.CancellationException; +import java.util.stream.Stream; + +import org.reactivestreams.Processor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.Scannable; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +import static reactor.core.publisher.Sinks.Many; + +/** + * A base processor that exposes {@link Flux} API for {@link Processor}. + * + * Implementors include {@link UnicastProcessor}, {@link EmitterProcessor}, {@link ReplayProcessor}. + * + * @author Stephane Maldini + * + * @param the input value type + * @param the output value type + * @deprecated Processors will be removed in 3.5. Prefer using {@link Many} instead, + * or see https://github.com/reactor/reactor-core/issues/2431 for alternatives + */ +@Deprecated +public abstract class FluxProcessor extends Flux + implements Processor, CoreSubscriber, Scannable, Disposable, ContextHolder { + + /** + * Build a {@link FluxProcessor} whose data are emitted by the most recent emitted {@link Publisher}. + * The {@link Flux} will complete once both the publishers source and the last switched to {@link Publisher} have + * completed. + * + *

    + * + * + * @param the produced type + * @return a {@link FluxProcessor} accepting publishers and producing T + * @deprecated should use {@link Sinks}, {@link Many#asFlux()} and {@link Flux#switchOnNext(Publisher)}. To be removed in 3.5.0. + */ + @Deprecated + public static FluxProcessor, T> switchOnNext() { + UnicastProcessor> emitter = UnicastProcessor.create(); + FluxProcessor, T> p = FluxProcessor.wrap(emitter, switchOnNext(emitter)); + return p; + } + /** + * Transform a receiving {@link Subscriber} and a producing {@link Publisher} in a logical {@link FluxProcessor}. + * The link between the passed upstream and returned downstream will not be created automatically, e.g. not + * subscribed together. A {@link Processor} might choose to have orthogonal sequence input and output. + * + * @param the receiving type + * @param the producing type + * + * @param upstream the upstream subscriber + * @param downstream the downstream publisher + * @return a new blackboxed {@link FluxProcessor} + */ + public static FluxProcessor wrap(final Subscriber upstream, final Publisher downstream) { + return new DelegateProcessor<>(downstream, upstream); + } + + @Override + public void dispose() { + onError(new CancellationException("Disposed")); + } + + /** + * Return the number of active {@link Subscriber} or {@literal -1} if untracked. + * + * @return the number of active {@link Subscriber} or {@literal -1} if untracked + */ + public long downstreamCount(){ + return inners().count(); + } + + /** + * Return the processor buffer capacity if any or {@link Integer#MAX_VALUE} + * + * @return processor buffer capacity if any or {@link Integer#MAX_VALUE} + */ + public int getBufferSize() { + return Integer.MAX_VALUE; + } + + /** + * Current error if any, default to null + * + * @return Current error if any, default to null + */ + @Nullable + public Throwable getError() { + return null; + } + + /** + * Return true if any {@link Subscriber} is actively subscribed + * + * @return true if any {@link Subscriber} is actively subscribed + */ + public boolean hasDownstreams() { + return downstreamCount() != 0L; + } + + /** + * Return true if terminated with onComplete + * + * @return true if terminated with onComplete + */ + public final boolean hasCompleted() { + return isTerminated() && getError() == null; + } + + /** + * Return true if terminated with onError + * + * @return true if terminated with onError + */ + public final boolean hasError() { + return isTerminated() && getError() != null; + } + + @Override + public Stream inners() { + return Stream.empty(); + } + + /** + * Has this upstream finished or "completed" / "failed" ? + * + * @return has this upstream finished or "completed" / "failed" ? + */ + public boolean isTerminated() { + return false; + } + + /** + * Return true if this {@link FluxProcessor} supports multithread producing + * + * @return true if this {@link FluxProcessor} supports multithread producing + */ + public boolean isSerialized() { + return false; + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.TERMINATED) return isTerminated(); + if (key == Attr.ERROR) return getError(); + if (key == Attr.CAPACITY) return getBufferSize(); + + return null; + } + + @Override + public Context currentContext() { + return Context.empty(); + } + + /** + * Create a {@link FluxProcessor} that safely gates multi-threaded producer + * {@link Subscriber#onNext(Object)}. + * + *

    Discard Support: The resulting processor discards elements received from the source + * {@link Publisher} (if any) when it cancels subscription to said source. + * + * @return a serializing {@link FluxProcessor} + */ + public final FluxProcessor serialize() { + return new DelegateProcessor<>(this, Operators.serialize(this)); + } + + /** + * Create a {@link FluxSink} that safely gates multi-threaded producer + * {@link Subscriber#onNext(Object)}. This processor will be subscribed to + * that {@link FluxSink}, and any previous subscribers will be unsubscribed. + * + *

    The returned {@link FluxSink} will not apply any + * {@link FluxSink.OverflowStrategy} and overflowing {@link FluxSink#next(Object)} + * will behave in two possible ways depending on the Processor: + *

      + *
    • an unbounded processor will handle the overflow itself by dropping or + * buffering
    • + *
    • a bounded processor will block/spin
    • + *
    + * + * @return a serializing {@link FluxSink} + * @deprecated To be removed in 3.5, prefer clear cut usage of {@link Sinks} + * through the {@link Sinks#many()} spec. + */ + @Deprecated + public final FluxSink sink() { + return sink(FluxSink.OverflowStrategy.IGNORE); + } + + /** + * Create a {@link FluxSink} that safely gates multi-threaded producer + * {@link Subscriber#onNext(Object)}. This processor will be subscribed to + * that {@link FluxSink}, and any previous subscribers will be unsubscribed. + * + *

    The returned {@link FluxSink} will not apply any + * {@link FluxSink.OverflowStrategy} and overflowing {@link FluxSink#next(Object)} + * will behave in two possible ways depending on the Processor: + *

      + *
    • an unbounded processor will handle the overflow itself by dropping or + * buffering
    • + *
    • a bounded processor will block/spin on IGNORE strategy, or apply the + * strategy behavior
    • + *
    + * + * @param strategy the overflow strategy, see {@link FluxSink.OverflowStrategy} + * for the + * available strategies + * @return a serializing {@link FluxSink} + * @deprecated To be removed in 3.5, prefer clear cut usage of {@link Sinks} + * through the {@link Sinks#many()} spec. + */ + @Deprecated + public final FluxSink sink(FluxSink.OverflowStrategy strategy) { + Objects.requireNonNull(strategy, "strategy"); + if (getBufferSize() == Integer.MAX_VALUE){ + strategy = FluxSink.OverflowStrategy.IGNORE; + } + + FluxCreate.BaseSink s = FluxCreate.createSink(this, strategy); + onSubscribe(s); + + if(s.isCancelled() || + (isSerialized() && getBufferSize() == Integer.MAX_VALUE)){ + return s; + } + if (serializeAlways()) + return new FluxCreate.SerializedFluxSink<>(s); + else + return new FluxCreate.SerializeOnRequestSink<>(s); + } + + /** + * Returns serialization strategy. If true, {@link FluxProcessor#sink()} will always + * be serialized. Otherwise sink is serialized only if {@link FluxSink#onRequest(java.util.function.LongConsumer)} + * is invoked. + * @return true to serialize any sink, false to delay serialization till onRequest + */ + protected boolean serializeAlways() { + return true; + } + + /** + * Return true if {@code FluxProcessor} + * + * @return true if {@code FluxProcessor} + */ + protected boolean isIdentityProcessor() { + return false; + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/MonoProcessor.java new file mode 100644 index 0000000000..4b66234af6 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoProcessor.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.stream.Stream; + +import org.reactivestreams.Processor; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.Scannable; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +/** + * A {@code MonoProcessor} is a {@link Processor} that is also a {@link Mono}. + * + *

    + * + * + *

    + * Implementations might implements stateful semantics, allowing multiple subscriptions. + * Once a {@link MonoProcessor} has been resolved, implementations may also replay cached signals to newer subscribers. + *

    + * Despite having default implementations, most methods should be reimplemented with meaningful semantics relevant to + * concrete child classes. + * + * @param the type of the value that will be made available + * + * @author Stephane Maldini + * @deprecated Processors will be removed in 3.5. Prefer using {@link Sinks.One} or {@link Sinks.Empty} instead, + * or see https://github.com/reactor/reactor-core/issues/2431 for alternatives + */ +@Deprecated +public abstract class MonoProcessor extends Mono + implements Processor, CoreSubscriber, Disposable, + Subscription, + Scannable { + + /** + * Create a {@link MonoProcessor} that will eagerly request 1 on {@link #onSubscribe(Subscription)}, cache and emit + * the eventual result for 1 or N subscribers. + * + * @param type of the expected value + * + * @return A {@link MonoProcessor}. + * @deprecated Use {@link Sinks#one()}, to be removed in 3.5 + */ + @Deprecated + public static MonoProcessor create() { + return new NextProcessor<>(null); + } + + /** + * @deprecated the {@link MonoProcessor} will cease to implement {@link Subscription} in 3.5 + */ + @Override + @Deprecated + public void cancel() { + } + + /** + * Indicates whether this {@code MonoProcessor} has been interrupted via cancellation. + * + * @return {@code true} if this {@code MonoProcessor} is cancelled, {@code false} + * otherwise. + * @deprecated the {@link MonoProcessor} will cease to implement {@link Subscription} and this method will be removed in 3.5 + */ + @Deprecated + public boolean isCancelled() { + return false; + } + + /** + * @param n the request amount + * @deprecated the {@link MonoProcessor} will cease to implement {@link Subscription} in 3.5 + */ + @Override + @Deprecated + public void request(long n) { + Operators.validate(n); + } + + @Override + public void dispose() { + onError(new CancellationException("Disposed")); + } + + /** + * Block the calling thread indefinitely, waiting for the completion of this {@code MonoProcessor}. If the + * {@link MonoProcessor} is completed with an error a RuntimeException that wraps the error is thrown. + * + * @return the value of this {@code MonoProcessor} + */ + @Override + @Nullable + public O block() { + return block(null); + } + + /** + * Block the calling thread for the specified time, waiting for the completion of this {@code MonoProcessor}. If the + * {@link MonoProcessor} is completed with an error a RuntimeException that wraps the error is thrown. + * + * @param timeout the timeout value as a {@link Duration} + * + * @return the value of this {@code MonoProcessor} or {@code null} if the timeout is reached and the {@code MonoProcessor} has + * not completed + */ + @Override + @Nullable + public O block(@Nullable Duration timeout) { + return peek(); + } + + /** + * Return the produced {@link Throwable} error if any or null + * + * @return the produced {@link Throwable} error if any or null + */ + @Nullable + public Throwable getError() { + return null; + } + + /** + * Indicates whether this {@code MonoProcessor} has been completed with an error. + * + * @return {@code true} if this {@code MonoProcessor} was completed with an error, {@code false} otherwise. + */ + public final boolean isError() { + return getError() != null; + } + + /** + * Indicates whether this {@code MonoProcessor} has been successfully completed a value. + * + * @return {@code true} if this {@code MonoProcessor} is successful, {@code false} otherwise. + */ + public final boolean isSuccess() { + return isTerminated() && !isError(); + } + + /** + * Indicates whether this {@code MonoProcessor} has been terminated by the + * source producer with a success or an error. + * + * @return {@code true} if this {@code MonoProcessor} is successful, {@code false} otherwise. + */ + public boolean isTerminated() { + return false; + } + + @Override + public boolean isDisposed() { + return isTerminated() || isCancelled(); + } + + /** + * Returns the value that completed this {@link MonoProcessor}. Returns {@code null} if the {@link MonoProcessor} has not been completed. If the + * {@link MonoProcessor} is completed with an error a RuntimeException that wraps the error is thrown. + * + * @return the value that completed the {@link MonoProcessor}, or {@code null} if it has not been completed + * + * @throws RuntimeException if the {@link MonoProcessor} was completed with an error + * @deprecated this method is discouraged, consider peeking into a MonoProcessor by {@link Mono#toFuture() turning it into a CompletableFuture} + */ + @Nullable + @Deprecated + public O peek() { + return null; + } + + @Override + public Context currentContext() { + InnerProducer[] innerProducersArray = + inners().filter(InnerProducer.class::isInstance) + .map(InnerProducer.class::cast) + .toArray(InnerProducer[]::new); + + return Operators.multiSubscribersContext(innerProducersArray); + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + //touch guard + boolean t = isTerminated(); + + if (key == Attr.TERMINATED) return t; + if (key == Attr.ERROR) return getError(); + if (key == Attr.PREFETCH) return Integer.MAX_VALUE; + if (key == Attr.CANCELLED) return isCancelled(); + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return null; + } + + /** + * Return the number of active {@link Subscriber} or {@literal -1} if untracked. + * + * @return the number of active {@link Subscriber} or {@literal -1} if untracked + */ + public long downstreamCount() { + return inners().count(); + } + + /** + * Return true if any {@link Subscriber} is actively subscribed + * + * @return true if any {@link Subscriber} is actively subscribed + */ + public final boolean hasDownstreams() { + return downstreamCount() != 0; + } + + @Override + public Stream inners() { + return Stream.empty(); + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/NextProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/NextProcessor.java index 58717d6a79..a2ac8128f8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/NextProcessor.java +++ b/reactor-core/src/main/java/reactor/core/publisher/NextProcessor.java @@ -36,7 +36,9 @@ import reactor.util.annotation.Nullable; import reactor.util.context.Context; -class NextProcessor extends Mono implements CoreSubscriber, reactor.core.Disposable, Scannable { +// NextProcessor extends a deprecated class but is itself not deprecated and is here to stay, hence the following line is ok. +@SuppressWarnings("deprecation") +class NextProcessor extends MonoProcessor implements CoreSubscriber, reactor.core.Disposable, Scannable { /** * This boolean indicates a usage as `Mono#share()` where, for alignment with Flux#share(), the removal of all @@ -63,23 +65,24 @@ public boolean isDisposed() { return isTerminated(); } - /** - * Indicates whether this {@link NextProcessor} has been completed with an error. - * - * @return {@code true} if this {@link NextProcessor} was completed with an error, {@code false} otherwise. - */ - public final boolean isError() { - return getError() != null; - } - - /** - * Indicates whether this {@link NextProcessor} has been successfully completed a value. - * - * @return {@code true} if this {@link NextProcessor} is successful, {@code false} otherwise. - */ - public final boolean isSuccess() { - return isTerminated() && !isError(); - } + //TODO reintroduce the boolean getters below once MonoProcessor is removed again +// /** +// * Indicates whether this {@link NextProcessor} has been completed with an error. +// * +// * @return {@code true} if this {@link NextProcessor} was completed with an error, {@code false} otherwise. +// */ +// public final boolean isError() { +// return getError() != null; +// } +// +// /** +// * Indicates whether this {@link NextProcessor} has been successfully completed a value. +// * +// * @return {@code true} if this {@link NextProcessor} is successful, {@code false} otherwise. +// */ +// public final boolean isSuccess() { +// return isTerminated() && !isError(); +// } @SuppressWarnings("rawtypes") static final AtomicReferenceFieldUpdater SUBSCRIBERS = @@ -127,7 +130,8 @@ public final boolean isSuccess() { * @throws RuntimeException if the {@link NextProcessor} was completed with an error */ @Nullable - O peek() { + @Override + public O peek() { if (!isTerminated()) { return null; } @@ -380,10 +384,11 @@ public Context currentContext() { } /** - * Return the number of active {@link Subscriber} or {@literal -1} if untracked. - * - * @return the number of active {@link Subscriber} or {@literal -1} if untracked - */ + * Return the number of active {@link Subscriber} or {@literal -1} if untracked. + * + * @return the number of active {@link Subscriber} or {@literal -1} if untracked + */ + @Override public long downstreamCount() { return subscribers.length; } @@ -414,6 +419,13 @@ public void dispose() { } } + @Override + // This method is inherited from a deprecated class and will be removed in 3.5. + @SuppressWarnings("deprecation") + public void cancel() { + doCancel(); + } + void doCancel() { //TODO compare with the cancellation in remove(), do we need both approaches? if (isTerminated()) { return; @@ -437,22 +449,31 @@ public final void onSubscribe(Subscription subscription) { } } + @Override + // This method is inherited from a deprecated class and will be removed in 3.5. + @SuppressWarnings("deprecation") + public boolean isCancelled() { + return subscription == Operators.cancelledSubscription() && !isTerminated(); + } + /** - * Indicates whether this {@link NextProcessor} has been terminated by the - * source producer with a success or an error. - * - * @return {@code true} if this {@link NextProcessor} is successful, {@code false} otherwise. - */ + * Indicates whether this {@link NextProcessor} has been terminated by the + * source producer with a success or an error. + * + * @return {@code true} if this {@link NextProcessor} is successful, {@code false} otherwise. + */ + @Override public boolean isTerminated() { return subscribers == TERMINATED; } /** - * Return the produced {@link Throwable} error if any or null - * - * @return the produced {@link Throwable} error if any or null - */ + * Return the produced {@link Throwable} error if any or null + * + * @return the produced {@link Throwable} error if any or null + */ @Nullable + @Override public Throwable getError() { return error; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ReplayProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/ReplayProcessor.java new file mode 100644 index 0000000000..39b1daaf5b --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/ReplayProcessor.java @@ -0,0 +1,686 @@ +/* + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.stream.Stream; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.core.publisher.Sinks.EmitResult; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; +import reactor.util.context.Context; + +import static reactor.core.publisher.FluxReplay.ReplaySubscriber.EMPTY; +import static reactor.core.publisher.FluxReplay.ReplaySubscriber.TERMINATED; + +/** + * Replays all or the last N items to Subscribers. + *

    + * + *

    + * + * @param the value type + * @deprecated To be removed in 3.5, prefer clear cut usage of {@link Sinks} through + * variations under {@link Sinks.MulticastReplaySpec Sinks.many().replay()}. + */ +@Deprecated +public final class ReplayProcessor extends FluxProcessor + implements Fuseable, InternalManySink { + + /** + * Create a {@link ReplayProcessor} that caches the last element it has pushed, + * replaying it to late subscribers. This is a buffer-based ReplayProcessor with + * a history size of 1. + *

    + * + * + * @param the type of the pushed elements + * + * @return a new {@link ReplayProcessor} that replays its last pushed element to each new + * {@link Subscriber} + * @deprecated use {@link Sinks.MulticastReplaySpec#latest() Sinks.many().replay().latest()} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor cacheLast() { + return cacheLastOrDefault(null); + } + + /** + * Create a {@link ReplayProcessor} that caches the last element it has pushed, + * replaying it to late subscribers. If a {@link Subscriber} comes in before + * any value has been pushed, then the {@code defaultValue} is emitted instead. + * This is a buffer-based ReplayProcessor with a history size of 1. + *

    + * + * + * @param value a default value to start the sequence with in case nothing has been + * cached yet. + * @param the type of the pushed elements + * + * @return a new {@link ReplayProcessor} that replays its last pushed element to each new + * {@link Subscriber}, or a default one if nothing was pushed yet + * @deprecated use {@link Sinks.MulticastReplaySpec#latestOrDefault(Object) Sinks.many().replay().latestOrDefault(value)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor cacheLastOrDefault(@Nullable T value) { + ReplayProcessor b = create(1); + if (value != null) { + b.onNext(value); + } + return b; + } + + /** + * Create a new {@link ReplayProcessor} that replays an unbounded number of elements, + * using a default internal {@link Queues#SMALL_BUFFER_SIZE Queue}. + * + * @param the type of the pushed elements + * + * @return a new {@link ReplayProcessor} that replays the whole history to each new + * {@link Subscriber}. + * @deprecated use {@link Sinks.MulticastReplaySpec#all() Sinks.many().replay().all()} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor create() { + return create(Queues.SMALL_BUFFER_SIZE, true); + } + + /** + * Create a new {@link ReplayProcessor} that replays up to {@code historySize} + * elements. + * + * @param historySize the backlog size, ie. maximum items retained for replay. + * @param the type of the pushed elements + * + * @return a new {@link ReplayProcessor} that replays a limited history to each new + * {@link Subscriber}. + * @deprecated use {@link Sinks.MulticastReplaySpec#limit(int) Sinks.many().replay().limit(historySize)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor create(int historySize) { + return create(historySize, false); + } + + /** + * Create a new {@link ReplayProcessor} that either replay all the elements or a + * limited amount of elements depending on the {@code unbounded} parameter. + * + * @param historySize maximum items retained if bounded, or initial link size if unbounded + * @param unbounded true if "unlimited" data store must be supplied + * @param the type of the pushed elements + * + * @return a new {@link ReplayProcessor} that replays the whole history to each new + * {@link Subscriber} if configured as unbounded, a limited history otherwise. + * @deprecated use {@link Sinks.MulticastReplaySpec#limit(int) Sinks.many().replay().limit(historySize)} + * for bounded cases ({@code unbounded == false}) or {@link Sinks.MulticastReplaySpec#all(int) Sinks.many().replay().all(bufferSize)} + * otherwise (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor create(int historySize, boolean unbounded) { + FluxReplay.ReplayBuffer buffer; + if (unbounded) { + buffer = new FluxReplay.UnboundedReplayBuffer<>(historySize); + } + else { + buffer = new FluxReplay.SizeBoundReplayBuffer<>(historySize); + } + return new ReplayProcessor<>(buffer); + } + + /** + * Creates a time-bounded replay processor. + *

    + * In this setting, the {@code ReplayProcessor} internally tags each observed item + * with a timestamp value supplied by the {@link Schedulers#parallel()} and keeps only + * those whose age is less than the supplied time value converted to milliseconds. For + * example, an item arrives at T=0 and the max age is set to 5; at T>=5 this first + * item is then evicted by any subsequent item or termination signal, leaving the + * buffer empty. + *

    + * Once the processor is terminated, subscribers subscribing to it will receive items + * that remained in the buffer after the terminal signal, regardless of their age. + *

    + * If an subscriber subscribes while the {@code ReplayProcessor} is active, it will + * observe only those items from within the buffer that have an age less than the + * specified time, and each item observed thereafter, even if the buffer evicts items + * due to the time constraint in the mean time. In other words, once an subscriber + * subscribes, it observes items without gaps in the sequence except for any outdated + * items at the beginning of the sequence. + *

    + * + * @param the type of items observed and emitted by the Processor + * @param maxAge the maximum age of the contained items + * + * @return a new {@link ReplayProcessor} that replays elements based on their age. + * @deprecated use {@link Sinks.MulticastReplaySpec#limit(Duration) Sinks.many().replay().limit(maxAge)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor createTimeout(Duration maxAge) { + return createTimeout(maxAge, Schedulers.parallel()); + } + + /** + * Creates a time-bounded replay processor. + *

    + * In this setting, the {@code ReplayProcessor} internally tags each observed item + * with a timestamp value supplied by the {@link Scheduler} and keeps only + * those whose age is less than the supplied time value converted to milliseconds. For + * example, an item arrives at T=0 and the max age is set to 5; at T>=5 this first + * item is then evicted by any subsequent item or termination signal, leaving the + * buffer empty. + *

    + * Once the processor is terminated, subscribers subscribing to it will receive items + * that remained in the buffer after the terminal signal, regardless of their age. + *

    + * If an subscriber subscribes while the {@code ReplayProcessor} is active, it will + * observe only those items from within the buffer that have an age less than the + * specified time, and each item observed thereafter, even if the buffer evicts items + * due to the time constraint in the mean time. In other words, once an subscriber + * subscribes, it observes items without gaps in the sequence except for any outdated + * items at the beginning of the sequence. + *

    + * + * @param the type of items observed and emitted by the Processor + * @param maxAge the maximum age of the contained items + * + * @return a new {@link ReplayProcessor} that replays elements based on their age. + * @deprecated use {@link Sinks.MulticastReplaySpec#limit(Duration, Scheduler) Sinks.many().replay().limit(maxAge, scheduler)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor createTimeout(Duration maxAge, Scheduler scheduler) { + return createSizeAndTimeout(Integer.MAX_VALUE, maxAge, scheduler); + } + + /** + * Creates a time- and size-bounded replay processor. + *

    + * In this setting, the {@code ReplayProcessor} internally tags each received item + * with a timestamp value supplied by the {@link Schedulers#parallel()} and holds at + * most + * {@code size} items in its internal buffer. It evicts items from the start of the + * buffer if their age becomes less-than or equal to the supplied age in milliseconds + * or the buffer reaches its {@code size} limit. + *

    + * When subscribers subscribe to a terminated {@code ReplayProcessor}, they observe + * the items that remained in the buffer after the terminal signal, regardless of + * their age, but at most {@code size} items. + *

    + * If an subscriber subscribes while the {@code ReplayProcessor} is active, it will + * observe only those items from within the buffer that have age less than the + * specified time and each subsequent item, even if the buffer evicts items due to the + * time constraint in the mean time. In other words, once an subscriber subscribes, it + * observes items without gaps in the sequence except for the outdated items at the + * beginning of the sequence. + *

    + * + * @param the type of items observed and emitted by the Processor + * @param maxAge the maximum age of the contained items + * @param size the maximum number of buffered items + * + * @return a new {@link ReplayProcessor} that replay up to {@code size} elements, but + * will evict them from its history based on their age. + * @deprecated use {@link Sinks.MulticastReplaySpec#limit(int, Duration) Sinks.many().replay().limit(size, maxAge)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor createSizeAndTimeout(int size, Duration maxAge) { + return createSizeAndTimeout(size, maxAge, Schedulers.parallel()); + } + + /** + * Creates a time- and size-bounded replay processor. + *

    + * In this setting, the {@code ReplayProcessor} internally tags each received item + * with a timestamp value supplied by the {@link Scheduler} and holds at most + * {@code size} items in its internal buffer. It evicts items from the start of the + * buffer if their age becomes less-than or equal to the supplied age in milliseconds + * or the buffer reaches its {@code size} limit. + *

    + * When subscribers subscribe to a terminated {@code ReplayProcessor}, they observe + * the items that remained in the buffer after the terminal signal, regardless of + * their age, but at most {@code size} items. + *

    + * If an subscriber subscribes while the {@code ReplayProcessor} is active, it will + * observe only those items from within the buffer that have age less than the + * specified time and each subsequent item, even if the buffer evicts items due to the + * time constraint in the mean time. In other words, once an subscriber subscribes, it + * observes items without gaps in the sequence except for the outdated items at the + * beginning of the sequence. + *

    + * + * @param the type of items observed and emitted by the Processor + * @param maxAge the maximum age of the contained items in milliseconds + * @param size the maximum number of buffered items + * @param scheduler the {@link Scheduler} that provides the current time + * + * @return a new {@link ReplayProcessor} that replay up to {@code size} elements, but + * will evict them from its history based on their age. + * @deprecated use {@link Sinks.MulticastReplaySpec#limit(int, Duration, Scheduler) Sinks.many().replay().limit(size, maxAge, scheduler)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static ReplayProcessor createSizeAndTimeout(int size, + Duration maxAge, + Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + if (size <= 0) { + throw new IllegalArgumentException("size > 0 required but it was " + size); + } + return new ReplayProcessor<>(new FluxReplay.SizeAndTimeBoundReplayBuffer<>(size, + maxAge.toNanos(), + scheduler)); + } + + final FluxReplay.ReplayBuffer buffer; + + Subscription subscription; + + volatile FluxReplay.ReplaySubscription[] subscribers; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater + SUBSCRIBERS = AtomicReferenceFieldUpdater.newUpdater(ReplayProcessor.class, + FluxReplay.ReplaySubscription[].class, + "subscribers"); + + ReplayProcessor(FluxReplay.ReplayBuffer buffer) { + this.buffer = buffer; + SUBSCRIBERS.lazySet(this, EMPTY); + } + + @Override + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + FluxReplay.ReplaySubscription rs = new ReplayInner<>(actual, this); + actual.onSubscribe(rs); + + if (add(rs)) { + if (rs.isCancelled()) { + remove(rs); + return; + } + } + buffer.replay(rs); + } + + @Override + @Nullable + public Throwable getError() { + return buffer.getError(); + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT){ + return subscription; + } + if (key == Attr.CAPACITY) return buffer.capacity(); + + return super.scanUnsafe(key); + } + + @Override + public Stream inners() { + return Stream.of(subscribers); + } + + @Override + public long downstreamCount() { + return subscribers.length; + } + + @Override + public boolean isTerminated() { + return buffer.isDone(); + } + + boolean add(FluxReplay.ReplaySubscription rs) { + for (; ; ) { + FluxReplay.ReplaySubscription[] a = subscribers; + if (a == TERMINATED) { + return false; + } + int n = a.length; + + @SuppressWarnings("unchecked") FluxReplay.ReplaySubscription[] b = + new ReplayInner[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = rs; + if (SUBSCRIBERS.compareAndSet(this, a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(FluxReplay.ReplaySubscription rs) { + outer: + for (; ; ) { + FluxReplay.ReplaySubscription[] a = subscribers; + if (a == TERMINATED || a == EMPTY) { + return; + } + int n = a.length; + + for (int i = 0; i < n; i++) { + if (a[i] == rs) { + FluxReplay.ReplaySubscription[] b; + + if (n == 1) { + b = EMPTY; + } + else { + b = new ReplayInner[n - 1]; + System.arraycopy(a, 0, b, 0, i); + System.arraycopy(a, i + 1, b, i, n - i - 1); + } + + if (SUBSCRIBERS.compareAndSet(this, a, b)) { + return; + } + + continue outer; + } + } + + break; + } + } + + @Override + public void onSubscribe(Subscription s) { + if (buffer.isDone()) { + s.cancel(); + } + else if (Operators.validate(subscription, s)) { + subscription = s; + s.request(Long.MAX_VALUE); + } + } + + @Override + public Context currentContext() { + return Operators.multiSubscribersContext(subscribers); + } + + @Override + public int getPrefetch() { + return Integer.MAX_VALUE; + } + + @Override + public void onComplete() { + //no particular error condition handling for onComplete + @SuppressWarnings("unused") EmitResult emitResult = tryEmitComplete(); + } + + @Override + public EmitResult tryEmitComplete() { + FluxReplay.ReplayBuffer b = buffer; + if (b.isDone()) { + return EmitResult.FAIL_TERMINATED; + } + + b.onComplete(); + + @SuppressWarnings("unchecked") FluxReplay.ReplaySubscription[] a = + SUBSCRIBERS.getAndSet(this, TERMINATED); + + for (FluxReplay.ReplaySubscription rs : a) { + b.replay(rs); + } + return EmitResult.OK; + } + + @Override + public void onError(Throwable throwable) { + emitError(throwable, Sinks.EmitFailureHandler.FAIL_FAST); + } + + @Override + public EmitResult tryEmitError(Throwable t) { + FluxReplay.ReplayBuffer b = buffer; + if (b.isDone()) { + return EmitResult.FAIL_TERMINATED; + } + + b.onError(t); + + @SuppressWarnings("unchecked") FluxReplay.ReplaySubscription[] a = + SUBSCRIBERS.getAndSet(this, TERMINATED); + + for (FluxReplay.ReplaySubscription rs : a) { + b.replay(rs); + } + return EmitResult.OK; + } + + @Override + public void onNext(T t) { + emitNext(t, Sinks.EmitFailureHandler.FAIL_FAST); + } + + @Override + public EmitResult tryEmitNext(T t) { + FluxReplay.ReplayBuffer b = buffer; + if (b.isDone()) { + return EmitResult.FAIL_TERMINATED; + } + + //note: ReplayProcessor can so far ALWAYS buffer the element, no FAIL_ZERO_SUBSCRIBER here + b.add(t); + for (FluxReplay.ReplaySubscription rs : subscribers) { + b.replay(rs); + } + return EmitResult.OK; + } + + @Override + public int currentSubscriberCount() { + return subscribers.length; + } + + @Override + public Flux asFlux() { + return this; + } + + @Override + protected boolean isIdentityProcessor() { + return true; + } + + static final class ReplayInner + implements FluxReplay.ReplaySubscription { + + final CoreSubscriber actual; + + final ReplayProcessor parent; + + final FluxReplay.ReplayBuffer buffer; + + int index; + + int tailIndex; + + Object node; + + volatile int wip; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(ReplayInner.class, + "wip"); + + volatile long requested; + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(ReplayInner.class, + "requested"); + + int fusionMode; + + ReplayInner(CoreSubscriber actual, + ReplayProcessor parent) { + this.actual = actual; + this.parent = parent; + this.buffer = parent.buffer; + } + + @Override + public long requested() { + return requested; + } + + @Override + public boolean isCancelled() { + return requested == Long.MIN_VALUE; + } + + @Override + public CoreSubscriber actual() { + return actual; + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & ASYNC) != 0) { + fusionMode = ASYNC; + return ASYNC; + } + return NONE; + } + + @Override + @Nullable + public T poll() { + return buffer.poll(this); + } + + @Override + public void clear() { + buffer.clear(this); + } + + @Override + public boolean isEmpty() { + return buffer.isEmpty(this); + } + + @Override + public int size() { + return buffer.size(this); + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + if (fusionMode() == NONE) { + Operators.addCapCancellable(REQUESTED, this, n); + } + buffer.replay(this); + } + } + + @Override + public void requestMore(int index) { + this.index = index; + } + + @Override + public void cancel() { + if (REQUESTED.getAndSet(this, Long.MIN_VALUE) != Long.MIN_VALUE) { + parent.remove(this); + + if (enter()) { + node = null; + } + } + } + + @Override + public void node(@Nullable Object node) { + this.node = node; + } + + @Override + public int fusionMode() { + return fusionMode; + } + + @Override + @Nullable + public Object node() { + return node; + } + + @Override + public int index() { + return index; + } + + @Override + public void index(int index) { + this.index = index; + } + + @Override + public int tailIndex() { + return tailIndex; + } + + @Override + public void tailIndex(int tailIndex) { + this.tailIndex = tailIndex; + } + + @Override + public boolean enter() { + return WIP.getAndIncrement(this) == 0; + } + + @Override + public int leave(int missed) { + return WIP.addAndGet(this, -missed); + } + + @Override + public void produced(long n) { + REQUESTED.addAndGet(this, -n); + } + } +} \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java index 208c2ae172..3c63f094b5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java @@ -33,7 +33,7 @@ * @author Simon Baslé */ final class SinkManyBestEffort extends Flux - implements InternalManySink, Scannable { + implements InternalManySink, Scannable, DirectInnerContainer { static final DirectInner[] EMPTY = new DirectInner[0]; static final DirectInner[] TERMINATED = new DirectInner[0]; @@ -221,7 +221,7 @@ public void subscribe(CoreSubscriber actual) { * * @return {@code true} if the inner could be added, {@code false} if the publisher cannot accept new subscribers */ - boolean add(DirectInner s) { + public boolean add(DirectInner s) { DirectInner[] a = subscribers; if (a == TERMINATED) { return false; @@ -252,7 +252,7 @@ boolean add(DirectInner s) { * @param s the {@link SinkManyBestEffort.DirectInner} to remove */ @SuppressWarnings("unchecked") - void remove(DirectInner s) { + public void remove(DirectInner s) { DirectInner[] a = subscribers; if (a == TERMINATED || a == EMPTY) { return; @@ -295,14 +295,14 @@ void remove(DirectInner s) { static class DirectInner extends AtomicBoolean implements InnerProducer { final CoreSubscriber actual; - final SinkManyBestEffort parent; + final DirectInnerContainer parent; volatile long requested; @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater( DirectInner.class, "requested"); - DirectInner(CoreSubscriber actual, SinkManyBestEffort parent) { + DirectInner(CoreSubscriber actual, DirectInnerContainer parent) { this.actual = actual; this.parent = parent; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/UnicastProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/UnicastProcessor.java new file mode 100644 index 0000000000..d32f9e7f1d --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/UnicastProcessor.java @@ -0,0 +1,652 @@ +/* + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.Exceptions; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.core.publisher.Sinks.EmitResult; +import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; +import reactor.util.context.Context; + +/** + * A Processor implementation that takes a custom queue and allows + * only a single subscriber. UnicastProcessor allows multiplexing of the events which + * means that it supports multiple producers and only one consumer. + * However, it should be noticed that multi-producer case is only valid if appropriate + * Queue + * is provided. Otherwise, it could break + * Reactive Streams Spec if Publishers + * publish on different threads. + * + *

    + * + *

    + * + *
    + *
    + * + *

    + * Note: UnicastProcessor does not respect the actual subscriber's + * demand as it is described in + * Reactive Streams Spec. However, + * UnicastProcessor embraces configurable Queue internally which allows enabling + * backpressure support and preventing of consumer's overwhelming. + * + * Hence, interaction model between producers and UnicastProcessor will be PUSH + * only. In opposite, interaction model between UnicastProcessor and consumer will be + * PUSH-PULL as defined in + * Reactive Streams Spec. + * + * In the case when upstream's signals overflow the bound of internal Queue, + * UnicastProcessor will fail with signaling onError( + * {@literal reactor.core.Exceptions.OverflowException}). + * + *

    + * + *

    + *

    + * + *
    + *
    + * + *

    + * Note: The implementation keeps the order of signals. That means that in + * case of terminal signal (completion or error signals) it will be postponed + * until all of the previous signals has been consumed. + *

    + * + *

    + *

    + * + * @param the input and output type + * @deprecated to be removed in 3.5, prefer clear cut usage of {@link Sinks} through + * variations under {@link Sinks.UnicastSpec Sinks.many().unicast()}. + */ +@Deprecated +public final class UnicastProcessor extends FluxProcessor + implements Fuseable.QueueSubscription, Fuseable, InnerOperator, + InternalManySink { + + /** + * Create a new {@link UnicastProcessor} that will buffer on an internal queue in an + * unbounded fashion. + * + * @param the relayed type + * @return a unicast {@link FluxProcessor} + * @deprecated use {@link Sinks.UnicastSpec#onBackpressureBuffer() Sinks.many().unicast().onBackpressureBuffer()} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static UnicastProcessor create() { + return new UnicastProcessor<>(Queues.unbounded().get()); + } + + /** + * Create a new {@link UnicastProcessor} that will buffer on a provided queue in an + * unbounded fashion. + * + * @param queue the buffering queue + * @param the relayed type + * @return a unicast {@link FluxProcessor} + * @deprecated use {@link Sinks.UnicastSpec#onBackpressureBuffer(Queue) Sinks.many().unicast().onBackpressureBuffer(queue)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static UnicastProcessor create(Queue queue) { + return new UnicastProcessor<>(Hooks.wrapQueue(queue)); + } + + /** + * Create a new {@link UnicastProcessor} that will buffer on a provided queue in an + * unbounded fashion. + * + * @param queue the buffering queue + * @param endcallback called on any terminal signal + * @param the relayed type + * @return a unicast {@link FluxProcessor} + * @deprecated use {@link Sinks.UnicastSpec#onBackpressureBuffer(Queue, Disposable) Sinks.many().unicast().onBackpressureBuffer(queue, endCallback)} + * (or the unsafe variant if you're sure about external synchronization). To be removed in 3.5. + */ + @Deprecated + public static UnicastProcessor create(Queue queue, Disposable endcallback) { + return new UnicastProcessor<>(Hooks.wrapQueue(queue), endcallback); + } + + /** + * Create a new {@link UnicastProcessor} that will buffer on a provided queue in an + * unbounded fashion. + * + * @param queue the buffering queue + * @param endcallback called on any terminal signal + * @param onOverflow called when queue.offer return false and unicastProcessor is + * about to emit onError. + * @param the relayed type + * + * @return a unicast {@link FluxProcessor} + * @deprecated use {@link Sinks.UnicastSpec#onBackpressureBuffer(Queue, Disposable) Sinks.many().unicast().onBackpressureBuffer(queue, endCallback)} + * (or the unsafe variant if you're sure about external synchronization). The {@code onOverflow} callback is not + * supported anymore. To be removed in 3.5. + */ + @Deprecated + public static UnicastProcessor create(Queue queue, + Consumer onOverflow, + Disposable endcallback) { + return new UnicastProcessor<>(Hooks.wrapQueue(queue), onOverflow, endcallback); + } + + final Queue queue; + final Consumer onOverflow; + + volatile Disposable onTerminate; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater ON_TERMINATE = + AtomicReferenceFieldUpdater.newUpdater(UnicastProcessor.class, Disposable.class, "onTerminate"); + + volatile boolean done; + Throwable error; + + boolean hasDownstream; //important to not loose the downstream too early and miss discard hook, while having relevant hasDownstreams() + volatile CoreSubscriber actual; + + volatile boolean cancelled; + + volatile int once; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(UnicastProcessor.class, "once"); + + volatile int wip; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(UnicastProcessor.class, "wip"); + + volatile int discardGuard; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater DISCARD_GUARD = + AtomicIntegerFieldUpdater.newUpdater(UnicastProcessor.class, "discardGuard"); + + volatile long requested; + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(UnicastProcessor.class, "requested"); + + boolean outputFused; + + public UnicastProcessor(Queue queue) { + this.queue = Objects.requireNonNull(queue, "queue"); + this.onTerminate = null; + this.onOverflow = null; + } + + public UnicastProcessor(Queue queue, Disposable onTerminate) { + this.queue = Objects.requireNonNull(queue, "queue"); + this.onTerminate = Objects.requireNonNull(onTerminate, "onTerminate"); + this.onOverflow = null; + } + + @Deprecated + public UnicastProcessor(Queue queue, + Consumer onOverflow, + Disposable onTerminate) { + this.queue = Objects.requireNonNull(queue, "queue"); + this.onOverflow = Objects.requireNonNull(onOverflow, "onOverflow"); + this.onTerminate = Objects.requireNonNull(onTerminate, "onTerminate"); + } + + @Override + public int getBufferSize() { + return Queues.capacity(this.queue); + } + + @Override + public Stream inners() { + return hasDownstream ? Stream.of(Scannable.from(actual)) : Stream.empty(); + } + + @Override + public Object scanUnsafe(Attr key) { + if (Attr.ACTUAL == key) return actual(); + if (Attr.BUFFERED == key) return queue.size(); + if (Attr.PREFETCH == key) return Integer.MAX_VALUE; + if (Attr.CANCELLED == key) return cancelled; + + //TERMINATED and ERROR covered in super + return super.scanUnsafe(key); + } + + @Override + public void onComplete() { + //no particular error condition handling for onComplete + @SuppressWarnings("unused") EmitResult emitResult = tryEmitComplete(); + } + + @Override + public EmitResult tryEmitComplete() { + if (done) { + return EmitResult.FAIL_TERMINATED; + } + if (cancelled) { + return EmitResult.FAIL_CANCELLED; + } + + done = true; + + doTerminate(); + + drain(null); + return EmitResult.OK; + } + + @Override + public void onError(Throwable throwable) { + emitError(throwable, Sinks.EmitFailureHandler.FAIL_FAST); + } + + @Override + public EmitResult tryEmitError(Throwable t) { + if (done) { + return EmitResult.FAIL_TERMINATED; + } + if (cancelled) { + return EmitResult.FAIL_CANCELLED; + } + + error = t; + done = true; + + doTerminate(); + + drain(null); + return EmitResult.OK; + } + + @Override + public void onNext(T t) { + emitNext(t, Sinks.EmitFailureHandler.FAIL_FAST); + } + + @Override + public void emitNext(T value, Sinks.EmitFailureHandler failureHandler) { + if (onOverflow == null) { + InternalManySink.super.emitNext(value, failureHandler); + return; + } + + // TODO consider deprecating onOverflow and suggesting using a strategy instead + InternalManySink.super.emitNext( + value, (signalType, emission) -> { + boolean shouldRetry = failureHandler.onEmitFailure(SignalType.ON_NEXT, emission); + if (!shouldRetry) { + switch (emission) { + case FAIL_ZERO_SUBSCRIBER: + case FAIL_OVERFLOW: + try { + onOverflow.accept(value); + } + catch (Throwable e) { + Exceptions.throwIfFatal(e); + emitError(e, Sinks.EmitFailureHandler.FAIL_FAST); + } + break; + } + } + return shouldRetry; + } + ); + } + + @Override + public EmitResult tryEmitNext(T t) { + if (done) { + return EmitResult.FAIL_TERMINATED; + } + if (cancelled) { + return EmitResult.FAIL_CANCELLED; + } + + if (!queue.offer(t)) { + return (once > 0) ? EmitResult.FAIL_OVERFLOW : EmitResult.FAIL_ZERO_SUBSCRIBER; + } + drain(t); + return EmitResult.OK; + } + + @Override + public int currentSubscriberCount() { + return hasDownstream ? 1 : 0; + } + + @Override + public Flux asFlux() { + return this; + } + + @Override + protected boolean isIdentityProcessor() { + return true; + } + + void doTerminate() { + Disposable r = onTerminate; + if (r != null && ON_TERMINATE.compareAndSet(this, r, null)) { + r.dispose(); + } + } + + void drainRegular(CoreSubscriber a) { + int missed = 1; + + final Queue q = queue; + + for (;;) { + + long r = requested; + long e = 0L; + + while (r != e) { + boolean d = done; + + T t = q.poll(); + boolean empty = t == null; + + if (checkTerminated(d, empty, a, q, t)) { + return; + } + + if (empty) { + break; + } + + a.onNext(t); + + e++; + } + + if (r == e) { + if (checkTerminated(done, q.isEmpty(), a, q, null)) { + return; + } + } + + if (e != 0 && r != Long.MAX_VALUE) { + REQUESTED.addAndGet(this, -e); + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + } + + void drainFused(CoreSubscriber a) { + int missed = 1; + + for (;;) { + + if (cancelled) { + // We are the holder of the queue, but we still have to perform discarding under the guarded block + // to prevent any racing done by downstream + this.clear(); + hasDownstream = false; + return; + } + + boolean d = done; + + a.onNext(null); + + if (d) { + hasDownstream = false; + + Throwable ex = error; + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + return; + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + } + + void drain(@Nullable T dataSignalOfferedBeforeDrain) { + if (WIP.getAndIncrement(this) != 0) { + if (dataSignalOfferedBeforeDrain != null) { + if (cancelled) { + Operators.onDiscard(dataSignalOfferedBeforeDrain, + actual.currentContext()); + } + else if (done) { + Operators.onNextDropped(dataSignalOfferedBeforeDrain, + currentContext()); + } + } + return; + } + + int missed = 1; + + for (;;) { + CoreSubscriber a = actual; + if (a != null) { + + if (outputFused) { + drainFused(a); + } else { + drainRegular(a); + } + return; + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + } + + boolean checkTerminated(boolean d, boolean empty, CoreSubscriber a, Queue q, @Nullable T t) { + if (cancelled) { + Operators.onDiscard(t, a.currentContext()); + Operators.onDiscardQueueWithClear(q, a.currentContext(), null); + hasDownstream = false; + return true; + } + if (d && empty) { + Throwable e = error; + hasDownstream = false; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return true; + } + + return false; + } + + @Override + public void onSubscribe(Subscription s) { + if (done || cancelled) { + s.cancel(); + } else { + s.request(Long.MAX_VALUE); + } + } + + @Override + public int getPrefetch() { + return Integer.MAX_VALUE; + } + + @Override + public Context currentContext() { + CoreSubscriber actual = this.actual; + return actual != null ? actual.currentContext() : Context.empty(); + } + + @Override + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { + + this.hasDownstream = true; + actual.onSubscribe(this); + this.actual = actual; + if (cancelled) { + this.hasDownstream = false; + } else { + drain(null); + } + } else { + Operators.error(actual, new IllegalStateException("UnicastProcessor " + + "allows only a single Subscriber")); + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + Operators.addCap(REQUESTED, this, n); + drain(null); + } + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + + doTerminate(); + + if (WIP.getAndIncrement(this) == 0) { + if (!outputFused) { + // discard MUST be happening only and only if there is no racing on elements consumption + // which is guaranteed by the WIP guard here in case non-fused output + Operators.onDiscardQueueWithClear(queue, currentContext(), null); + } + hasDownstream = false; + } + } + + @Override + @Nullable + public T poll() { + return queue.poll(); + } + + @Override + public int size() { + return queue.size(); + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + + @Override + public void clear() { + // use guard on the queue instance as the best way to ensure there is no racing on draining + // the call to this method must be done only during the ASYNC fusion so all the callers will be waiting + // this should not be performance costly with the assumption the cancel is rare operation + if (DISCARD_GUARD.getAndIncrement(this) != 0) { + return; + } + + int missed = 1; + + for (;;) { + Operators.onDiscardQueueWithClear(queue, currentContext(), null); + + int dg = discardGuard; + if (missed == dg) { + missed = DISCARD_GUARD.addAndGet(this, -missed); + if (missed == 0) { + break; + } + } + else { + missed = dg; + } + } + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & Fuseable.ASYNC) != 0) { + outputFused = true; + return Fuseable.ASYNC; + } + return Fuseable.NONE; + } + + @Override + public boolean isDisposed() { + return cancelled || done; + } + + @Override + public boolean isTerminated() { + return done; + } + + @Override + @Nullable + public Throwable getError() { + return error; + } + + @Override + public CoreSubscriber actual() { + return actual; + } + + @Override + public long downstreamCount() { + return hasDownstreams() ? 1L : 0L; + } + + @Override + public boolean hasDownstreams() { + return hasDownstream; + } + +} From cde76d305b92cb4a8f4e3b66336dc18a643042a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 11 Jul 2022 16:58:42 +0200 Subject: [PATCH 038/312] Polish Observation names and KeyValue tag keys (#3110) This commit polishes Micrometer.observation(): - anonymous observation (no `name()`) now named `reactor.observation` - named observations simply use the user-defined name (no more `.observation.flow` suffix) - all KeyValue "tags" set by reactor for observations are prefixed with `reactor.` It also fixes the javadoc links for reactor-core classes, which were not set up correctly in reactor-core-micrometer due to a bad if condition in javadoc.gradle. --- gradle/javadoc.gradle | 4 +- .../observability/micrometer/Micrometer.java | 14 ++--- .../MicrometerMeterListenerConfiguration.java | 10 ++-- .../MicrometerMeterListenerFactory.java | 1 + .../MicrometerObservationListener.java | 26 ++++---- ...meterObservationListenerConfiguration.java | 8 +-- ...rometerMeterListenerConfigurationTest.java | 14 +++-- .../MicrometerObservationIntegrationTest.java | 9 +-- ...rObservationListenerConfigurationTest.java | 12 ++-- ...rometerObservationListenerFactoryTest.java | 4 +- .../MicrometerObservationListenerTest.java | 60 +++++++++++-------- 11 files changed, 89 insertions(+), 73 deletions(-) diff --git a/gradle/javadoc.gradle b/gradle/javadoc.gradle index a2c3e89aee..b38ba27f77 100644 --- a/gradle/javadoc.gradle +++ b/gradle/javadoc.gradle @@ -27,13 +27,15 @@ javadoc { options.memberLevel = JavadocMemberLevel.PROTECTED - if (project.name.contains("core")) { + if (project.name == "reactor-core") { options.links([rootProject.jdkJavadoc, "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/"] as String[]) } else { options.links([rootProject.jdkJavadoc, "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/", "https://projectreactor.io/docs/core/release/api/"] as String[]); + //TODO add micrometer javadocs here + //tried to using javadoc.io but it is currently impossible to link to these from Javadoc JDK 8, no package-list } options.tags = [ "apiNote:a:API Note:", "implSpec:a:Implementation Requirements:", "implNote:a:Implementation Note:" ] diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index f34892f08a..6327f29fc7 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -38,8 +38,7 @@ public final class Micrometer { private static MeterRegistry registry = Metrics.globalRegistry; /** - * The default "name" to use as a prefix for meter or observation IDs if the instrumented sequence doesn't - * define a {@link reactor.core.publisher.Flux#name(String) name}. + * The default "name" to use as a prefix for meter if the instrumented sequence doesn't define a {@link reactor.core.publisher.Flux#name(String) name}. */ public static final String DEFAULT_METER_PREFIX = "reactor"; @@ -127,9 +126,9 @@ protected MeterRegistry useRegistry() { * To be used with either the {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} or * {@link reactor.core.publisher.Mono#tap(SignalListenerFactory)} operator. *

    - * The {@code NAME.observation.flow} {@link Observation} covers the entire length of the sequence, - * from subscription to termination. Said termination can be a cancellation, a completion with or without values - * or an error. This is denoted by the low cardinality {@code status} {@link KeyValue}. + * The {@link Observation} covers the entire length of the sequence, from subscription to termination. + * Said termination can be a cancellation, a completion with or without values or an error. + * This is denoted by the low cardinality {@code status} {@link KeyValue}. * In case of an exception, a high cardinality {@code exception} KeyValue with the exception class name is also added. * Finally, the low cardinality {@code type} KeyValue informs whether we're observing a {@code Flux} * or a {@code Mono}. @@ -137,8 +136,9 @@ protected MeterRegistry useRegistry() { * Note that the Micrometer {@code context-propagation} is used to populate thread locals * around the opening of the observation (upon {@code onSubscribe(Subscription)}). *

    - * Observation names are prefixed by the {@link reactor.core.publisher.Flux#name(String)} defined upstream - * of the tap if applicable or by the default prefix {@link #DEFAULT_METER_PREFIX}. + * The observation is named after the {@link reactor.core.publisher.Flux#name(String)} defined upstream + * of the tap if applicable or use {@code "reactor.observation"} otherwise (although it is strongly recommended + * to provide a meaningful name). * Similarly, Reactor tags defined upstream via eg. {@link reactor.core.publisher.Flux#tag(String, String)}) * are gathered and added to the default set of {@link io.micrometer.common.KeyValues} used by the Observation * as {@link Observation#lowCardinalityKeyValues(KeyValues) low cardinality keyValues}. diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java index 363e90686d..bf7992508d 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java @@ -43,7 +43,7 @@ final class MicrometerMeterListenerConfiguration { static MicrometerMeterListenerConfiguration fromFlux(Flux source, MeterRegistry meterRegistry, Clock clock) { Tags defaultTags = MicrometerMeterListener.DEFAULT_TAGS_FLUX; - final String name = resolveName(source, LOGGER); + final String name = resolveName(source, LOGGER, Micrometer.DEFAULT_METER_PREFIX); final Tags tags = resolveTags(source, defaultTags); return new MicrometerMeterListenerConfiguration(name, tags, meterRegistry, clock, false); @@ -51,7 +51,7 @@ static MicrometerMeterListenerConfiguration fromFlux(Flux source, MeterRegist static MicrometerMeterListenerConfiguration fromMono(Mono source, MeterRegistry meterRegistry, Clock clock) { Tags defaultTags = MicrometerMeterListener.DEFAULT_TAGS_MONO; - final String name = resolveName(source, LOGGER); + final String name = resolveName(source, LOGGER, Micrometer.DEFAULT_METER_PREFIX); final Tags tags = resolveTags(source, defaultTags); return new MicrometerMeterListenerConfiguration(name, tags, meterRegistry, clock, true); @@ -65,17 +65,17 @@ static MicrometerMeterListenerConfiguration fromMono(Mono source, MeterRegist * * @return a name */ - static String resolveName(Publisher source, Logger logger) { + static String resolveName(Publisher source, Logger logger, String defaultName) { Scannable scannable = Scannable.from(source); if (!scannable.isScanAvailable()) { logger.warn("Attempting to activate metrics but the upstream is not Scannable. You might want to use `name()` (and optionally `tags()`) right before this listener"); - return Micrometer.DEFAULT_METER_PREFIX; + return defaultName; } String nameOrDefault = scannable.name(); if (scannable.stepName() .equals(nameOrDefault)) { - return Micrometer.DEFAULT_METER_PREFIX; + return defaultName; } else { return nameOrDefault; diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java index 4c9722ca7b..028549a506 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java @@ -37,6 +37,7 @@ protected Clock useClock() { return Clock.SYSTEM; } + @SuppressWarnings("deprecation") protected MeterRegistry useRegistry() { return Micrometer.getRegistry(); } diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java index f4b477d3f8..1e7b050282 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java @@ -39,11 +39,17 @@ final class MicrometerObservationListener implements SignalListener { private static final Logger LOGGER = Loggers.getLogger(MicrometerObservationListener.class); - static final String OBSERVATION_FLOW = ".observation.flow"; + static final String ANONYMOUS_OBSERVATION = "reactor.observation"; + static final String KEY_STATUS = "reactor.status"; + static final String KEY_TYPE = "reactor.type"; + static final String STATUS_CANCELLED = MicrometerMeterListener.TAG_STATUS_CANCELLED; + static final String STATUS_COMPLETED = MicrometerMeterListener.TAG_STATUS_COMPLETED; + static final String STATUS_COMPLETED_EMPTY = MicrometerMeterListener.TAG_STATUS_COMPLETED_EMPTY; + static final String STATUS_ERROR = MicrometerMeterListener.TAG_STATUS_ERROR; /** * A value for the status tag, to be used when a Mono completes from onNext. - * In production, this is set to {@link MicrometerMeterListener#TAG_STATUS_COMPLETED}. + * In production, this is set to {@link #STATUS_COMPLETED}. * In some tests, this can be overridden as a way to assert {@link #doOnComplete()} is no-op. */ final String completedOnNextStatus; @@ -59,7 +65,7 @@ final class MicrometerObservationListener implements SignalListener { boolean valued; MicrometerObservationListener(ContextView subscriberContext, MicrometerObservationListenerConfiguration configuration) { - this(subscriberContext, configuration, MicrometerMeterListener.TAG_STATUS_COMPLETED); + this(subscriberContext, configuration, STATUS_COMPLETED); } //for test purposes, we can pass in a value for the status tag, to be used when a Mono completes from onNext @@ -73,7 +79,7 @@ final class MicrometerObservationListener implements SignalListener { //creation of the listener matches subscription (Publisher.subscribe(Subscriber) / doFirst) //while doOnSubscription matches the moment where the Publisher acknowledges said subscription subscribeToTerminalObservation = Observation.createNotStarted( - configuration.sequenceName + OBSERVATION_FLOW, + configuration.sequenceName, configuration.registry ) .contextualName(configuration.sequenceName) @@ -117,7 +123,7 @@ public Context addToContext(Context originalContext) { @Override public void doOnCancel() { Observation observation = subscribeToTerminalObservation - .lowCardinalityKeyValue(MicrometerMeterListener.TAG_KEY_STATUS, MicrometerMeterListener.TAG_STATUS_CANCELLED); + .lowCardinalityKeyValue(KEY_STATUS, STATUS_CANCELLED); observation.stop(); if (scope != null) { @@ -130,16 +136,16 @@ public void doOnComplete() { // We differentiate between empty completion and value completion via tags. String status = null; if (!valued) { - status = MicrometerMeterListener.TAG_STATUS_COMPLETED_EMPTY; + status = STATUS_COMPLETED_EMPTY; } else if (!configuration.isMono) { - status = MicrometerMeterListener.TAG_STATUS_COMPLETED; + status = STATUS_COMPLETED; } // if status == null, recording with OnComplete tag is done directly in onNext for the Mono(valued) case if (status != null) { Observation completeObservation = subscribeToTerminalObservation - .lowCardinalityKeyValue(MicrometerMeterListener.TAG_KEY_STATUS, status); + .lowCardinalityKeyValue(KEY_STATUS, status); completeObservation.stop(); if (scope != null) { @@ -151,7 +157,7 @@ else if (!configuration.isMono) { @Override public void doOnError(Throwable e) { Observation errorObservation = subscribeToTerminalObservation - .lowCardinalityKeyValue(MicrometerMeterListener.TAG_KEY_STATUS, MicrometerMeterListener.TAG_STATUS_ERROR) + .lowCardinalityKeyValue(KEY_STATUS, STATUS_ERROR) .error(e); errorObservation.stop(); @@ -166,7 +172,7 @@ public void doOnNext(T t) { if (configuration.isMono) { //record valued completion directly Observation completeObservation = subscribeToTerminalObservation - .lowCardinalityKeyValue(MicrometerMeterListener.TAG_KEY_STATUS, completedOnNextStatus); + .lowCardinalityKeyValue(KEY_STATUS, completedOnNextStatus); completeObservation.stop(); if (scope != null) { diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java index 34013e4475..ae63ea00d3 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java @@ -38,14 +38,14 @@ */ final class MicrometerObservationListenerConfiguration { - static final KeyValues DEFAULT_KV_FLUX = KeyValues.of("type", "Flux"); - static final KeyValues DEFAULT_KV_MONO = KeyValues.of("type", "Mono"); + static final KeyValues DEFAULT_KV_FLUX = KeyValues.of(MicrometerObservationListener.KEY_TYPE, "Flux"); + static final KeyValues DEFAULT_KV_MONO = KeyValues.of(MicrometerObservationListener.KEY_TYPE, "Mono"); private static final Logger LOGGER = Loggers.getLogger(MicrometerObservationListenerConfiguration.class); static MicrometerObservationListenerConfiguration fromFlux(Flux source, ObservationRegistry observationRegistry) { KeyValues defaultKeyValues = DEFAULT_KV_FLUX; - final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER); + final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, MicrometerObservationListener.ANONYMOUS_OBSERVATION); final KeyValues keyValues = resolveKeyValues(source, defaultKeyValues); return new MicrometerObservationListenerConfiguration(name, keyValues, observationRegistry, false); @@ -53,7 +53,7 @@ static MicrometerObservationListenerConfiguration fromFlux(Flux source, Obser static MicrometerObservationListenerConfiguration fromMono(Mono source, ObservationRegistry observationRegistry) { KeyValues defaultKeyValues = DEFAULT_KV_MONO; - final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER); + final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, MicrometerObservationListener.ANONYMOUS_OBSERVATION); final KeyValues keyValues = resolveKeyValues(source, defaultKeyValues); return new MicrometerObservationListenerConfiguration(name, keyValues, observationRegistry, true); diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java index f858b0962f..153e41e167 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java @@ -124,12 +124,13 @@ void fromMono(@Nullable String name, @Nullable String tag) { @Test void resolveName_notSet() { + String defaultName = "ANONYMOUS"; TestLogger logger = new TestLogger(false); Flux flux = Flux.just(1); - String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger); + String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger, defaultName); - assertThat(resolvedName).isEqualTo(Micrometer.DEFAULT_METER_PREFIX); + assertThat(resolvedName).isEqualTo(defaultName); assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); } @@ -138,7 +139,7 @@ void resolveName_setRightAbove() { TestLogger logger = new TestLogger(false); Flux flux = Flux.just(1).name("someName"); - String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger); + String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger, "UNEXPECTED"); assertThat(resolvedName).isEqualTo("someName"); assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); @@ -149,7 +150,7 @@ void resolveName_setHigherAbove() { TestLogger logger = new TestLogger(false); Flux flux = Flux.just(1).name("someName").filter(i -> i % 2 == 0).map(i -> i + 10); - String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger); + String resolvedName = MicrometerMeterListenerConfiguration.resolveName(flux, logger, "UNEXPECTED"); assertThat(resolvedName).isEqualTo("someName"); assertThat(logger.getOutContent() + logger.getErrContent()).as("logs").isEmpty(); @@ -157,12 +158,13 @@ void resolveName_setHigherAbove() { @Test void resolveName_notScannable() { + String defaultName = "ANONYMOUS"; TestLogger testLogger = new TestLogger(false); Publisher publisher = Operators::complete; - String resolvedName = MicrometerMeterListenerConfiguration.resolveName(publisher, testLogger); + String resolvedName = MicrometerMeterListenerConfiguration.resolveName(publisher, testLogger, defaultName); - assertThat(resolvedName).as("resolved name").isEqualTo(Micrometer.DEFAULT_METER_PREFIX); + assertThat(resolvedName).as("resolved name").isEqualTo(defaultName); assertThat(testLogger.getErrContent()).contains("Attempting to activate metrics but the upstream is not Scannable. You might want to use `name()` (and optionally `tags()`) right before this listener"); } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java index f2c09d2f43..f5b266b017 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java @@ -32,8 +32,6 @@ import reactor.core.scheduler.Schedulers; import static org.assertj.core.api.Assertions.assertThat; -import static reactor.core.observability.micrometer.MicrometerMeterListener.TAG_KEY_STATUS; -import static reactor.core.observability.micrometer.MicrometerMeterListener.TAG_STATUS_ERROR; /** * @author Simon Baslé @@ -60,8 +58,6 @@ public SampleTestRunnerConsumer yourCode() throws Exception { .doOnNext(v -> { if (id == 2L) throw EXCEPTION; }) - //FIXME enforce providing a name via String or ObservationConvention - //FIXME Micrometer taps should ignore name() .name("query" + id) .tap(Micrometer.observation(getObservationRegistry())); @@ -82,10 +78,9 @@ public SampleTestRunnerConsumer yourCode() throws Exception { spansAssert.hasSize(4); assertThatMain - //FIXME reactor-defined tags should have a reactor. prefix //FIXME reactor-defined Tags and KeyValues should be Documented - .hasTag(TAG_KEY_STATUS, TAG_STATUS_ERROR) - .hasTag("type", "Flux") + .hasTag("reactor.status", "error") + .hasTag("reactor.type", "Flux") .hasTag("interval", "500ms") .hasTag("size", "3") //TODO propose new duration assertion? span's timestamps should return Instant, not long. OTel is using nanos, Brave is storing long microsecond diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java index 9c92f492af..825686cd1d 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java @@ -61,17 +61,17 @@ void fromFlux(@Nullable String name, @Nullable String tag) { assertThat(configuration.sequenceName) .as("sequenceName") - .isEqualTo(name == null ? Micrometer.DEFAULT_METER_PREFIX : name); + .isEqualTo(name == null ? MicrometerObservationListener.ANONYMOUS_OBSERVATION : name); if (tag == null) { assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) .as("commonKeyValues without additional KeyValue") - .containsExactly("type=Flux"); + .containsExactly("reactor.type" + "=Flux"); } else { assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) .as("commonKeyValues") - .containsExactlyInAnyOrder("type=Flux", "tag="+tag); + .containsExactlyInAnyOrder("reactor.type" + "=Flux", "tag="+tag); } } @@ -101,17 +101,17 @@ void fromMono(@Nullable String name, @Nullable String tag) { assertThat(configuration.sequenceName) .as("sequenceName") - .isEqualTo(name == null ? Micrometer.DEFAULT_METER_PREFIX : name); + .isEqualTo(name == null ? MicrometerObservationListener.ANONYMOUS_OBSERVATION : name); if (tag == null) { assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) .as("commonKeyValues without additional KeyValue") - .containsExactly("type=Mono"); + .containsExactly("reactor.type" + "=Mono"); } else { assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) .as("commonKeyValues") - .containsExactlyInAnyOrder("type=Mono", "tag="+tag); + .containsExactlyInAnyOrder("reactor.type" + "=Mono", "tag="+tag); } } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java index 7889d3df38..e3c2fa5c36 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java @@ -46,7 +46,7 @@ void configurationFromMono() { assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); assertThat(configuration.isMono).as("isMono").isTrue(); - assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("tag(type=Mono)"); + assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("tag(" + "reactor.type" + "=Mono)"); } @Test @@ -55,7 +55,7 @@ void configurationFromFlux() { assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); assertThat(configuration.isMono).as("isMono").isFalse(); - assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("tag(type=Flux)"); + assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("tag(" + "reactor.type" + "=Flux)"); } @Test diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java index 2d94efdd26..69f57c6abb 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java @@ -69,7 +69,7 @@ public long monotonicTime() { @Test void whenStartedFluxWithDefaultName() { configuration = new MicrometerObservationListenerConfiguration( - Micrometer.DEFAULT_METER_PREFIX, + MicrometerObservationListener.ANONYMOUS_OBSERVATION, //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -87,7 +87,7 @@ void whenStartedFluxWithDefaultName() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("reactor.observation.flow") + .hasNameEqualTo("reactor.observation") .as("subscribeToTerminalObservation") .hasBeenStarted() .isNotStopped() @@ -116,7 +116,8 @@ void whenStartedFluxWithCustomName() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("testName.observation.flow") + .hasNameEqualTo("testName") + .hasContextualNameEqualTo("testName") .as("subscribeToTerminalObservation") .hasBeenStarted() .isNotStopped() @@ -127,7 +128,7 @@ void whenStartedFluxWithCustomName() { @Test void whenStartedMono() { configuration = new MicrometerObservationListenerConfiguration( - Micrometer.DEFAULT_METER_PREFIX, + MicrometerObservationListener.ANONYMOUS_OBSERVATION, //note: "type" key is added by MicrometerObservationListenerConfiguration#fromMono (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -145,7 +146,8 @@ void whenStartedMono() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("reactor.observation.flow") + .hasNameEqualTo("reactor.observation") + .hasContextualNameEqualTo("reactor.observation") .as("subscribeToTerminalObservation") .hasBeenStarted() .isNotStopped() @@ -167,14 +169,15 @@ void tapFromFluxWithTags() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("testFlux.observation.flow") + .hasNameEqualTo("testFlux") + .hasContextualNameEqualTo("testFlux") .as("subscribeToTerminalObservation") .hasBeenStarted() .hasBeenStopped() .hasLowCardinalityKeyValue("testTag1", "testTagValue1") .hasLowCardinalityKeyValue("testTag2", "testTagValue2") - .hasLowCardinalityKeyValue("type", "Flux") - .hasLowCardinalityKeyValue("status", "completed") + .hasLowCardinalityKeyValue("reactor.type", "Flux") + .hasLowCardinalityKeyValue("reactor.status", "completed") .hasKeyValuesCount(4); } @@ -192,14 +195,15 @@ void tapFromMonoWithTags() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("testMono.observation.flow") + .hasNameEqualTo("testMono") + .hasContextualNameEqualTo("testMono") .as("subscribeToTerminalObservation") .hasBeenStarted() .hasBeenStopped() .hasLowCardinalityKeyValue("testTag1", "testTagValue1") .hasLowCardinalityKeyValue("testTag2", "testTagValue2") - .hasLowCardinalityKeyValue("type", "Mono") - .hasLowCardinalityKeyValue("status", "completed") + .hasLowCardinalityKeyValue("reactor.type", "Mono") + .hasLowCardinalityKeyValue("reactor.status", "completed") .hasKeyValuesCount(4); } @@ -218,11 +222,12 @@ void observationStoppedByCancellation() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("flux.observation.flow") + .hasNameEqualTo("flux") + .hasContextualNameEqualTo("flux") .hasBeenStarted() .hasBeenStopped() .hasLowCardinalityKeyValue("forcedType", "Flux") - .hasLowCardinalityKeyValue("status", "cancelled") + .hasLowCardinalityKeyValue("reactor.status", "cancelled") .hasKeyValuesCount(2) .doesNotHaveError(); } @@ -242,11 +247,12 @@ void observationStoppedByCompleteEmpty() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("emptyFlux.observation.flow") + .hasNameEqualTo("emptyFlux") + .hasContextualNameEqualTo("emptyFlux") .hasBeenStarted() .hasBeenStopped() .hasLowCardinalityKeyValue("forcedType", "Flux") - .hasLowCardinalityKeyValue("status", "completedEmpty") + .hasLowCardinalityKeyValue("reactor.status", "completedEmpty") .hasKeyValuesCount(2) .doesNotHaveError(); } @@ -267,11 +273,12 @@ void observationStoppedByCompleteWithValues() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("flux.observation.flow") + .hasNameEqualTo("flux") + .hasContextualNameEqualTo("flux") .hasBeenStarted() .hasBeenStopped() .hasLowCardinalityKeyValue("forcedType", "Flux") - .hasLowCardinalityKeyValue("status", "completed") + .hasLowCardinalityKeyValue("reactor.status", "completed") .hasKeyValuesCount(2) .doesNotHaveError(); } @@ -294,11 +301,12 @@ void observationMonoStoppedByOnNext() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("valuedMono.observation.flow") + .hasNameEqualTo("valuedMono") + .hasContextualNameEqualTo("valuedMono") .hasBeenStarted() .hasBeenStopped() .hasLowCardinalityKeyValue("forcedType", "Mono") - .hasLowCardinalityKeyValue("status", expectedStatus) + .hasLowCardinalityKeyValue("reactor.status", expectedStatus) .doesNotHaveError(); listener.doOnComplete(); @@ -307,7 +315,7 @@ void observationMonoStoppedByOnNext() { assertThat(registry) .hasSingleObservationThat() .as("post-doOnComplete") - .hasLowCardinalityKeyValue("status", expectedStatus); + .hasLowCardinalityKeyValue("reactor.status", expectedStatus); } @Test @@ -325,11 +333,12 @@ void observationEmptyMonoStoppedByOnComplete() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("emptyMono.observation.flow") + .hasNameEqualTo("emptyMono") + .hasContextualNameEqualTo("emptyMono") .hasBeenStarted() .hasBeenStopped() .hasLowCardinalityKeyValue("forcedType", "Mono") - .hasLowCardinalityKeyValue("status", "completedEmpty") + .hasLowCardinalityKeyValue("reactor.status", "completedEmpty") .doesNotHaveError(); } @@ -349,12 +358,13 @@ void observationStoppedByError() { assertThat(registry) .hasSingleObservationThat() - .hasNameEqualTo("errorFlux.observation.flow") + .hasNameEqualTo("errorFlux") + .hasContextualNameEqualTo("errorFlux") .hasBeenStarted() .hasBeenStopped() - .hasOnlyKeys("forcedType", "status") + .hasOnlyKeys("forcedType", "reactor.status") .hasLowCardinalityKeyValue("forcedType", "Flux") - .hasLowCardinalityKeyValue("status", "error") + .hasLowCardinalityKeyValue("reactor.status", "error") .hasError(exception); } } \ No newline at end of file From 4665943f22f122243fee335599121eb5301611bc Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Jul 2022 10:23:21 +0300 Subject: [PATCH 039/312] Update Micrometer dependency to latest milestone releases (#3116) Update Micrometer Tracing dependency to version 1.0.0-M6 Update Context Propagation dependency to version 1.0.0-M3 Update Micrometer dependency to version 1.10.0-M3 Fixes #3114 --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3d8fc39f5..4cc329362f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.12" jmh = "1.35" junit = "5.8.2" #note that context-propagation has a different version directly set in libraries -micrometer = "1.10.0-SNAPSHOT" +micrometer = "1.10.0-M3" reactiveStreams = "1.0.4" [libraries] @@ -31,9 +31,9 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-M3" micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-M6" micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.6.1" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 12473a43636c13a5316edefca9de34e9809a3955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 12 Jul 2022 14:15:12 +0200 Subject: [PATCH 040/312] [release] Prepare and release 3.5.0-M4 --- README.md | 6 +++--- gradle.properties | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 778f59c024..78b7847941 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.0-M3" - testCompile "io.projectreactor:reactor-test:3.5.0-M3" + compile "io.projectreactor:reactor-core:3.5.0-M4" + testCompile "io.projectreactor:reactor-test:3.5.0-M4" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.5.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.5.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.0-M3" + // implementation "io.projectreactor:reactor-tools:3.5.0-M4" } ``` diff --git a/gradle.properties b/gradle.properties index 09436d6018..be7d066f55 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-SNAPSHOT -bomVersion=2022.0.0-M3 -metricsMicrometerVersion=1.0.0-SNAPSHOT \ No newline at end of file +version=3.5.0-M4 +bomVersion=2022.0.0-M4 +metricsMicrometerVersion=1.0.0-M4 From 5e9b08465f718352913f09f723b41ffae7e3f427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 12 Jul 2022 15:05:05 +0200 Subject: [PATCH 041/312] [release] Next development version 3.5.0-SNAPSHOT for M5 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index be7d066f55..6ef2f59482 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-M4 +version=3.5.0-SNAPSHOT bomVersion=2022.0.0-M4 -metricsMicrometerVersion=1.0.0-M4 +metricsMicrometerVersion=1.0.0-SNAPSHOT From f47fc563daba2a54d47a94ff42e30d81d5ffe59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 22 Jul 2022 16:34:20 +0200 Subject: [PATCH 042/312] Micrometer#observation(): avoids scopes, set parentObservation (#3119) This commit removes context-propagation dependency entirely from the reactor-core-micrometer module. It also changes the MicrometerObservationListener so that it doesn't use any Scope at all. To deal with hierarchical Observations, we instead do the following: - read Observation from Context - don't open a scope, but rather start new Observation setting old one as parent explicitly - store the new Observation in the ContextView --- gradle/libs.versions.toml | 10 +- reactor-core-micrometer/build.gradle | 6 +- .../MicrometerObservationListener.java | 92 ++++++----- .../MicrometerObservationListenerTest.java | 154 +++++++++++++++++- .../micrometer/MicrometerTest.java | 7 + 5 files changed, 224 insertions(+), 45 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb69f8177f..10e939c55d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.12" jmh = "1.35" junit = "5.8.2" #note that context-propagation has a different version directly set in libraries -micrometer = "1.10.0-M3" +micrometer = "1.10.0-SNAPSHOT" # was -M3 reactiveStreams = "1.0.4" [libraries] @@ -26,14 +26,18 @@ jmh-annotations-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jsr166backport = "io.projectreactor:jsr166:1.0.0.RELEASE" jsr305 = "com.google.code.findbugs:jsr305:3.0.1" +junit-api = { module = "org.junit.jupiter:junit-jupiter-api" } junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } +junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } +junit-params = { module = "org.junit.jupiter:junit-jupiter-params" } +junit-platform = { module = "org.junit.platform:junit-platform-launcher" } logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-M3" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" # was -M3 micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-M6" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" # was -M6 micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.6.1" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } diff --git a/reactor-core-micrometer/build.gradle b/reactor-core-micrometer/build.gradle index f815e7e03c..a53bcca324 100644 --- a/reactor-core-micrometer/build.gradle +++ b/reactor-core-micrometer/build.gradle @@ -46,7 +46,6 @@ dependencies { implementation platform(libs.micrometer.bom) api libs.micrometer.core - implementation libs.micrometer.contextPropagation testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" @@ -58,7 +57,10 @@ dependencies { testImplementation libs.micrometer.core testImplementation libs.micrometer.test testImplementation libs.micrometer.observation.test - testImplementation libs.micrometer.tracing.test + testImplementation libs.micrometer.contextPropagation + testImplementation(libs.micrometer.tracing.test) { //brings in context-propagation + exclude group: "io.micrometer", module: "context-propagation" + } testImplementation(project(":reactor-test")) { exclude module: 'reactor-core' diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java index 1e7b050282..387ba8e031 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java @@ -16,7 +16,6 @@ package reactor.core.observability.micrometer; -import io.micrometer.context.ContextSnapshot; import io.micrometer.observation.Observation; import reactor.core.observability.SignalListener; @@ -47,6 +46,15 @@ final class MicrometerObservationListener implements SignalListener { static final String STATUS_COMPLETED_EMPTY = MicrometerMeterListener.TAG_STATUS_COMPLETED_EMPTY; static final String STATUS_ERROR = MicrometerMeterListener.TAG_STATUS_ERROR; + /** + * The key to use to store {@link Observation} in context (same as the one from {@code ObservationThreadLocalAccessor}). + * + * @implNote this might be redundant, but we got {@code com.sun.tools.javac.code.Symbol$CompletionFailure: class file for io.micrometer.context.ThreadLocalAccessor not found} + * in reactor-netty while compiling a similar arrangement. A unit test in MicrometerTest acts as a smoke test in case + * micrometer-observation's {@code ObservationThreadLocalAccessor.KEY} changes to something else. + */ + static final String CONTEXT_KEY_OBSERVATION = "micrometer.observation"; + /** * A value for the status tag, to be used when a Mono completes from onNext. * In production, this is set to {@link #STATUS_COMPLETED}. @@ -54,13 +62,11 @@ final class MicrometerObservationListener implements SignalListener { */ final String completedOnNextStatus; final MicrometerObservationListenerConfiguration configuration; - final ContextView originalContext; - final Observation subscribeToTerminalObservation; + final ContextView originalContext; + final Observation tapObservation; @Nullable - Context contextWithScope; - @Nullable - Observation.Scope scope = null; + Context contextWithObservation; boolean valued; @@ -78,7 +84,7 @@ final class MicrometerObservationListener implements SignalListener { //creation of the listener matches subscription (Publisher.subscribe(Subscriber) / doFirst) //while doOnSubscription matches the moment where the Publisher acknowledges said subscription - subscribeToTerminalObservation = Observation.createNotStarted( + tapObservation = Observation.createNotStarted( configuration.sequenceName, configuration.registry ) @@ -88,17 +94,41 @@ final class MicrometerObservationListener implements SignalListener { @Override public void doFirst() { - ContextSnapshot contextSnapshot = ContextSnapshot.capture(this.originalContext); - - try (ContextSnapshot.Scope ignored = contextSnapshot.setThreadLocalValues()) { - this.scope = this.subscribeToTerminalObservation - .start() - .openScope(); - //reacquire the scope from ThreadLocal - //tap context hasn't been initialized yet, so addToContext can now use the Scope - ContextSnapshot contextSnapshot2 = ContextSnapshot.capture(this.originalContext); - this.contextWithScope = contextSnapshot2.updateContext(Context.of(this.originalContext)); + /* Implementation note on using parentObservation vs openScope: + Opening a Scope is never necessary in this tap listener, because the Observation we create is stored in + the Context the tap operator will expose to upstream, rather than via ThreadLocal population. + + We also make a best-effort attempt to discover such an Observation in the context here in doFirst, so that this + can explicitly be used as the parentObservation. At this point, if none is found we take also the opportunity + of checking if the registry has a currentObservation. + + As a consequence, fanout (eg. with a `flatMap`) upstream of the tap should be able to see the current Observation + in the context and the inner publishers should inherit it as their parent observation if they also use `tap(Micrometer.observation())`. + + Note that Reactor's threading model doesn't generally guarantee that doFirst and doOnNext/doOnComplete/doOnError run + in the same thread, and that's the main reason why Scopes are avoided here (as their sole purpose is to set up + Thread Local variables). + */ + + Observation o; + Observation p; + if (this.originalContext.hasKey(CONTEXT_KEY_OBSERVATION)) { + p = this.originalContext.get(CONTEXT_KEY_OBSERVATION); + } + else { + p = this.configuration.registry.getCurrentObservation(); + } + + if (p != null) { + o = this.tapObservation + .parentObservation(p) + .start(); + } + else { + o = this.tapObservation.start(); } + this.contextWithObservation = Context.of(this.originalContext) + .put(CONTEXT_KEY_OBSERVATION, o); } @Override @@ -106,29 +136,26 @@ public Context addToContext(Context originalContext) { if (this.originalContext != originalContext) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("addToContext call on Observation {} with unexpected originalContext {}", - this.subscribeToTerminalObservation, originalContext); + this.tapObservation, originalContext); } return originalContext; } - if (this.contextWithScope == null) { + if (this.contextWithObservation == null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("addToContext call on Observation {} before contextWithScope is set", - this.subscribeToTerminalObservation); + this.tapObservation); } return originalContext; } - return contextWithScope; + return contextWithObservation; } @Override public void doOnCancel() { - Observation observation = subscribeToTerminalObservation + Observation observation = tapObservation .lowCardinalityKeyValue(KEY_STATUS, STATUS_CANCELLED); observation.stop(); - if (scope != null) { - scope.close(); - } } @Override @@ -144,26 +171,20 @@ else if (!configuration.isMono) { // if status == null, recording with OnComplete tag is done directly in onNext for the Mono(valued) case if (status != null) { - Observation completeObservation = subscribeToTerminalObservation + Observation completeObservation = tapObservation .lowCardinalityKeyValue(KEY_STATUS, status); completeObservation.stop(); - if (scope != null) { - scope.close(); - } } } @Override public void doOnError(Throwable e) { - Observation errorObservation = subscribeToTerminalObservation + Observation errorObservation = tapObservation .lowCardinalityKeyValue(KEY_STATUS, STATUS_ERROR) .error(e); errorObservation.stop(); - if (scope != null) { - scope.close(); - } } @Override @@ -171,13 +192,10 @@ public void doOnNext(T t) { valued = true; if (configuration.isMono) { //record valued completion directly - Observation completeObservation = subscribeToTerminalObservation + Observation completeObservation = tapObservation .lowCardinalityKeyValue(KEY_STATUS, completedOnNextStatus); completeObservation.stop(); - if (scope != null) { - scope.close(); - } } } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java index 69f57c6abb..ade08c1d31 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java @@ -17,9 +17,12 @@ package reactor.core.observability.micrometer; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Clock; +import io.micrometer.observation.Observation; +import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import io.micrometer.observation.tck.TestObservationRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -78,7 +81,7 @@ void whenStartedFluxWithDefaultName() { MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); assertThat(listener.valued).as("valued").isFalse(); - assertThat(listener.subscribeToTerminalObservation) + assertThat(listener.tapObservation) .as("subscribeToTerminalObservation field") .isNotNull(); assertThat(registry).as("before start").doesNotHaveAnyObservation(); @@ -107,7 +110,7 @@ void whenStartedFluxWithCustomName() { MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); assertThat(listener.valued).as("valued").isFalse(); - assertThat(listener.subscribeToTerminalObservation) + assertThat(listener.tapObservation) .as("subscribeToTerminalObservation field") .isNotNull(); assertThat(registry).as("no observation started").doesNotHaveAnyObservation(); @@ -137,7 +140,7 @@ void whenStartedMono() { MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration); assertThat(listener.valued).as("valued").isFalse(); - assertThat(listener.subscribeToTerminalObservation) + assertThat(listener.tapObservation) .as("subscribeToTerminalObservation field") .isNotNull(); assertThat(registry).as("no observation started").doesNotHaveAnyObservation(); @@ -367,4 +370,149 @@ void observationStoppedByError() { .hasLowCardinalityKeyValue("reactor.status", "error") .hasError(exception); } + + @Test + void observationGetsParentFromContext() { + configuration = new MicrometerObservationListenerConfiguration( + MicrometerObservationListener.ANONYMOUS_OBSERVATION, + //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) + KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + false); + + Observation parent = Observation.start("testParent", registry); + Context contextWithAParent = Context.of(subscriberContext).put(ObservationThreadLocalAccessor.KEY, parent); + MicrometerObservationListener listener = new MicrometerObservationListener<>(contextWithAParent, configuration); + + //we don't open scopes, but we did however start an observation + assertThat(registry) + .satisfies(r -> assertThat(r.getCurrentObservationScope()).as("current scope").isNull()) + .doesNotHaveAnyRemainingCurrentObservation(); + + listener.doFirst(); // forces observation start and discovery of the parent from context + listener.doOnComplete(); // forces observation stop + + assertThat(listener.originalContext).as("originalContext").isSameAs(contextWithAParent); + assertThat(listener.contextWithObservation).as("contextWithObservation") + .matches(c -> c.hasKey(ObservationThreadLocalAccessor.KEY), "has OBSERVATION_CONTEXT_KEY"); + + assertThat(registry) + //-- + .hasObservationWithNameEqualTo("testParent") + .that() + .isSameAs(parent.getContext()) + .hasBeenStarted() + .isNotStopped() + .hasNoKeyValues() + .backToTestObservationRegistry() + //-- + .hasObservationWithNameEqualTo("reactor.observation") + .that() + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2") + .hasLowCardinalityKeyValue("reactor.status", "completedEmpty") + .hasParentObservationEqualTo(parent) + .backToTestObservationRegistry() + //-- + .doesNotHaveAnyRemainingCurrentObservation(); + + parent.stop(); + assertThat(registry) + .doesNotHaveAnyRemainingCurrentObservation() + .satisfies(r -> assertThat(r.getCurrentObservationScope()).as("no leftover currentObservationScope()").isNull()); + } + + @Test + void observationWithEmptyContextHasNoParent() { + configuration = new MicrometerObservationListenerConfiguration( + MicrometerObservationListener.ANONYMOUS_OBSERVATION, + //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) + KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + false); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(Context.empty(), configuration); + + //we don't open scopes so it's never stored + assertThat(registry) + .satisfies(r -> assertThat(r.getCurrentObservationScope()).as("no current scope").isNull()) + .doesNotHaveAnyRemainingCurrentObservation(); + + listener.doFirst(); // forces observation start and discovery of the parent from context + listener.doOnComplete(); // forces observation stop + + assertThat(listener.contextWithObservation).as("contextWithObservation") + .matches(c -> c.hasKey(ObservationThreadLocalAccessor.KEY), "has OBSERVATION_CONTEXT_KEY"); + + assertThat(registry) + //-- + .hasSingleObservationThat() + .hasNameEqualTo("reactor.observation") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2") + .hasLowCardinalityKeyValue("reactor.status", "completedEmpty") + .doesNotHaveParentObservation() + .backToTestObservationRegistry() + //-- + .doesNotHaveAnyRemainingCurrentObservation(); + + assertThat(registry) + .doesNotHaveAnyRemainingCurrentObservation() + .satisfies(r -> assertThat(r.getCurrentObservationScope()).as("no leftover currentObservationScope()").isNull()); + } + + @Test + void observationWithEmptyContextHasParentWhenExternalScopeOpened() { + configuration = new MicrometerObservationListenerConfiguration( + MicrometerObservationListener.ANONYMOUS_OBSERVATION, + //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) + KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + registry, + false); + + MicrometerObservationListener listener = new MicrometerObservationListener<>(Context.empty(), configuration); + + Observation parentFromThreadLocal = Observation.start("testParent", registry); + Observation.Scope parentScope = parentFromThreadLocal.openScope(); + + //operator doesn't open scopes but we have + assertThat(registry) + .satisfies(r -> assertThat(r.getCurrentObservationScope()).as("has parent scope").isEqualTo(parentScope)) + .hasRemainingCurrentObservationSameAs(parentFromThreadLocal); + + listener.doFirst(); // forces observation start and discovery of the parent from context + listener.doOnComplete(); // forces observation stop + + assertThat(listener.contextWithObservation).as("contextWithObservation") + .matches(c -> c.hasKey(ObservationThreadLocalAccessor.KEY), "has OBSERVATION_CONTEXT_KEY"); + + assertThat(registry) + //-- + .hasObservationWithNameEqualTo("testParent") + .that() + .isSameAs(parentFromThreadLocal.getContext()) + .hasBeenStarted() + .isNotStopped() + .hasNoKeyValues() + .backToTestObservationRegistry() + //-- + .hasObservationWithNameEqualTo("reactor.observation") + .that() + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2") + .hasLowCardinalityKeyValue("reactor.status", "completedEmpty") + .hasParentObservationEqualTo(parentFromThreadLocal); + + parentFromThreadLocal.stop(); + parentScope.close(); + assertThat(registry) + .doesNotHaveAnyRemainingCurrentObservation() + .satisfies(r -> assertThat(r.getCurrentObservationScope()).as("no leftover currentObservationScope()").isNull()); + } } \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java index 595ee6b5ca..9b68580b37 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java @@ -20,6 +20,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -92,4 +93,10 @@ public long monotonicTime() { assertThat(factory.useClock()).as("clock").isSameAs(customLocalClock).isNotSameAs(Clock.SYSTEM); assertThat(factory.useRegistry()).as("registry").isSameAs(customLocalRegistry).isNotSameAs(customCommonRegistry); } + + @Test + void observationContextKeySmokeTest() { + assertThat(MicrometerObservationListener.CONTEXT_KEY_OBSERVATION) + .isEqualTo(ObservationThreadLocalAccessor.KEY); + } } \ No newline at end of file From d2c5a4d4ea63f922685e57ea8e17a31482254542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 25 Jul 2022 10:09:52 +0200 Subject: [PATCH 043/312] Adapt to ContextAccessor change: new readValue method (#3124) --- .../util/context/ReactorContextAccessor.java | 8 ++++++++ .../context/ReactorContextAccessorTest.java | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java index 370e3b0e09..29d86e6a93 100644 --- a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java +++ b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java @@ -21,6 +21,8 @@ import io.micrometer.context.ContextAccessor; +import reactor.util.annotation.Nullable; + /** * A {@code ContextAccessor} to enable reading values from a Reactor * {@link ContextView} and writing values to {@link Context}. @@ -48,6 +50,12 @@ public void readValues(ContextView source, Predicate keyPredicate, Map T readValue(ContextView sourceContext, Object key) { + return sourceContext.getOrDefault(key, null); + } + @Override public boolean canWriteTo(Class contextType) { return Context.class.isAssignableFrom(contextType); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java b/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java index d7f23492ad..eafbdfe6fc 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java @@ -104,4 +104,23 @@ void writeValuesWithPutAllMap() { Mockito.verify(target, never()).putAll((ContextView) any()); Mockito.verify(target, times(1)).putAllMap(anyMap()); } + + @Test + void readValueWhenKeyPresent() { + ReactorContextAccessor test = new ReactorContextAccessor(); + String expectedValue = "A"; + ContextView source = Context.of(1, expectedValue, 2, "B"); + + String readValue = test.readValue(source, 1); + assertThat(readValue).isSameAs(expectedValue); + } + + @Test + void readValueReturnsNullWhenKeyAbsent() { + ReactorContextAccessor test = new ReactorContextAccessor(); + ContextView source = Context.of(1, "A"); + + String readValue = test.readValue(source, 2); + assertThat(readValue).isNull(); + } } From 687fdf83eed22bfd4e4321fca89c69154c6cba93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 27 Jul 2022 12:14:56 +0200 Subject: [PATCH 044/312] Remove Micrometer#useRegistry, remove notion of Clock (#3128) This commit reworks the `reactor-core-micrometer` `Micrometer` API to simplify things a bit and make the use of MeterRegistry more explicit: - remove `useRegistry` and the notion of a default global registry - remove `metrics()` and `metrics(MeterRegistry, Clock)` - replace the above by `metrics(MeterRegistry)` The `Clock` parameter was a bit redundant, as it can always be provided through the `MeterRegistry#config()`. For example: ``` new SimpleMeterRegistry(SimpleConfig.DEFAULT, myCustomClock); ``` This is a breaking change compared to previous milestones only. --- .../observability/micrometer/Micrometer.java | 66 ++----------------- .../micrometer/MicrometerMeterListener.java | 6 +- .../MicrometerMeterListenerConfiguration.java | 13 ++-- .../MicrometerMeterListenerFactory.java | 14 ++-- ...rometerMeterListenerConfigurationTest.java | 8 +-- .../MicrometerMeterListenerFactoryTest.java | 40 +---------- .../MicrometerMeterListenerTest.java | 15 ++--- .../micrometer/MicrometerTest.java | 62 +---------------- 8 files changed, 34 insertions(+), 190 deletions(-) diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index 6327f29fc7..215a510778 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -35,63 +35,16 @@ public final class Micrometer { private static final String SCHEDULERS_DECORATOR_KEY = "reactor.core.observability.micrometer.schedulerDecorator"; - private static MeterRegistry registry = Metrics.globalRegistry; /** * The default "name" to use as a prefix for meter if the instrumented sequence doesn't define a {@link reactor.core.publisher.Flux#name(String) name}. */ public static final String DEFAULT_METER_PREFIX = "reactor"; - /** - * Set the registry to use in reactor-core-micrometer for metrics related purposes. - * @return the previously configured registry. - * @deprecated in M4, will be removed in M5 / RC1. prefer your own singleton and explicitly - * passing the registry to {@link #metrics(MeterRegistry, Clock)} - */ - @Deprecated - public static MeterRegistry useRegistry(MeterRegistry newRegistry) { - MeterRegistry previous = registry; - registry = newRegistry; - return previous; - } - - /** - * Get the registry used in reactor-core-micrometer for metrics related purposes. - * - * @deprecated in M4, will be removed in M5 / RC1. prefer your own singleton and explicitly - * passing the registry to {@link #metrics(MeterRegistry, Clock)} - */ - @Deprecated - public static MeterRegistry getRegistry() { - return registry; - } - - /** - * A {@link SignalListener} factory that will ultimately produce Micrometer metrics - * to the configured default {@link #getRegistry() registry}. - * To be used with either the {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} or - * {@link reactor.core.publisher.Mono#tap(SignalListenerFactory)} operator. - *

    - * When used in a {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} operator, meter names use - * the {@link reactor.core.publisher.Flux#name(String)} set upstream of the tap as id prefix if applicable - * or default to {@link #DEFAULT_METER_PREFIX}. Similarly, upstream tags are gathered and added - * to the default set of tags for meters. - *

    - * Note that some monitoring systems like Prometheus require to have the exact same set of - * tags for each meter bearing the same name. - * - * @param the type of onNext in the target publisher - * @return a {@link SignalListenerFactory} to record metrics - * @deprecated in M4, will be removed in M5 / RC1. prefer explicitly passing a registry via {@link #metrics(MeterRegistry, Clock)} - */ - @Deprecated - public static SignalListenerFactory metrics() { - return new MicrometerMeterListenerFactory<>(); - } - /** * A {@link SignalListener} factory that will ultimately produce Micrometer metrics - * to the provided {@link MeterRegistry} using the provided {@link Clock} for timings. + * to the provided {@link MeterRegistry} (and using the registry's {@link MeterRegistry.Config#clock() configured} + * {@link Clock} in case additional timings are needed). * To be used with either the {@link reactor.core.publisher.Flux#tap(SignalListenerFactory)} or * {@link reactor.core.publisher.Mono#tap(SignalListenerFactory)} operator. *

    @@ -104,20 +57,11 @@ public static MeterRegistry getRegistry() { * tags for each meter bearing the same name. * * @param the type of onNext in the target publisher + * @param meterRegistry the {@link MeterRegistry} in which to register and publish metrics * @return a {@link SignalListenerFactory} to record metrics */ - public static SignalListenerFactory metrics(MeterRegistry registry, Clock clock) { - return new MicrometerMeterListenerFactory() { - @Override - protected Clock useClock() { - return clock; - } - - @Override - protected MeterRegistry useRegistry() { - return registry; - } - }; + public static SignalListenerFactory metrics(MeterRegistry meterRegistry) { + return new MicrometerMeterListenerFactory(meterRegistry); } /** diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java index 68ad175c2b..4fd0c0204f 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java @@ -126,7 +126,7 @@ public void doOnNext(T t) { } //record the delay since previous onNext/onSubscribe. This also records the count. long last = this.lastNextEventNanos; - this.lastNextEventNanos = configuration.clock.monotonicTime(); + this.lastNextEventNanos = configuration.registry.config().clock().monotonicTime(); this.onNextIntervalTimer.record(lastNextEventNanos - last, TimeUnit.NANOSECONDS); } @@ -138,8 +138,8 @@ public void doOnMalformedOnNext(T value) { @Override public void doOnSubscription() { recordOnSubscribe(configuration.sequenceName, configuration.commonTags, configuration.registry); - this.subscribeToTerminateSample = Timer.start(configuration.clock); - this.lastNextEventNanos = configuration.clock.monotonicTime(); + this.subscribeToTerminateSample = Timer.start(configuration.registry); + this.lastNextEventNanos = configuration.registry.config().clock().monotonicTime(); } @Override diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java index bf7992508d..6699700306 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfiguration.java @@ -41,20 +41,20 @@ final class MicrometerMeterListenerConfiguration { private static final Logger LOGGER = Loggers.getLogger(MicrometerMeterListenerConfiguration.class); - static MicrometerMeterListenerConfiguration fromFlux(Flux source, MeterRegistry meterRegistry, Clock clock) { + static MicrometerMeterListenerConfiguration fromFlux(Flux source, MeterRegistry meterRegistry) { Tags defaultTags = MicrometerMeterListener.DEFAULT_TAGS_FLUX; final String name = resolveName(source, LOGGER, Micrometer.DEFAULT_METER_PREFIX); final Tags tags = resolveTags(source, defaultTags); - return new MicrometerMeterListenerConfiguration(name, tags, meterRegistry, clock, false); + return new MicrometerMeterListenerConfiguration(name, tags, meterRegistry, false); } - static MicrometerMeterListenerConfiguration fromMono(Mono source, MeterRegistry meterRegistry, Clock clock) { + static MicrometerMeterListenerConfiguration fromMono(Mono source, MeterRegistry meterRegistry) { Tags defaultTags = MicrometerMeterListener.DEFAULT_TAGS_MONO; final String name = resolveName(source, LOGGER, Micrometer.DEFAULT_METER_PREFIX); final Tags tags = resolveTags(source, defaultTags); - return new MicrometerMeterListenerConfiguration(name, tags, meterRegistry, clock, true); + return new MicrometerMeterListenerConfiguration(name, tags, meterRegistry, true); } /** @@ -103,7 +103,6 @@ static Tags resolveTags(Publisher source, Tags tags) { return tags; } - final Clock clock; final Tags commonTags; final boolean isMono; final String sequenceName; @@ -112,9 +111,7 @@ static Tags resolveTags(Publisher source, Tags tags) { // separator is the dot, not camelCase... final MeterRegistry registry; - MicrometerMeterListenerConfiguration(String sequenceName, Tags tags, MeterRegistry registryCandidate, Clock clock, - boolean isMono) { - this.clock = clock; + MicrometerMeterListenerConfiguration(String sequenceName, Tags tags, MeterRegistry registryCandidate, boolean isMono) { this.commonTags = tags; this.isMono = isMono; this.sequenceName = sequenceName; diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java index 028549a506..8148067b2e 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactory.java @@ -17,6 +17,7 @@ package reactor.core.observability.micrometer; import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; import org.reactivestreams.Publisher; @@ -33,22 +34,19 @@ */ class MicrometerMeterListenerFactory implements SignalListenerFactory { - protected Clock useClock() { - return Clock.SYSTEM; - } + final MeterRegistry registry; - @SuppressWarnings("deprecation") - protected MeterRegistry useRegistry() { - return Micrometer.getRegistry(); + MicrometerMeterListenerFactory(MeterRegistry registry) { + this.registry = registry; } @Override public MicrometerMeterListenerConfiguration initializePublisherState(Publisher source) { if (source instanceof Mono) { - return MicrometerMeterListenerConfiguration.fromMono((Mono) source, useRegistry(), useClock()); + return MicrometerMeterListenerConfiguration.fromMono((Mono) source, this.registry); } else if (source instanceof Flux) { - return MicrometerMeterListenerConfiguration.fromFlux((Flux) source, useRegistry(), useClock()); + return MicrometerMeterListenerConfiguration.fromFlux((Flux) source, this.registry); } else { throw new IllegalArgumentException("MicrometerMeterListenerFactory must only be used via the tap operator / with a Flux or Mono"); diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java index 153e41e167..dc32c79c6a 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerConfigurationTest.java @@ -58,9 +58,9 @@ void fromFlux(@Nullable String name, @Nullable String tag) { flux = flux.tag("tag", tag); } - MicrometerMeterListenerConfiguration configuration = MicrometerMeterListenerConfiguration.fromFlux(flux, expectedRegistry, expectedClock); + MicrometerMeterListenerConfiguration configuration = MicrometerMeterListenerConfiguration.fromFlux(flux, expectedRegistry); - assertThat(configuration.clock).as("clock").isSameAs(expectedClock); + assertThat(configuration.registry.config().clock()).as("clock").isSameAs(expectedClock); assertThat(configuration.registry).as("registry").isSameAs(expectedRegistry); assertThat(configuration.isMono).as("isMono").isFalse(); @@ -100,9 +100,9 @@ void fromMono(@Nullable String name, @Nullable String tag) { mono = mono.tag("tag", tag); } - MicrometerMeterListenerConfiguration configuration = MicrometerMeterListenerConfiguration.fromMono(mono, expectedRegistry, expectedClock); + MicrometerMeterListenerConfiguration configuration = MicrometerMeterListenerConfiguration.fromMono(mono, expectedRegistry); - assertThat(configuration.clock).as("clock").isSameAs(expectedClock); + assertThat(configuration.registry.config().clock()).as("clock").isSameAs(expectedClock); assertThat(configuration.registry).as("registry").isSameAs(expectedRegistry); assertThat(configuration.isMono).as("isMono").isTrue(); diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactoryTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactoryTest.java index e3a10926d5..52bf2b53ca 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactoryTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerFactoryTest.java @@ -17,7 +17,7 @@ package reactor.core.observability.micrometer; import io.micrometer.core.instrument.Clock; -import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; @@ -36,34 +36,11 @@ */ class MicrometerMeterListenerFactoryTest { - @Test - void useClockDefaultsToSystemClock() { - MicrometerMeterListenerFactory factory = new MicrometerMeterListenerFactory<>(); - - assertThat(factory.useClock()).isSameAs(Clock.SYSTEM); - } - - @Test - void useRegistryDefaultsToCommonRegistry() { - SimpleMeterRegistry commonRegistry = new SimpleMeterRegistry(); - MeterRegistry defaultCommon = Micrometer.useRegistry(commonRegistry); - try { - MicrometerMeterListenerFactory factory = new MicrometerMeterListenerFactory<>(); - - assertThat(factory.useRegistry()).isSameAs(Micrometer.getRegistry()) - .isSameAs(commonRegistry); - } - finally { - Micrometer.useRegistry(defaultCommon); - } - } - @Test void configurationFromMono() { MicrometerMeterListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Mono.just(1)); assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); - assertThat(configuration.clock).as("clock").isSameAs(CUSTOM_CLOCK); assertThat(configuration.isMono).as("isMono").isTrue(); assertThat(configuration.commonTags).map(Object::toString).containsExactly("tag(type=Mono)"); } @@ -73,7 +50,6 @@ void configurationFromFlux() { MicrometerMeterListenerConfiguration configuration = CUSTOM_FACTORY.initializePublisherState(Flux.just(1, 2)); assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); - assertThat(configuration.clock).as("clock").isSameAs(CUSTOM_CLOCK); assertThat(configuration.isMono).as("isMono").isFalse(); assertThat(configuration.commonTags).map(Object::toString).containsExactly("tag(type=Flux)"); } @@ -106,17 +82,7 @@ public long monotonicTime() { return 0; } }; - protected static final SimpleMeterRegistry CUSTOM_REGISTRY = new SimpleMeterRegistry(); + protected static final SimpleMeterRegistry CUSTOM_REGISTRY = new SimpleMeterRegistry(SimpleConfig.DEFAULT, CUSTOM_CLOCK); protected static final MicrometerMeterListenerFactory - CUSTOM_FACTORY = new MicrometerMeterListenerFactory() { - @Override - protected Clock useClock() { - return CUSTOM_CLOCK; - } - - @Override - protected MeterRegistry useRegistry() { - return CUSTOM_REGISTRY; - } - }; + CUSTOM_FACTORY = new MicrometerMeterListenerFactory(CUSTOM_REGISTRY); } \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java index 3410668065..47ee415827 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java @@ -23,6 +23,7 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,7 +43,6 @@ class MicrometerMeterListenerTest { @BeforeEach void initRegistry() { - registry = new SimpleMeterRegistry(); virtualClockTime = new AtomicLong(); virtualClock = new Clock() { @Override @@ -55,11 +55,11 @@ public long monotonicTime() { return virtualClockTime.get(); } }; + registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, virtualClock); configuration = new MicrometerMeterListenerConfiguration( "testName", Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, - virtualClock, false); } @@ -69,7 +69,6 @@ void initialStateFluxWithDefaultName() { Micrometer.DEFAULT_METER_PREFIX, Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, - virtualClock, false); MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); @@ -95,7 +94,6 @@ void initialStateFluxWithCustomName() { "testName", Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, - virtualClock, false); MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); @@ -126,7 +124,6 @@ void initialStateMono() { Micrometer.DEFAULT_METER_PREFIX, Tags.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, - virtualClock, true); MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); @@ -284,7 +281,7 @@ void doOnNextRecordsInterval() { @Test void doOnNextRecordsInterval_defaultName() { configuration = new MicrometerMeterListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), - registry, virtualClock, false); + registry, false); MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); @@ -309,7 +306,7 @@ void doOnNextRecordsInterval_defaultName() { @Test void doOnNext_monoRecordsCompletionOnly() { configuration = new MicrometerMeterListenerConfiguration("testName", Tags.empty(), - registry, virtualClock, true); + registry, true); MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); listener.doOnSubscription(); @@ -369,7 +366,7 @@ void doOnRequestRecordsTotalDemand() { @Test void doOnRequestMonoIgnoresRequest() { - configuration = new MicrometerMeterListenerConfiguration("testName", Tags.empty(), registry, virtualClock, true); + configuration = new MicrometerMeterListenerConfiguration("testName", Tags.empty(), registry, true); MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); assertThatCode(() -> listener.doOnRequest(100L)).doesNotThrowAnyException(); assertThat(listener.requestedCounter).isNull(); @@ -377,7 +374,7 @@ void doOnRequestMonoIgnoresRequest() { @Test void doOnRequestDefaultNameIgnoresRequest() { - configuration = new MicrometerMeterListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), registry, virtualClock, false); + configuration = new MicrometerMeterListenerConfiguration(Micrometer.DEFAULT_METER_PREFIX, Tags.empty(), registry, false); MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); assertThatCode(() -> listener.doOnRequest(100L)).doesNotThrowAnyException(); assertThat(listener.requestedCounter).isNull(); diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java index 9b68580b37..f0099ea1f2 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java @@ -16,13 +16,9 @@ package reactor.core.observability.micrometer; -import io.micrometer.core.instrument.Clock; -import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -32,66 +28,12 @@ */ class MicrometerTest { - private MeterRegistry defaultRegistry; - - @BeforeEach - void init() { - defaultRegistry = Micrometer.getRegistry(); - } - - @AfterEach - void restore() { - Micrometer.useRegistry(defaultRegistry); - } - - @Test - void defaultRegistryCanBeChanged() { - MeterRegistry registry = Micrometer.getRegistry(); - try { - assertThat(registry).as("default common registry").isEqualTo(Metrics.globalRegistry); - - MeterRegistry replacement = new SimpleMeterRegistry(); - MeterRegistry old = Micrometer.useRegistry(replacement); - - assertThat(old).as("useRegistry return value").isSameAs(registry); - assertThat(Micrometer.getRegistry()).as("getRegistry post useRegistry").isSameAs(replacement); - } - finally { - Micrometer.useRegistry(registry); - } - } - - @Test - void metricsUsesCommonRegistry() { - SimpleMeterRegistry customCommonRegistry = new SimpleMeterRegistry(); - Micrometer.useRegistry(customCommonRegistry); - MicrometerMeterListenerFactory factory = (MicrometerMeterListenerFactory) Micrometer.metrics(); - - assertThat(factory.useClock()).as("clock").isSameAs(Clock.SYSTEM); - assertThat(factory.useRegistry()).as("registry").isSameAs(customCommonRegistry); - } - @Test void metricsUsesSpecifiedClockAndRegistry() { - SimpleMeterRegistry customCommonRegistry = new SimpleMeterRegistry(); - Micrometer.useRegistry(customCommonRegistry); SimpleMeterRegistry customLocalRegistry = new SimpleMeterRegistry(); - Clock customLocalClock = new Clock() { - @Override - public long wallTime() { - return 0; - } - - @Override - public long monotonicTime() { - return 0; - } - }; - - MicrometerMeterListenerFactory factory = (MicrometerMeterListenerFactory) Micrometer.metrics(customLocalRegistry, customLocalClock); + MicrometerMeterListenerFactory factory = (MicrometerMeterListenerFactory) Micrometer.metrics(customLocalRegistry); - assertThat(factory.useClock()).as("clock").isSameAs(customLocalClock).isNotSameAs(Clock.SYSTEM); - assertThat(factory.useRegistry()).as("registry").isSameAs(customLocalRegistry).isNotSameAs(customCommonRegistry); + assertThat(factory.registry).as("registry").isSameAs(customLocalRegistry).isNotSameAs(Metrics.globalRegistry); } @Test From 2bedc5f6ae02724a8e4a866f3be3996c93de4f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 28 Jul 2022 10:57:20 +0200 Subject: [PATCH 045/312] Fix a couple reactor-core-micrometer module compilation errors (#3132) The Micrometer#useRegistry has been removed but the scheduler hook was still referencing it instead of the (deprecated) one in core. This fixes the situation by using core's Metrics.MicrometerConfiguration getRegistry. Additionally, some tests compare to KeyValue#toString, which has recently been polished to have `keyValue` prefix rather than `tag`. --- .../observability/micrometer/Micrometer.java | 5 ++-- ...rObservationListenerConfigurationTest.java | 24 +++++++++---------- ...rometerObservationListenerFactoryTest.java | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index 215a510778..189c7d68bd 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -100,15 +100,16 @@ public final class Micrometer { * Set-up a decorator that will instrument any {@link ExecutorService} that backs a reactor-core {@link Scheduler} * (or scheduler implementations which use {@link Schedulers#decorateExecutorService(Scheduler, ScheduledExecutorService)}). *

    - * The {@link MeterRegistry} to use can be configured via {@link #useRegistry(MeterRegistry)} + * The {@link MeterRegistry} to use can be configured via {@link reactor.util.Metrics.MicrometerConfiguration#useRegistry(MeterRegistry)} * prior to using this method, the default being {@link io.micrometer.core.instrument.Metrics#globalRegistry}. * * @implNote Note that this is added as a decorator via Schedulers when enabling metrics for schedulers, * which doesn't change the Factory. */ + @Deprecated public static void enableSchedulersMetricsDecorator() { Schedulers.addExecutorServiceDecorator(SCHEDULERS_DECORATOR_KEY, - new MicrometerSchedulerMetricsDecorator(getRegistry())); + new MicrometerSchedulerMetricsDecorator(reactor.util.Metrics.MicrometerConfiguration.getRegistry())); } /** diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java index 825686cd1d..0530418c59 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java @@ -125,7 +125,7 @@ void resolveKeyValues_notSet() { KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); assertThat(resolvedKeyValues.stream().map(Object::toString)) - .containsExactly("tag(common1=commonValue1)"); + .containsExactly("keyValue(common1=commonValue1)"); } @Test @@ -138,8 +138,8 @@ void resolveKeyValues_setRightAbove() { KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactlyInAnyOrder( - "tag(common1=commonValue1)", - "tag(k1=v1)" + "keyValue(common1=commonValue1)", + "keyValue(k1=v1)" ); } @@ -155,8 +155,8 @@ void resolveKeyValues_setHigherAbove() { KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactlyInAnyOrder( - "tag(common1=commonValue1)", - "tag(k1=v1)" + "keyValue(common1=commonValue1)", + "keyValue(k1=v1)" ); } @@ -172,9 +172,9 @@ void resolveKeyValues_multipleScatteredKeyValuesSetAbove() { KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactlyInAnyOrder( - "tag(common1=commonValue1)", - "tag(k1=v1)", - "tag(k2=v2)" + "keyValue(common1=commonValue1)", + "keyValue(k1=v1)", + "keyValue(k2=v2)" ); } @@ -191,9 +191,9 @@ void resolveKeyValues_multipleScatteredKeyValuesSetAboveWithDeduplication() { KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(flux, defaultKeyValues); assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactly( - "tag(common1=commonValue1)", - "tag(k1=v1)", - "tag(k2=v2)" + "keyValue(common1=commonValue1)", + "keyValue(k1=v1)", + "keyValue(k2=v2)" ); } @@ -204,6 +204,6 @@ void resolveKeyValues_notScannable() { KeyValues resolvedKeyValues = MicrometerObservationListenerConfiguration.resolveKeyValues(publisher, defaultKeyValues); - assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactly("tag(common1=commonValue1)"); + assertThat(resolvedKeyValues.stream().map(Object::toString)).containsExactly("keyValue(common1=commonValue1)"); } } \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java index e3c2fa5c36..8bfd7ab044 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactoryTest.java @@ -46,7 +46,7 @@ void configurationFromMono() { assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); assertThat(configuration.isMono).as("isMono").isTrue(); - assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("tag(" + "reactor.type" + "=Mono)"); + assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("keyValue(" + "reactor.type" + "=Mono)"); } @Test @@ -55,7 +55,7 @@ void configurationFromFlux() { assertThat(configuration.registry).as("registry").isSameAs(CUSTOM_REGISTRY); assertThat(configuration.isMono).as("isMono").isFalse(); - assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("tag(" + "reactor.type" + "=Flux)"); + assertThat(configuration.commonKeyValues).map(Object::toString).containsExactly("keyValue(" + "reactor.type" + "=Flux)"); } @Test From 88587fc64b486ed04b15cac2081c01623a5b1c57 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 3 Aug 2022 18:27:44 +0300 Subject: [PATCH 046/312] Make some Mono sources and aggregators lazier (#3081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit changes 4 broad categories of Mono operators to be lazier and only start requesting from their upstream once they've been themselves requested. Previously, the request to upstream would happen immediately upon receiving the Subscription (in `onSubscribe`). The first category of Monos is the one where the upstream is a Flux that gets aggregated into a Mono, such as `MonoCollectList`, `MonoCount` but also `MonoAll` or `MonoElementAt`... These Mono now all inherit a common `BaseFluxToMonoOperator` which track two states as bits in a single volatile: request status (true/false) and completed status (true/false). It uses a non-volatile boolean hasRequest as a shortcut to ignore any request past the first one. Note that for now these implementations do declare a `Fuseable` trait, but they always negotiate fusion `NONE`. It is kept as a future possible improvement that they would all support SYNC fusion. The second category is Monos that combine multiple sources, like `MonoZip` or `MonoWhen`. A single volatile long-based state machine is used to coordinate the multiple inners and track which inner has made the first request. The third category is ParallelFlux-to-Mono operators, mixing the above two aspects: aggregating values and combining multiple sources (rails). Some of these can benefit from the `BaseFluxToMonoOperator` approach. The last category is sources like `MonoSupplier` or `MonoCallable`. Often the functional interface representing the logic would be run in `onSubscribe` directly, but making it lazier implies the creation of dedicated `Subscription` implementations now. Fixes #2913. Signed-off-by: Oleh Dokuka Co-authored-by: Simon Baslé --- .../core/publisher/MonoAllBenchmark.java | 57 +++ .../core/publisher/MonoCallableBenchmark.java | 57 +++ .../reactor/core/publisher/FluxCallable.java | 18 +- .../core/publisher/FluxDefaultIfEmpty.java | 100 ++++-- .../java/reactor/core/publisher/FluxZip.java | 136 ++++++- .../java/reactor/core/publisher/MonoAll.java | 38 +- .../java/reactor/core/publisher/MonoAny.java | 41 +-- .../reactor/core/publisher/MonoCallable.java | 135 +++++-- .../reactor/core/publisher/MonoCollect.java | 100 +++--- .../core/publisher/MonoCollectList.java | 79 ++--- .../core/publisher/MonoCompletionStage.java | 114 +++++- .../reactor/core/publisher/MonoCount.java | 38 +- .../core/publisher/MonoDelayElement.java | 136 +++++-- .../reactor/core/publisher/MonoElementAt.java | 47 +-- .../core/publisher/MonoFilterWhen.java | 293 ++++++++++------ .../reactor/core/publisher/MonoFlatMap.java | 129 +++++-- .../core/publisher/MonoHasElement.java | 63 ++-- .../core/publisher/MonoHasElements.java | 50 +-- .../reactor/core/publisher/MonoReduce.java | 138 ++++++-- .../core/publisher/MonoReduceSeed.java | 107 +++--- .../core/publisher/MonoStreamCollector.java | 96 ++--- .../reactor/core/publisher/MonoSupplier.java | 126 +++++-- .../core/publisher/MonoTakeLastOne.java | 131 +++++-- .../java/reactor/core/publisher/MonoWhen.java | 248 ++++++++++--- .../java/reactor/core/publisher/MonoZip.java | 332 +++++++++++++----- .../reactor/core/publisher/Operators.java | 149 +++++++- .../core/publisher/ParallelCollect.java | 69 ++-- .../core/publisher/ParallelMergeReduce.java | 107 ++++-- .../core/publisher/ParallelReduceSeed.java | 93 +++-- .../reactor/core/publisher/ParallelThen.java | 200 +++++++++-- .../publisher/FluxDefaultIfEmptyTest.java | 17 +- .../core/publisher/FluxDetachTest.java | 4 +- .../core/publisher/FluxDoOnEachTest.java | 19 - .../reactor/core/publisher/MonoAllTest.java | 8 +- .../reactor/core/publisher/MonoAnyTest.java | 8 +- .../core/publisher/MonoCallableTest.java | 19 +- .../core/publisher/MonoCollectListTest.java | 6 +- .../core/publisher/MonoCollectTest.java | 23 +- .../publisher/MonoCompletionStageTest.java | 31 +- .../reactor/core/publisher/MonoCountTest.java | 13 +- .../core/publisher/MonoDelayElementTest.java | 26 +- .../core/publisher/MonoElementAtTest.java | 8 +- .../core/publisher/MonoFilterWhenTest.java | 12 +- .../core/publisher/MonoFlatMapTest.java | 12 +- .../core/publisher/MonoHasElementsTest.java | 41 +-- .../core/publisher/MonoPeekAfterTest.java | 71 ---- .../publisher/MonoPublishMulticastTest.java | 43 +-- .../core/publisher/MonoReduceSeedTest.java | 37 +- .../core/publisher/MonoReduceTest.java | 33 +- .../publisher/MonoStreamCollectorTest.java | 6 +- .../core/publisher/MonoSubscribeOnTest.java | 4 +- .../core/publisher/MonoSupplierTest.java | 9 +- .../core/publisher/MonoTakeLastOneTest.java | 7 +- .../reactor/core/publisher/MonoUsingTest.java | 49 +-- .../reactor/core/publisher/MonoZipTest.java | 21 +- .../publisher/OnDiscardShouldNotLeakTest.java | 41 ++- .../core/publisher/ParallelCollectTest.java | 22 +- .../publisher/ParallelMergeReduceTest.java | 16 +- .../publisher/ParallelReduceSeedTest.java | 20 +- .../core/publisher/ParallelThenTest.java | 45 ++- .../publisher/MonoMetricsFuseableTest.java | 95 +---- 61 files changed, 2741 insertions(+), 1452 deletions(-) create mode 100644 benchmarks/src/main/java/reactor/core/publisher/MonoAllBenchmark.java create mode 100644 benchmarks/src/main/java/reactor/core/publisher/MonoCallableBenchmark.java diff --git a/benchmarks/src/main/java/reactor/core/publisher/MonoAllBenchmark.java b/benchmarks/src/main/java/reactor/core/publisher/MonoAllBenchmark.java new file mode 100644 index 0000000000..97f27110ea --- /dev/null +++ b/benchmarks/src/main/java/reactor/core/publisher/MonoAllBenchmark.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({Mode.AverageTime}) +@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class MonoAllBenchmark { + + @Param({"0", "10", "1000", "100000"}) + int rangeSize; + + public static void main(String[] args) throws Exception { + reactor.core.scrabble.ShakespearePlaysScrabbleOpt + s = new reactor.core.scrabble.ShakespearePlaysScrabbleOpt(); + s.init(); + System.out.println(s.measureThroughput()); + } + + @SuppressWarnings("unused") + @Benchmark + public void measureThroughput() { + Flux.range(0, rangeSize) + .all(i -> i < Integer.MAX_VALUE) + .block(); + } +} diff --git a/benchmarks/src/main/java/reactor/core/publisher/MonoCallableBenchmark.java b/benchmarks/src/main/java/reactor/core/publisher/MonoCallableBenchmark.java new file mode 100644 index 0000000000..886b9a2c43 --- /dev/null +++ b/benchmarks/src/main/java/reactor/core/publisher/MonoCallableBenchmark.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({Mode.AverageTime}) +@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class MonoCallableBenchmark { + + @Param({"10", "1000", "100000"}) + int rangeSize; + + public static void main(String[] args) throws Exception { + reactor.core.scrabble.ShakespearePlaysScrabbleOpt + s = new reactor.core.scrabble.ShakespearePlaysScrabbleOpt(); + s.init(); + System.out.println(s.measureThroughput()); + } + + @SuppressWarnings("unused") + @Benchmark + public void measureThroughput() { + Flux.range(0, rangeSize) + .all(i -> i < Integer.MAX_VALUE) + .block(); + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxCallable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxCallable.java index 59f8059fd3..d479901d20 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxCallable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxCallable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,21 +37,7 @@ final class FluxCallable extends Flux implements Callable, Fuseable, So @Override public void subscribe(CoreSubscriber actual) { - Operators.MonoSubscriber wrapper = new Operators.MonoSubscriber<>(actual); - actual.onSubscribe(wrapper); - - try { - T v = callable.call(); - if (v == null) { - wrapper.onComplete(); - } - else { - wrapper.complete(v); - } - } - catch (Throwable ex) { - actual.onError(Operators.onOperatorError(ex, actual.currentContext())); - } + actual.onSubscribe(new MonoCallable.MonoCallableSubscription<>(actual, callable)); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxDefaultIfEmpty.java b/reactor-core/src/main/java/reactor/core/publisher/FluxDefaultIfEmpty.java index 6772e62aaa..69302d69ff 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxDefaultIfEmpty.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxDefaultIfEmpty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,9 @@ package reactor.core.publisher; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; -import reactor.core.Fuseable; import reactor.util.annotation.Nullable; /** @@ -50,45 +49,62 @@ public Object scanUnsafe(Attr key) { } static final class DefaultIfEmptySubscriber - extends Operators.MonoSubscriber { + extends Operators.BaseFluxToMonoOperator { - Subscription s; + boolean done; boolean hasValue; - DefaultIfEmptySubscriber(CoreSubscriber actual, T value) { + volatile T fallbackValue; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater FALLBACK_VALUE = + AtomicReferenceFieldUpdater.newUpdater(DefaultIfEmptySubscriber.class, Object.class, "fallbackValue"); + + DefaultIfEmptySubscriber(CoreSubscriber actual, T fallbackValue) { super(actual); - //noinspection deprecation - this.value = value; //we write once, setValue() is NO-OP + FALLBACK_VALUE.lazySet(this, fallbackValue); } @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) return s; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.TERMINATED) return done; return super.scanUnsafe(key); } @Override public void request(long n) { - super.request(n); + if (!hasRequest) { + hasRequest = true; + + final int state = this.state; + + if (state != 1 && STATE.compareAndSet(this, state, state | 1)) { + if (state > 1) { + final T fallbackValue = this.fallbackValue; + if (fallbackValue != null && FALLBACK_VALUE.compareAndSet(this, + fallbackValue, + null)) { + // completed before request means source was empty + actual.onNext(fallbackValue); + actual.onComplete(); + } + return; + } + } + } + s.request(n); } @Override public void cancel() { super.cancel(); - s.cancel(); - } - - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - actual.onSubscribe(this); + final T fallbackValue = this.fallbackValue; + if (fallbackValue != null && FALLBACK_VALUE.compareAndSet(this, fallbackValue, null)) { + Operators.onDiscard(fallbackValue, actual.currentContext()); } } @@ -96,6 +112,11 @@ public void onSubscribe(Subscription s) { public void onNext(T t) { if (!hasValue) { hasValue = true; + + final T fallbackValue = this.fallbackValue; + if (fallbackValue != null && FALLBACK_VALUE.compareAndSet(this, fallbackValue, null)) { + Operators.onDiscard(fallbackValue, actual.currentContext()); + } } actual.onNext(t); @@ -103,22 +124,45 @@ public void onNext(T t) { @Override public void onComplete() { - if (hasValue) { - actual.onComplete(); - } else { - complete(this.value); + if (done) { + return; } + + done = true; + + if (!hasValue) { + completePossiblyEmpty(); + + return; + } + + actual.onComplete(); } @Override - public void setValue(T value) { - // value is constant. writes from the base class are redundant, and the constant - // would always be visible in cancel(), so it will safely be discarded. + public void onError(Throwable t) { + if (done) { + return; + } + + done = true; + if (!hasValue) { + final T fallbackValue = this.fallbackValue; + if (fallbackValue != null && FALLBACK_VALUE.compareAndSet(this, fallbackValue, null)) { + Operators.onDiscard(t, actual.currentContext()); + } + } + + actual.onError(t); } @Override - public int requestFusion(int requestedMode) { - return Fuseable.NONE; // prevent fusion because of the upstream + T accumulatedValue() { + final T fallbackValue = this.fallbackValue; + if (fallbackValue != null && FALLBACK_VALUE.compareAndSet(this, fallbackValue, null)) { + return fallbackValue; + } + return null; } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxZip.java b/reactor-core/src/main/java/reactor/core/publisher/FluxZip.java index 80b80d2211..e3343e5e28 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxZip.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxZip.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -304,40 +304,142 @@ void handleBoth(CoreSubscriber s, coordinator.subscribe(n, sc, srcs); } else { - Operators.MonoSubscriber sds = new Operators.MonoSubscriber<>(s); + s.onSubscribe(new ZipScalarCoordinator<>(s, zipper, scalars)); + } + } + else { + ZipCoordinator coordinator = + new ZipCoordinator<>(s, zipper, n, queueSupplier, prefetch); + + s.onSubscribe(coordinator); - s.onSubscribe(sds); + coordinator.subscribe(srcs, n); + } + } - R r; + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PREFETCH) return prefetch; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + return null; + } + + static final class ZipScalarCoordinator implements InnerProducer, + Fuseable, + Fuseable.QueueSubscription { + + final CoreSubscriber actual; + final Function zipper; + final Object[] scalars; + + volatile int state; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater STATE = + AtomicIntegerFieldUpdater.newUpdater(ZipScalarCoordinator.class, "state"); + + boolean done; + boolean cancelled; + + ZipScalarCoordinator(CoreSubscriber actual, Function zipper, Object[] scalars) { + this.actual = actual; + this.zipper = zipper; + this.scalars = scalars; + } + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.TERMINATED) return done; + if (key == Attr.CANCELLED) return cancelled; + if (key == Attr.BUFFERED) return scalars.length; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return InnerProducer.super.scanUnsafe(key); + } + + @Override + public CoreSubscriber actual() { + return this.actual; + } + + @Override + public void request(long n) { + if (done) { + return; + } + done = true; + + final int state = this.state; + if (state == 0 && STATE.compareAndSet(this, 0, 1)) { + final R r; try { r = Objects.requireNonNull(zipper.apply(scalars), "The zipper returned a null value"); } catch (Throwable e) { - s.onError(Operators.onOperatorError(e, s.currentContext())); + actual.onError(Operators.onOperatorError(e, actual.currentContext())); return; } - sds.complete(r); + this.actual.onNext(r); + this.actual.onComplete(); + } + } + + @Override + public void cancel() { + if (cancelled) { + return; } + cancelled = true; + final int state = this.state; + if (state == 0 && STATE.compareAndSet(this, 0, 2)) { + final Context context = actual.currentContext(); + for (Object scalar : scalars) { + Operators.onDiscard(scalar, context); + } + } } - else { - ZipCoordinator coordinator = - new ZipCoordinator<>(s, zipper, n, queueSupplier, prefetch); - s.onSubscribe(coordinator); + @Override + public R poll() { + if (done) { + return null; + } + done = true; - coordinator.subscribe(srcs, n); + return Objects.requireNonNull(zipper.apply(scalars), "The zipper returned a null value"); } - } - @Override - public Object scanUnsafe(Attr key) { - if (key == Attr.PREFETCH) return prefetch; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + @Override + public int requestFusion(int requestedMode) { + return requestedMode & SYNC; + } + + @Override + public int size() { + return done ? 0 : 1; + } + + @Override + public boolean isEmpty() { + return done; + } + + @Override + public void clear() { + if (done || cancelled) { + return; + } + + cancelled = true; + + final Context context = actual.currentContext(); + for (Object scalar : scalars) { + Operators.onDiscard(scalar, context); + } + } } static final class ZipSingleCoordinator extends Operators.MonoSubscriber { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoAll.java b/reactor-core/src/main/java/reactor/core/publisher/MonoAll.java index 8c2ef49e26..383b35a364 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoAll.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoAll.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.util.Objects; import java.util.function.Predicate; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -55,11 +54,8 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class AllSubscriber extends Operators.MonoSubscriber { + static final class AllSubscriber extends Operators.BaseFluxToMonoOperator { final Predicate predicate; - - Subscription s; - boolean done; AllSubscriber(CoreSubscriber actual, Predicate predicate) { @@ -71,32 +67,14 @@ static final class AllSubscriber extends Operators.MonoSubscriber @Nullable public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return done; - if (key == Attr.PARENT) return s; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; return super.scanUnsafe(key); } - @Override - public void cancel() { - s.cancel(); - super.cancel(); - } - - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - actual.onSubscribe(this); - - s.request(Long.MAX_VALUE); - } - } - @Override public void onNext(T t) { - if (done) { + Operators.onDiscard(t, this.actual.currentContext()); return; } @@ -113,7 +91,8 @@ public void onNext(T t) { done = true; s.cancel(); - complete(false); + this.actual.onNext(false); + this.actual.onComplete(); } } @@ -134,8 +113,13 @@ public void onComplete() { return; } done = true; - complete(true); + + completePossiblyEmpty(); } + @Override + Boolean accumulatedValue() { + return true; + } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoAny.java b/reactor-core/src/main/java/reactor/core/publisher/MonoAny.java index 28f1dea44c..8117de8579 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoAny.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoAny.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.util.Objects; import java.util.function.Predicate; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -55,10 +54,10 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class AnySubscriber extends Operators.MonoSubscriber { - final Predicate predicate; + static final class AnySubscriber extends + Operators.BaseFluxToMonoOperator { - Subscription s; + final Predicate predicate; boolean done; @@ -71,33 +70,14 @@ static final class AnySubscriber extends Operators.MonoSubscriber @Nullable public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return done; - if (key == Attr.PARENT) return s; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; return super.scanUnsafe(key); } - @Override - public void cancel() { - s.cancel(); - super.cancel(); - } - - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); - - s.request(Long.MAX_VALUE); - } - } - @Override public void onNext(T t) { - if (done) { + Operators.onDiscard(t, this.actual.currentContext()); return; } @@ -110,11 +90,13 @@ public void onNext(T t) { actual.onError(Operators.onOperatorError(s, e, t, actual.currentContext())); return; } + if (b) { done = true; s.cancel(); - complete(true); + this.actual.onNext(true); + this.actual.onComplete(); } } @@ -135,8 +117,13 @@ public void onComplete() { return; } done = true; - complete(false); + + completePossiblyEmpty(); } + @Override + Boolean accumulatedValue() { + return false; + } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCallable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCallable.java index f74f11283e..313e13713b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCallable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCallable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,14 @@ import java.time.Duration; import java.util.Objects; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; -/** + /** * Executes a Callable function and emits a single value to each individual Subscriber. *

    * Preferred to {@link java.util.function.Supplier} because the Callable may throw. @@ -44,28 +45,7 @@ final class MonoCallable extends Mono @Override public void subscribe(CoreSubscriber actual) { - Operators.MonoSubscriber - sds = new Operators.MonoSubscriber<>(actual); - - actual.onSubscribe(sds); - - if (sds.isCancelled()) { - return; - } - - try { - T t = callable.call(); - if (t == null) { - sds.onComplete(); - } - else { - sds.complete(t); - } - } - catch (Throwable e) { - actual.onError(Operators.onOperatorError(e, actual.currentContext())); - } - + actual.onSubscribe(new MonoCallableSubscription<>(actual, this.callable)); } @Override @@ -97,4 +77,111 @@ public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; return null; } + + static class MonoCallableSubscription + implements InnerProducer, Fuseable, QueueSubscription { + + final CoreSubscriber actual; + final Callable callable; + + boolean done; + + volatile int requestedOnce; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater REQUESTED_ONCE = + AtomicIntegerFieldUpdater.newUpdater(MonoCallableSubscription.class, + "requestedOnce"); + + volatile boolean cancelled; + + MonoCallableSubscription(CoreSubscriber actual, Callable callable) { + this.actual = actual; + this.callable = callable; + } + + @Override + public CoreSubscriber actual() { + return this.actual; + } + + @Override + public T poll() { + if (this.done) { + return null; + } + + this.done = true; + + try { + return this.callable.call(); + } + catch (Throwable e) { + throw Exceptions.propagate(e); + } + } + + @Override + public void request(long n) { + if (this.cancelled) { + return; + } + + if (this.requestedOnce == 1 || !REQUESTED_ONCE.compareAndSet(this, 0 , 1)) { + return; + } + + final CoreSubscriber s = this.actual; + + final T value; + try { + value = this.callable.call(); + } + catch (Exception e) { + if (this.cancelled) { + Operators.onErrorDropped(e, s.currentContext()); + return; + } + + s.onError(e); + return; + } + + + if (this.cancelled) { + Operators.onDiscard(value, s.currentContext()); + return; + } + + if (value != null) { + s.onNext(value); + } + + s.onComplete(); + } + + @Override + public void cancel() { + this.cancelled = true; + } + + @Override + public int requestFusion(int requestedMode) { + return requestedMode & SYNC; + } + + @Override + public int size() { + return this.done ? 0 : 1; + } + + @Override + public boolean isEmpty() { + return this.done; + } + + @Override + public void clear() { + this.done = true; + } + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCollect.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCollect.java index cde1d01115..9d46852797 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCollect.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCollect.java @@ -21,8 +21,6 @@ import java.util.function.BiConsumer; import java.util.function.Supplier; -import org.reactivestreams.Subscription; - import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -67,14 +65,12 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class CollectSubscriber extends Operators.MonoSubscriber { + static final class CollectSubscriber extends Operators.BaseFluxToMonoOperator { final BiConsumer action; R container; - Subscription s; - boolean done; CollectSubscriber(CoreSubscriber actual, @@ -88,21 +84,9 @@ static final class CollectSubscriber extends Operators.MonoSubscriber c = (Collection) v; - Operators.onDiscardMultiple(c, actual.currentContext()); - } - else { - super.discard(v); + R accumulatedValue() { + final R c; + synchronized (this) { + c = container; + container = null; } + + return c; } - @Override - public void cancel() { - int state; - R c; - state = STATE.getAndSet(this, CANCELLED); - if (state != CANCELLED) { - s.cancel(); - } - if (state <= HAS_REQUEST_NO_VALUE) { - synchronized (this) { - c = container; - this.value = null; - container = null; - } + void discard(R v) { + if (v instanceof Collection) { + Collection c = (Collection) v; + Operators.onDiscardMultiple(c, actual.currentContext()); } else { - c = null; - } - if (c != null) { - discard(c); + Operators.onDiscard(v, actual.currentContext()); } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCollectList.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCollectList.java index 16ef74b7f7..c8054beb26 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCollectList.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCollectList.java @@ -19,8 +19,6 @@ import java.util.ArrayList; import java.util.List; -import org.reactivestreams.Subscription; - import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -47,12 +45,10 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class MonoCollectListSubscriber extends Operators.MonoSubscriber> { + static final class MonoCollectListSubscriber extends Operators.BaseFluxToMonoOperator> { List list; - Subscription s; - boolean done; MonoCollectListSubscriber(CoreSubscriber> actual) { @@ -64,21 +60,10 @@ static final class MonoCollectListSubscriber extends Operators.MonoSubscriber @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) return s; if (key == Attr.TERMINATED) return done; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return super.scanUnsafe(key); - } + if (key == Attr.CANCELLED) return !done && list == null; - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); - - s.request(Long.MAX_VALUE); - } + return super.scanUnsafe(key); } @Override @@ -87,7 +72,8 @@ public void onNext(T t) { Operators.onNextDropped(t, actual.currentContext()); return; } - List l; + + final List l; synchronized (this) { l = list; if (l != null) { @@ -95,6 +81,7 @@ public void onNext(T t) { return; } } + Operators.onDiscard(t, actual.currentContext()); } @@ -105,57 +92,55 @@ public void onError(Throwable t) { return; } done = true; - List l; + + final List l; synchronized (this) { l = list; list = null; } - discard(l); + + if (l == null) { + return; + } + + Operators.onDiscardMultiple(l, actual.currentContext()); + actual.onError(t); } @Override public void onComplete() { - if(done) { + if (done) { return; } done = true; - List l; + + completePossiblyEmpty(); + } + + @Override + public void cancel() { + s.cancel(); + + final List l; synchronized (this) { l = list; list = null; } + if (l != null) { - complete(l); + Operators.onDiscardMultiple(l, actual.currentContext()); } } @Override - protected void discard(List v) { - Operators.onDiscardMultiple(v, actual.currentContext()); - } - - @Override - public void cancel() { - int state; - List l; - state = STATE.getAndSet(this, CANCELLED); - if (state != CANCELLED) { - s.cancel(); - } + List accumulatedValue() { + final List l; synchronized (this) { - if (state <= HAS_REQUEST_NO_VALUE) { - l = list; - this.value = null; - list = null; - } - else { - l = null; - } - } - if (l != null) { - discard(l); + l = list; + list = null; } + return l; } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java index 7aafb26688..4268fb6e04 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,17 @@ import java.util.Objects; import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.function.BiConsumer; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.util.annotation.Nullable; import reactor.util.context.Context; /** @@ -46,32 +50,61 @@ final class MonoCompletionStage extends Mono @Override public void subscribe(CoreSubscriber actual) { - Operators.MonoSubscriber - sds = new Operators.MonoSubscriber<>(actual); + actual.onSubscribe(new MonoCompletionStageSubscription<>(actual, future)); + } - actual.onSubscribe(sds); + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + return null; + } - if (sds.isCancelled()) { - return; + static class MonoCompletionStageSubscription implements InnerProducer, + Fuseable, + QueueSubscription, + BiConsumer { + + final CoreSubscriber actual; + final CompletionStage future; + + volatile int requestedOnce; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater REQUESTED_ONCE = + AtomicIntegerFieldUpdater.newUpdater(MonoCompletionStageSubscription.class, "requestedOnce"); + + volatile boolean cancelled; + + MonoCompletionStageSubscription(CoreSubscriber actual, CompletionStage future) { + this.actual = actual; + this.future = future; } - future.whenComplete((v, e) -> { - if (sds.isCancelled()) { + @Override + public CoreSubscriber actual() { + return this.actual; + } + + @Override + public void accept(@Nullable T value, @Nullable Throwable e) { + final CoreSubscriber actual = this.actual; + + if (this.cancelled) { //nobody is interested in the Mono anymore, don't risk dropping errors - Context ctx = sds.currentContext(); + final Context ctx = actual.currentContext(); if (e == null || e instanceof CancellationException) { //we discard any potential value and ignore Future cancellations - Operators.onDiscard(v, ctx); + Operators.onDiscard(value, ctx); } else { //we make sure we keep _some_ track of a Future failure AFTER the Mono cancellation Operators.onErrorDropped(e, ctx); //and we discard any potential value just in case both e and v are not null - Operators.onDiscard(v, ctx); + Operators.onDiscard(value, ctx); } return; } + try { if (e instanceof CompletionException) { actual.onError(e.getCause()); @@ -79,8 +112,9 @@ public void subscribe(CoreSubscriber actual) { else if (e != null) { actual.onError(e); } - else if (v != null) { - sds.complete(v); + else if (value != null) { + actual.onNext(value); + actual.onComplete(); } else { actual.onComplete(); @@ -90,12 +124,54 @@ else if (v != null) { Operators.onErrorDropped(e1, actual.currentContext()); throw Exceptions.bubble(e1); } - }); - } + } - @Override - public Object scanUnsafe(Attr key) { - if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; - return null; + @Override + public void request(long n) { + if (this.cancelled) { + return; + } + + if (this.requestedOnce == 1 || !REQUESTED_ONCE.compareAndSet(this, 0 , 1)) { + return; + } + + future.whenComplete(this); + } + + @Override + public void cancel() { + this.cancelled = true; + + final CompletionStage future = this.future; + if (future instanceof CompletableFuture) { + //noinspection unchecked + ((CompletableFuture) future).cancel(true); + } + } + + @Override + public int requestFusion(int requestedMode) { + return NONE; + } + + @Override + public T poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCount.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCount.java index 8f4cda5fb9..b25d0326e5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCount.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCount.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package reactor.core.publisher; - -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -46,11 +44,11 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class CountSubscriber extends Operators.MonoSubscriber { + static final class CountSubscriber extends Operators.BaseFluxToMonoOperator { - long counter; + boolean done; - Subscription s; + long counter; CountSubscriber(CoreSubscriber actual) { super(actual); @@ -59,38 +57,30 @@ static final class CountSubscriber extends Operators.MonoSubscriber @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) return s; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.TERMINATED) return done; return super.scanUnsafe(key); } @Override - public void cancel() { - super.cancel(); - s.cancel(); + public void onNext(T t) { + Operators.onDiscard(t, currentContext()); + counter++; } @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); - - s.request(Long.MAX_VALUE); - } + public void onError(Throwable t) { + this.actual.onError(t); } @Override - public void onNext(T t) { - counter++; + public void onComplete() { + completePossiblyEmpty(); } @Override - public void onComplete() { - complete(counter); + Long accumulatedValue() { + return counter; } - } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoDelayElement.java b/reactor-core/src/main/java/reactor/core/publisher/MonoDelayElement.java index 9b2d6d8577..155321a17a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoDelayElement.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoDelayElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,13 @@ import java.util.Objects; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.Fuseable; import reactor.core.scheduler.Scheduler; import reactor.util.annotation.Nullable; @@ -63,20 +66,35 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class DelayElementSubscriber extends Operators.MonoSubscriber { + static final class DelayElementSubscriber implements InnerOperator, + Fuseable, + Fuseable.QueueSubscription, + Runnable { + static final Disposable CANCELLED = Disposables.disposed(); + static final Disposable TERMINATED = Disposables.disposed(); + + final CoreSubscriber actual; final long delay; final Scheduler scheduler; final TimeUnit unit; Subscription s; - - volatile Disposable task; + T value; boolean done; - DelayElementSubscriber(CoreSubscriber actual, Scheduler scheduler, - long delay, TimeUnit unit) { - super(actual); + volatile Disposable task; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater TASK = + AtomicReferenceFieldUpdater.newUpdater(DelayElementSubscriber.class, Disposable.class, "task"); + + DelayElementSubscriber( + CoreSubscriber actual, + Scheduler scheduler, + long delay, + TimeUnit unit + ) { + this.actual = actual; this.scheduler = scheduler; this.delay = delay; this.unit = unit; @@ -85,23 +103,22 @@ static final class DelayElementSubscriber extends Operators.MonoSubscriber actual() { + return this.actual; } @Override @@ -110,7 +127,6 @@ public void onSubscribe(Subscription s) { this.s = s; actual.onSubscribe(this); - s.request(Long.MAX_VALUE); } } @@ -120,15 +136,66 @@ public void onNext(T t) { Operators.onNextDropped(t, actual.currentContext()); return; } + this.done = true; + this.value = t; + try { - this.task = scheduler.schedule(() -> complete(t), delay, unit); + final Disposable currentTask = this.task; + final Disposable nextTask = scheduler.schedule(this, delay, unit); + if (currentTask != null || !TASK.compareAndSet(this, null, nextTask)) { + this.value = null; + nextTask.dispose(); + Operators.onDiscard(t, actual.currentContext()); + } } catch (RejectedExecutionException ree) { - actual.onError(Operators.onRejectedExecution(ree, this, null, t, - actual.currentContext())); + this.value = null; + Operators.onDiscard(t, actual.currentContext()); + actual.onError(Operators.onRejectedExecution(ree, this, null, t, actual.currentContext())); + } + } + + @Override + public void run() { + final Disposable currentTask = this.task; + + if (currentTask == CANCELLED || !TASK.compareAndSet(this, currentTask, TERMINATED)) { return; } + + final T value = this.value; + this.value = null; + + this.actual.onNext(value); + this.actual.onComplete(); + } + + @Override + public void cancel() { + final Disposable task = this.task; + if (task == CANCELLED || task == TERMINATED) { + return; + } + + if (TASK.compareAndSet(this, task, CANCELLED)) { + if (task != null) { + task.dispose(); + + final T value = this.value; + this.value = null; + + Operators.onDiscard(value, actual.currentContext()); + return; + } + } + + s.cancel(); + } + + @Override + public void request(long n) { + s.request(n); } @Override @@ -137,6 +204,7 @@ public void onComplete() { return; } this.done = true; + actual.onComplete(); } @@ -147,7 +215,33 @@ public void onError(Throwable t) { return; } this.done = true; + actual.onError(t); } + + @Override + public T poll() { + return null; + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoElementAt.java b/reactor-core/src/main/java/reactor/core/publisher/MonoElementAt.java index b6a889bcf0..a890db08b8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoElementAt.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoElementAt.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.Objects; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -66,20 +65,17 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class ElementAtSubscriber - extends Operators.MonoSubscriber { + static final class ElementAtSubscriber extends Operators.BaseFluxToMonoOperator { + @Nullable final T defaultValue; long index; final long target; - Subscription s; - boolean done; - ElementAtSubscriber(CoreSubscriber actual, long index, - T defaultValue) { + ElementAtSubscriber(CoreSubscriber actual, long index, @Nullable T defaultValue) { super(actual); this.index = index; this.target = index; @@ -90,35 +86,10 @@ static final class ElementAtSubscriber @Nullable public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return done; - if (key == Attr.PARENT) return s; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; return super.scanUnsafe(key); } - @Override - public void request(long n) { - super.request(n); - if (n > 0L) { - s.request(Long.MAX_VALUE); - } - } - - @Override - public void cancel() { - super.cancel(); - s.cancel(); - } - - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); - } - } - @Override public void onNext(T t) { if (done) { @@ -157,8 +128,9 @@ public void onComplete() { } done = true; - if(defaultValue != null) { - complete(defaultValue); + final T dv = defaultValue; + if (dv != null) { + completePossiblyEmpty(); } else{ long count = target - index; @@ -167,5 +139,10 @@ public void onComplete() { actual.currentContext())); } } + + @Override + T accumulatedValue() { + return defaultValue; + } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java index afdba693b6..7865886443 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; +import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -65,60 +66,46 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class MonoFilterWhenMain extends Operators.MonoSubscriber { - - /* Implementation notes on state transitions: - * This subscriber runs through a few possible state transitions, that are - * expressed through the signal methods rather than an explicit state variable, - * as they are simple enough (states suffixed with a * correspond to a terminal - * signal downstream): - * - SUBSCRIPTION -> EMPTY | VALUED | EARLY ERROR - * - EMPTY -> COMPLETE - * - VALUED -> FILTERING | EARLY ERROR - * - EARLY ERROR* - * - FILTERING -> FEMPTY | FERROR | FVALUED - * - FEMPTY -> COMPLETE - * - FERROR* - * - FVALUED -> ON NEXT + COMPLETE | COMPLETE - * - COMPLETE* - */ + static final class MonoFilterWhenMain implements InnerOperator, + Fuseable, //for constants only + Fuseable.QueueSubscription { final Function> asyncPredicate; + final CoreSubscriber actual; - //this is only touched by onNext and read by onComplete, so no need for volatile - boolean sourceValued; + Subscription s; - Subscription upstream; - - volatile FilterWhenInner asyncFilter; + boolean done; + volatile FilterWhenInner asyncFilter; + @SuppressWarnings("rawtypes") static final AtomicReferenceFieldUpdater ASYNC_FILTER = AtomicReferenceFieldUpdater.newUpdater(MonoFilterWhenMain.class, FilterWhenInner.class, "asyncFilter"); - @SuppressWarnings("ConstantConditions") - static final FilterWhenInner INNER_CANCELLED = new FilterWhenInner(null, false); + @SuppressWarnings({"ConstantConditions", "rawtypes"}) + static final FilterWhenInner INNER_CANCELLED = new FilterWhenInner(null, false, null); + @SuppressWarnings({"ConstantConditions", "rawtypes"}) + static final FilterWhenInner INNER_TERMINATED = new FilterWhenInner(null, false, null); MonoFilterWhenMain(CoreSubscriber actual, Function> asyncPredicate) { - super(actual); + this.actual = actual; this.asyncPredicate = asyncPredicate; } @Override public void onSubscribe(Subscription s) { - if (Operators.validate(upstream, s)) { - upstream = s; - actual.onSubscribe(this); - s.request(Long.MAX_VALUE); + if (Operators.validate(this.s, s)) { + this.s = s; + this.actual.onSubscribe(this); } } @SuppressWarnings("unchecked") @Override public void onNext(T t) { + this.done = true; //we assume the source is a Mono, so only one onNext will ever happen - sourceValued = true; - setValue(t); Publisher p; try { @@ -127,8 +114,8 @@ public void onNext(T t) { } catch (Throwable ex) { Exceptions.throwIfFatal(ex); - super.onError(ex); Operators.onDiscard(t, actual.currentContext()); + this.actual.onError(ex); return; } @@ -140,177 +127,253 @@ public void onNext(T t) { } catch (Throwable ex) { Exceptions.throwIfFatal(ex); - super.onError(ex); Operators.onDiscard(t, actual.currentContext()); + this.actual.onError(ex); return; } if (u != null && u) { - complete(t); + this.actual.onNext(t); + this.actual.onComplete(); } else { - actual.onComplete(); Operators.onDiscard(t, actual.currentContext()); + actual.onComplete(); } } else { - FilterWhenInner inner = new FilterWhenInner(this, !(p instanceof Mono)); - if (ASYNC_FILTER.compareAndSet(this, null, inner)) { - p.subscribe(inner); - } + FilterWhenInner inner = new FilterWhenInner<>(this, !(p instanceof Mono), t); + p.subscribe(inner); } } @Override public void onComplete() { - if (!sourceValued) { - //there was no value, we can complete empty - super.onComplete(); + if (this.done) { + return; } - //otherwise just wait for the inner filter to apply, rather than complete too soon - } - /* implementation note on onError: - * if the source errored, we can propagate that directly since there - * was no chance for an inner subscriber to have been triggered - * (the source being a Mono). So we can just have the parent's behavior - * of calling actual.onError(t) for onError. - */ + //there was no value, we can complete empty + this.done = true; + this.actual.onComplete(); + } @Override - public void cancel() { - if (super.state != CANCELLED) { - super.cancel(); - upstream.cancel(); - cancelInner(); + public void onError(Throwable t) { + if (this.done) { + Operators.onErrorDropped(t, currentContext()); + return; } + + //there was no value, we can complete empty + this.done = true; + + /* implementation note on onError: + * if the source errored, we can propagate that directly since there + * was no chance for an inner subscriber to have been triggered + * (the source being a Mono). So we can just have the parent's behavior + * of calling actual.onError(t) for onError. + */ + this.actual.onError(t); } - void cancelInner() { - FilterWhenInner a = asyncFilter; - if (a != INNER_CANCELLED) { - a = ASYNC_FILTER.getAndSet(this, INNER_CANCELLED); - if (a != null && a != INNER_CANCELLED) { + @Override + public void request(long n) { + s.request(n); + } + + @Override + public void cancel() { + this.s.cancel(); + + final FilterWhenInner a = asyncFilter; + if (a != INNER_CANCELLED && a != INNER_TERMINATED && ASYNC_FILTER.compareAndSet(this, a, INNER_CANCELLED)) { + if (a != null) { a.cancel(); } } } - void innerResult(@Nullable Boolean item) { - if (item != null && item) { - //will reset the value with itself, but using parent's `value` saves a field - complete(this.value); + public boolean trySetInner(FilterWhenInner inner) { + final FilterWhenInner a = this.asyncFilter; + if (a == null && ASYNC_FILTER.compareAndSet(this, null, inner)) { + return true; } - else { - super.onComplete(); - discard(this.value); + Operators.onDiscard(inner.value, currentContext()); + return false; + } + + void innerResult(boolean item, FilterWhenInner inner) { + final FilterWhenInner a = this.asyncFilter; + if (a == inner && ASYNC_FILTER.compareAndSet(this, inner, INNER_TERMINATED)) { + if (item) { + //will reset the value with itself, but using parent's `value` saves a field + this.actual.onNext(inner.value); + this.actual.onComplete(); + } + else { + Operators.onDiscard(inner.value, currentContext()); + this.actual.onComplete(); + } } + // do nothing, value already discarded } - void innerError(Throwable ex) { + void innerError(Throwable ex, FilterWhenInner inner) { //if the inner subscriber (the filter one) errors, then we can //always propagate that error directly, as it means that the source Mono //was at least valued rather than in error. - super.onError(ex); - discard(this.value); + final FilterWhenInner a = this.asyncFilter; + if (a == inner && ASYNC_FILTER.compareAndSet(this, inner, INNER_TERMINATED)) { + Operators.onDiscard(inner.value, currentContext()); + this.actual.onError(ex); + } + + Operators.onErrorDropped(ex, currentContext()); + + // do nothing with value, value already discarded } @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) return upstream; - if (key == Attr.TERMINATED) return asyncFilter != null - ? asyncFilter.scanUnsafe(Attr.TERMINATED) - : super.scanUnsafe(Attr.TERMINATED); - if (key == RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.PARENT) return s; + if (key == Attr.PREFETCH) return 0; + if (key == Attr.TERMINATED) { + final FilterWhenInner af = asyncFilter; + return done && (af == null || af == INNER_TERMINATED); + } + if (key == Attr.CANCELLED) return asyncFilter == INNER_CANCELLED; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; //CANCELLED, PREFETCH - return super.scanUnsafe(key); + return InnerOperator.super.scanUnsafe(key); } @Override public Stream inners() { - FilterWhenInner c = asyncFilter; - return c == null ? Stream.empty() : Stream.of(c); + final FilterWhenInner c = asyncFilter; + return c == null || c == INNER_CANCELLED || c == INNER_TERMINATED ? Stream.empty() : Stream.of(c); + } + + @Override + public CoreSubscriber actual() { + return this.actual; + } + + @Override + public T poll() { + return null; + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + } } - static final class FilterWhenInner implements InnerConsumer { + static final class FilterWhenInner implements InnerConsumer { - final MonoFilterWhenMain main; + final MonoFilterWhenMain parent; /** should the filter publisher be cancelled once we received the first value? */ final boolean cancelOnNext; - boolean done; + final T value; - volatile Subscription sub; + boolean done; - static final AtomicReferenceFieldUpdater SUB = - AtomicReferenceFieldUpdater.newUpdater(FilterWhenInner.class, Subscription.class, "sub"); + Subscription s; - FilterWhenInner(MonoFilterWhenMain main, boolean cancelOnNext) { - this.main = main; + FilterWhenInner(MonoFilterWhenMain parent, boolean cancelOnNext, T value) { + this.parent = parent; this.cancelOnNext = cancelOnNext; + this.value = value; } @Override public void onSubscribe(Subscription s) { - if (Operators.setOnce(SUB, this, s)) { - s.request(Long.MAX_VALUE); + if (Operators.validate(this.s, s)) { + this.s = s; + if (this.parent.trySetInner(this)) { + s.request(Long.MAX_VALUE); + } else { + s.cancel(); + } } } @Override public void onNext(Boolean t) { - if (!done) { - if (cancelOnNext) { - sub.cancel(); - } - done = true; - main.innerResult(t); + if (done) { + return; } + + done = true; + + if (cancelOnNext) { + s.cancel(); + } + + parent.innerResult(t, this); } @Override public void onError(Throwable t) { - if (!done) { - done = true; - main.innerError(t); - } else { - Operators.onErrorDropped(t, main.currentContext()); + if (done) { + Operators.onErrorDropped(t, parent.currentContext()); + return; } - } - @Override - public Context currentContext() { - return main.currentContext(); + done = true; + parent.innerError(t, this); } @Override public void onComplete() { - if (!done) { - //the filter publisher was empty - done = true; - main.innerResult(null); //will trigger actual.onComplete() + if (done) { + return; } + + //the filter publisher was empty + done = true; + parent.innerResult(false, this); //will trigger actual.onComplete() } - void cancel() { - Operators.terminate(SUB, this); + @Override + public Context currentContext() { + return parent.currentContext(); } @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) return sub; - if (key == Attr.ACTUAL) return main; - if (key == Attr.CANCELLED) return sub == Operators.cancelledSubscription(); + if (key == Attr.PARENT) return s; + if (key == Attr.ACTUAL) return parent; if (key == Attr.TERMINATED) return done; - if (key == Attr.PREFETCH) return Integer.MAX_VALUE; + if (key == Attr.PREFETCH) return 0; if (key == Attr.REQUESTED_FROM_DOWNSTREAM) return done ? 0L : 1L; - if (key == RUN_STYLE) return SYNC; + if (key == Attr.RUN_STYLE) return SYNC; return null; } + + void cancel() { + this.s.cancel(); + } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMap.java index 8c5f048437..8d30a5d688 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,10 +54,7 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act return null; } - FlatMapMain manager = new FlatMapMain<>(actual, mapper); - actual.onSubscribe(manager); - - return manager; + return new FlatMapMain<>(actual, mapper); } @Override @@ -66,26 +63,28 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class FlatMapMain extends Operators.MonoSubscriber { + static final class FlatMapMain implements InnerOperator, + Fuseable, //for constants only + QueueSubscription { final Function> mapper; - final FlatMapInner second; + final CoreSubscriber actual; boolean done; - volatile Subscription s; + Subscription s; + + volatile FlatMapInner second; + @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater S = - AtomicReferenceFieldUpdater.newUpdater(FlatMapMain.class, - Subscription.class, - "s"); + static final AtomicReferenceFieldUpdater SECOND = + AtomicReferenceFieldUpdater.newUpdater(FlatMapMain.class, FlatMapInner.class, "second"); - FlatMapMain(CoreSubscriber subscriber, + FlatMapMain(CoreSubscriber actual, Function> mapper) { - super(subscriber); + this.actual = actual; this.mapper = mapper; - this.second = new FlatMapInner<>(this); } @Override @@ -93,21 +92,29 @@ public Stream inners() { return Stream.of(second); } + @Override + public CoreSubscriber actual() { + return this.actual; + } + @Override @Nullable public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return s; - if (key == Attr.CANCELLED) return s == Operators.cancelledSubscription(); + if (key == Attr.PREFETCH) return 0; + if (key == Attr.CANCELLED) return second == FlatMapInner.CANCELLED; if (key == Attr.TERMINATED) return done; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return super.scanUnsafe(key); + return InnerOperator.super.scanUnsafe(key); } @Override public void onSubscribe(Subscription s) { - if (Operators.setOnce(S, this, s)) { - s.request(Long.MAX_VALUE); + if (Operators.validate(this.s, s)) { + this.s = s; + + this.actual.onSubscribe(this); } } @@ -148,13 +155,14 @@ public void onNext(T t) { actual.onComplete(); } else { - complete(v); + actual.onNext(v); + actual.onComplete(); } return; } try { - m.subscribe(second); + m.subscribe(new FlatMapInner<>(this)); } catch (Throwable e) { actual.onError(Operators.onOperatorError(this, e, t, @@ -181,17 +189,63 @@ public void onComplete() { actual.onComplete(); } + @Override + public void request(long n) { + this.s.request(n); + } + @Override public void cancel() { - super.cancel(); - Operators.terminate(S, this); - second.cancel(); + this.s.cancel(); + + final FlatMapInner second = this.second; + if (second == FlatMapInner.CANCELLED || !SECOND.compareAndSet(this, second, FlatMapInner.CANCELLED)) { + return; + } + + if (second != null) { + second.s.cancel(); + } + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public R poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } + + boolean setSecond(FlatMapInner inner) { + return this.second == null && SECOND.compareAndSet(this, null, inner); } void secondError(Throwable ex) { actual.onError(ex); } + void secondComplete(R t) { + actual.onNext(t); + actual.onComplete(); + } + void secondComplete() { actual.onComplete(); } @@ -199,14 +253,11 @@ void secondComplete() { static final class FlatMapInner implements InnerConsumer { + static final FlatMapInner CANCELLED = new FlatMapInner<>(null); + final FlatMapMain parent; - volatile Subscription s; - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater S = - AtomicReferenceFieldUpdater.newUpdater(FlatMapInner.class, - Subscription.class, - "s"); + Subscription s; boolean done; @@ -214,7 +265,6 @@ static final class FlatMapInner implements InnerConsumer { this.parent = parent; } - @Override public Context currentContext() { return parent.currentContext(); @@ -234,8 +284,14 @@ public Object scanUnsafe(Attr key) { @Override public void onSubscribe(Subscription s) { - if (Operators.setOnce(S, this, s)) { - s.request(Long.MAX_VALUE); + if (Operators.validate(this.s, s)) { + this.s = s; + + if (this.parent.setSecond(this)) { + s.request(Long.MAX_VALUE); + } else { + s.cancel(); + } } } @@ -246,7 +302,7 @@ public void onNext(R t) { return; } done = true; - this.parent.complete(t); + this.parent.secondComplete(t); } @Override @@ -267,10 +323,5 @@ public void onComplete() { done = true; this.parent.secondComplete(); } - - void cancel() { - Operators.terminate(S, this); - } - } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoHasElement.java b/reactor-core/src/main/java/reactor/core/publisher/MonoHasElement.java index e631682829..371699587c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoHasElement.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoHasElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package reactor.core.publisher; - -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -34,6 +32,7 @@ final class MonoHasElement extends InternalMonoOperator implement @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + return super.scanUnsafe(key); } @@ -43,8 +42,9 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber - extends Operators.MonoSubscriber { - Subscription s; + extends Operators.BaseFluxToMonoOperator { + + boolean done; HasElementSubscriber(CoreSubscriber actual) { super(actual); @@ -53,41 +53,52 @@ static final class HasElementSubscriber @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) { - return s; - } - if (key == Attr.RUN_STYLE) { - return Attr.RunStyle.SYNC; - } + if (key == Attr.TERMINATED) return done; + return super.scanUnsafe(key); } @Override - public void cancel() { - super.cancel(); - s.cancel(); - } + public void onNext(T t) { + if (done) { + Operators.onNextDropped(t, currentContext()); + return; + } - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - actual.onSubscribe(this); + this.done = true; - s.request(Long.MAX_VALUE); - } + Operators.onDiscard(t, currentContext()); + + this.actual.onNext(true); + this.actual.onComplete(); } @Override - public void onNext(T t) { - //here we avoid the cancel because the source is assumed to be a Mono - complete(true); + public void onError(Throwable t) { + if (done) { + Operators.onErrorDropped(t, currentContext()); + return; + } + + this.done = true; + + this.actual.onError(t); } @Override public void onComplete() { - complete(false); + if (done) { + return; + } + + this.done = true; + + completePossiblyEmpty(); } + @Override + Boolean accumulatedValue() { + return false; + } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoHasElements.java b/reactor-core/src/main/java/reactor/core/publisher/MonoHasElements.java index 51318a8e5e..b689893f86 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoHasElements.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoHasElements.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package reactor.core.publisher; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -43,8 +42,9 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class HasElementsSubscriber extends Operators.MonoSubscriber { - Subscription s; + static final class HasElementsSubscriber extends Operators.BaseFluxToMonoOperator { + + boolean done; HasElementsSubscriber(CoreSubscriber actual) { super(actual); @@ -53,39 +53,49 @@ static final class HasElementsSubscriber extends Operators.MonoSubscriber extends Operators.MonoSubscriber { + static final class ReduceSubscriber implements InnerOperator, + Fuseable, + QueueSubscription { + + static final Object CANCELLED = new Object(); final BiFunction aggregator; + final CoreSubscriber actual; + + T aggregate; Subscription s; @@ -63,18 +70,25 @@ static final class ReduceSubscriber extends Operators.MonoSubscriber { ReduceSubscriber(CoreSubscriber actual, BiFunction aggregator) { - super(actual); + this.actual = actual; this.aggregator = aggregator; } + @Override + public CoreSubscriber actual() { + return this.actual; + } + @Override @Nullable public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return done; + if (key == Attr.CANCELLED) return !done && aggregate == CANCELLED; + if (key == Attr.PREFETCH) return 0; if (key == Attr.PARENT) return s; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return super.scanUnsafe(key); + return InnerOperator.super.scanUnsafe(key); } @Override @@ -82,7 +96,6 @@ public void onSubscribe(Subscription s) { if (Operators.validate(this.s, s)) { this.s = s; actual.onSubscribe(this); - s.request(Long.MAX_VALUE); } } @@ -92,27 +105,45 @@ public void onNext(T t) { Operators.onNextDropped(t, actual.currentContext()); return; } - T r = this.value; + + final T r = this.aggregate; + if (r == CANCELLED) { + Operators.onDiscard(t, actual.currentContext()); + return; + } + + // initial scenario when aggregate has nothing in it if (r == null) { - setValue(t); + synchronized (this) { + if (this.aggregate == null) { + this.aggregate = t; + return; + } + } + + Operators.onDiscard(t, actual.currentContext()); } else { try { - r = Objects.requireNonNull(aggregator.apply(r, t), - "The aggregator returned a null value"); + synchronized (this) { + if (this.aggregate != CANCELLED) { + this.aggregate = Objects.requireNonNull(aggregator.apply(r, t), "The aggregator returned a null value"); + return; + } + } + Operators.onDiscard(t, actual.currentContext()); } catch (Throwable ex) { done = true; Context ctx = actual.currentContext(); + synchronized (this) { + this.aggregate = null; + } Operators.onDiscard(t, ctx); - Operators.onDiscard(this.value, ctx); - this.value = null; + Operators.onDiscard(r, ctx); actual.onError(Operators.onOperatorError(s, ex, t, actual.currentContext())); - return; } - - setValue(r); } } @@ -123,8 +154,22 @@ public void onError(Throwable t) { return; } done = true; - discard(this.value); - this.value = null; + + final T r; + synchronized (this) { + r = this.aggregate; + this.aggregate = null; + } + + if (r == CANCELLED) { + Operators.onErrorDropped(t, actual.currentContext()); + return; + } + + if (r != null) { + Operators.onDiscard(r, actual.currentContext()); + } + actual.onError(t); } @@ -134,19 +179,72 @@ public void onComplete() { return; } done = true; - T r = this.value; - if (r != null) { - complete(r); + + final T r; + synchronized (this) { + r = this.aggregate; + this.aggregate = null; + } + + if (r == CANCELLED) { + return; + } + + if (r == null) { + actual.onComplete(); } else { + actual.onNext(r); actual.onComplete(); } } @Override public void cancel() { - super.cancel(); s.cancel(); + + final T r; + synchronized (this) { + r = this.aggregate; + //noinspection unchecked + this.aggregate = (T) CANCELLED; + } + + if (r == null || r == CANCELLED) { + return; + } + + Operators.onDiscard(r, actual.currentContext()); + } + + @Override + public void request(long n) { + s.request(Long.MAX_VALUE); + } + + @Override + public T poll() { + return null; + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoReduceSeed.java b/reactor-core/src/main/java/reactor/core/publisher/MonoReduceSeed.java index 654a137030..4f6053e61e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoReduceSeed.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoReduceSeed.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import java.util.function.BiFunction; import java.util.function.Supplier; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -63,83 +62,71 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class ReduceSeedSubscriber extends Operators.MonoSubscriber { + static final class ReduceSeedSubscriber extends Operators.BaseFluxToMonoOperator { final BiFunction accumulator; - Subscription s; + R seed; boolean done; ReduceSeedSubscriber(CoreSubscriber actual, BiFunction accumulator, - R value) { + R seed) { super(actual); this.accumulator = accumulator; - //noinspection deprecation - this.value = value; //setValue is made NO-OP in order to ignore redundant writes in base class + this.seed = seed; } @Override @Nullable public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return done; - if (key == Attr.PARENT) return s; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.CANCELLED) return !done && seed == null; return super.scanUnsafe(key); } @Override public void cancel() { - super.cancel(); s.cancel(); - } - - @Override - public void setValue(R value) { - // value is updated directly in onNext. writes from the base class are redundant. - // if cancel() happens before first reduction, the seed is visible from constructor and will be discarded. - // if there was some accumulation in progress post cancel, onNext will take care of it. - } - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); + final R seed; + synchronized (this) { + seed = this.seed; + this.seed = null; + } - s.request(Long.MAX_VALUE); + if (seed == null) { + return; } + + Operators.onDiscard(seed, actual.currentContext()); } @Override public void onNext(T t) { - R v = this.value; - R accumulated; - - if (v != null) { //value null when cancelled - try { - accumulated = Objects.requireNonNull(accumulator.apply(v, t), - "The accumulator returned a null value"); - - } - catch (Throwable e) { - onError(Operators.onOperatorError(this, e, t, actual.currentContext())); - return; - } - if (STATE.get(this) == CANCELLED) { - discard(accumulated); - this.value = null; - } - else { - //noinspection deprecation - this.value = accumulated; //setValue is made NO-OP in order to ignore redundant writes in base class + final R v; + final R accumulated; + + try { + synchronized (this) { + v = this.seed; + if (v != null) { + accumulated = Objects.requireNonNull(accumulator.apply(v, t), + "The accumulator returned a null value"); + this.seed = accumulated; + return; + } } - } else { + + // the actual seed is null, meaning cancelled, new state have to be + // discarded as well Operators.onDiscard(t, actual.currentContext()); } + catch (Throwable e) { + onError(Operators.onOperatorError(this.s, e, t, actual.currentContext())); + } } @Override @@ -149,8 +136,19 @@ public void onError(Throwable t) { return; } done = true; - discard(this.value); - this.value = null; + + final R seed; + synchronized (this) { + seed = this.seed; + this.seed = null; + } + + if (seed == null) { + Operators.onErrorDropped(t, actual.currentContext()); + return; + } + + Operators.onDiscard(seed, actual.currentContext()); actual.onError(t); } @@ -162,8 +160,17 @@ public void onComplete() { } done = true; - complete(this.value); - //we DON'T null out the value, complete will do that once there's been a request + completePossiblyEmpty(); + } + + @Override + R accumulatedValue() { + final R seed; + synchronized (this) { + seed = this.seed; + this.seed = null; + } + return seed; } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoStreamCollector.java b/reactor-core/src/main/java/reactor/core/publisher/MonoStreamCollector.java index 788cdc5b94..88f49e4e32 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoStreamCollector.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoStreamCollector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import java.util.function.Function; import java.util.stream.Collector; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -67,8 +66,7 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } - static final class StreamCollectorSubscriber - extends Operators.MonoSubscriber { + static final class StreamCollectorSubscriber extends Operators.BaseFluxToMonoOperator { final BiConsumer accumulator; @@ -76,8 +74,6 @@ static final class StreamCollectorSubscriber A container; //not final to be able to null it out on termination - Subscription s; - boolean done; StreamCollectorSubscriber(CoreSubscriber actual, @@ -94,13 +90,11 @@ static final class StreamCollectorSubscriber @Nullable public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return done; - if (key == Attr.PARENT) return s; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; return super.scanUnsafe(key); } - protected void discardIntermediateContainer(A a) { + void discardIntermediateContainer(A a) { Context ctx = actual.currentContext(); if (a instanceof Collection) { Operators.onDiscardMultiple((Collection) a, ctx); @@ -109,18 +103,6 @@ protected void discardIntermediateContainer(A a) { Operators.onDiscard(a, ctx); } } - //NB: value and thus discard are not used - - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); - - s.request(Long.MAX_VALUE); - } - } @Override public void onNext(T t) { @@ -129,7 +111,14 @@ public void onNext(T t) { return; } try { - accumulator.accept(container, t); + synchronized (this) { + final A container = this.container; + if (container != null) { + accumulator.accept(container, t); + return; + } + } + Operators.onDiscard(t, actual.currentContext()); } catch (Throwable ex) { Context ctx = actual.currentContext(); @@ -145,8 +134,18 @@ public void onError(Throwable t) { return; } done = true; - discardIntermediateContainer(container); - container = null; + final A c; + synchronized (this) { + c = container; + container = null; + } + + if (c == null) { + Operators.onErrorDropped(t, actual.currentContext()); + return; + } + + discardIntermediateContainer(c); actual.onError(t); } @@ -157,33 +156,50 @@ public void onComplete() { } done = true; - A a = container; - container = null; + completePossiblyEmpty(); + } - R r; + @Override + public void cancel() { + super.cancel(); + + final A c; + synchronized (this) { + c = container; + container = null; + } + if (c != null) { + discardIntermediateContainer(c); + } + } + + R accumulatedValue() { + final A c; + synchronized (this) { + c = container; + container = null; + } + + if (c == null) { + return null; + } + R r; try { - r = finisher.apply(a); + r = finisher.apply(c); } catch (Throwable ex) { - discardIntermediateContainer(a); + discardIntermediateContainer(c); actual.onError(Operators.onOperatorError(ex, actual.currentContext())); - return; + return null; } if (r == null) { - actual.onError(Operators.onOperatorError(new NullPointerException("Collector returned null"), actual.currentContext())); - return; + actual.onError(Operators.onOperatorError(new NullPointerException( + "Collector returned null"), actual.currentContext())); } - complete(r); - } - @Override - public void cancel() { - super.cancel(); - s.cancel(); - discardIntermediateContainer(container); - container = null; + return r; } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSupplier.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSupplier.java index 5441aa1aaa..ac0efa5999 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSupplier.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.Objects; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.function.Supplier; @@ -44,27 +45,7 @@ final class MonoSupplier @Override public void subscribe(CoreSubscriber actual) { - Operators.MonoSubscriber - sds = new Operators.MonoSubscriber<>(actual); - - actual.onSubscribe(sds); - - if (sds.isCancelled()) { - return; - } - - try { - T t = supplier.get(); - if (t == null) { - sds.onComplete(); - } - else { - sds.complete(t); - } - } - catch (Throwable e) { - actual.onError(Operators.onOperatorError(e, actual.currentContext())); - } + actual.onSubscribe(new MonoSupplierSubscription<>(actual, supplier)); } @Override @@ -91,4 +72,105 @@ public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; return null; } + + static class MonoSupplierSubscription + implements InnerProducer, Fuseable, QueueSubscription { + + final CoreSubscriber actual; + final Supplier supplier; + + boolean done; + + volatile int requestedOnce; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater REQUESTED_ONCE = + AtomicIntegerFieldUpdater.newUpdater(MonoSupplierSubscription.class, "requestedOnce"); + + volatile boolean cancelled; + + MonoSupplierSubscription(CoreSubscriber actual, Supplier callable) { + this.actual = actual; + this.supplier = callable; + } + + @Override + public CoreSubscriber actual() { + return this.actual; + } + + @Override + public T poll() { + if (this.done) { + return null; + } + + this.done = true; + + return this.supplier.get(); + } + + @Override + public void request(long n) { + if (this.cancelled) { + return; + } + + if (this.requestedOnce == 1 || !REQUESTED_ONCE.compareAndSet(this, 0 , 1)) { + return; + } + + final CoreSubscriber s = this.actual; + + final T value; + try { + value = this.supplier.get(); + } + catch (Exception e) { + if (this.cancelled) { + Operators.onErrorDropped(e, s.currentContext()); + return; + } + + s.onError(e); + return; + } + + + if (this.cancelled) { + Operators.onDiscard(value, s.currentContext()); + return; + } + + if (value != null) { + s.onNext(value); + } + + s.onComplete(); + } + + @Override + public void cancel() { + this.cancelled = true; + } + + @Override + public int requestFusion(int requestedMode) { + return requestedMode & SYNC; + } + + @Override + public int size() { + return this.done ? 0 : 1; + } + + @Override + public boolean isEmpty() { + return this.done; + } + + @Override + public void clear() { + this.done = true; + } + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTakeLastOne.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTakeLastOne.java index b14e4959c5..9894012e0d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTakeLastOne.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTakeLastOne.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.util.NoSuchElementException; import java.util.Objects; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -56,37 +55,29 @@ public Object scanUnsafe(Attr key) { } static final class TakeLastOneSubscriber - extends Operators.MonoSubscriber { + extends Operators.BaseFluxToMonoOperator { + + static final Object CANCELLED = new Object(); final boolean mustEmit; - final T defaultValue; - Subscription s; + + T value; + + boolean done; TakeLastOneSubscriber(CoreSubscriber actual, @Nullable T defaultValue, boolean mustEmit) { super(actual); - this.defaultValue = defaultValue; + this.value = defaultValue; this.mustEmit = mustEmit; } - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); - - s.request(Long.MAX_VALUE); - } - - } - @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) return s; - if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.TERMINATED) return done && value == null; + if (key == Attr.CANCELLED) return value == CANCELLED; return super.scanUnsafe(key); } @@ -94,36 +85,112 @@ public Object scanUnsafe(Attr key) { @Override public void onNext(T t) { T old = this.value; - setValue(t); + if (old == CANCELLED) { + // cancelled + Operators.onDiscard(t, actual.currentContext()); + return; + } + + synchronized (this) { + old = this.value; + if (old != CANCELLED) { + this.value = t; + } + } + + if (old == CANCELLED) { + // cancelled + Operators.onDiscard(t, actual.currentContext()); + return; + } + Operators.onDiscard(old, actual.currentContext()); //FIXME cache context } + @Override + public void onError(Throwable t) { + if (this.done) { + Operators.onErrorDropped(t, actual.currentContext()); + return; + } + this.done = true; + + + final T v; + synchronized (this) { + v = this.value; + this.value = null; + } + + if (v == CANCELLED) { + Operators.onErrorDropped(t, actual.currentContext()); + return; + } + + if (v != null) { + Operators.onDiscard(v, actual.currentContext()); + } + + this.actual.onError(t); + } + @Override public void onComplete() { - T v = this.value; + if (this.done) { + return; + } + this.done = true; + + final T v = this.value; + + if (v == CANCELLED) { + return; + } + if (v == null) { if (mustEmit) { - if(defaultValue != null){ - complete(defaultValue); - } - else { - actual.onError(Operators.onOperatorError(new NoSuchElementException( - "Flux#last() didn't observe any " + "onNext signal"), - actual.currentContext())); - } + actual.onError(Operators.onOperatorError(new NoSuchElementException( + "Flux#last() didn't observe any " + "onNext signal"), + actual.currentContext())); } else { actual.onComplete(); } return; } - complete(v); + + completePossiblyEmpty(); } @Override public void cancel() { - super.cancel(); s.cancel(); + + final T v; + synchronized (this) { + v = this.value; + //noinspection unchecked + this.value = (T) CANCELLED; + } + + if (v != null) { + Operators.onDiscard(v, actual.currentContext()); + } + } + + @Override + T accumulatedValue() { + final T v; + synchronized (this) { + v = this.value; + this.value = null; + } + + if (v == CANCELLED) { + return null; + } + + return v; } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoWhen.java index 2386f63516..0ea2aaae06 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package reactor.core.publisher; import java.util.Objects; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.stream.Stream; @@ -25,6 +25,7 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; +import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -94,9 +95,8 @@ public void subscribe(CoreSubscriber actual) { return; } - WhenCoordinator parent = new WhenCoordinator(actual, n, delayError); + WhenCoordinator parent = new WhenCoordinator(a, actual, n, delayError); actual.onSubscribe(parent); - parent.subscribe(a); } @Override @@ -107,22 +107,34 @@ public Object scanUnsafe(Attr key) { return null; } - static final class WhenCoordinator extends Operators.MonoSubscriber { + static final class WhenCoordinator implements InnerProducer, + Fuseable, + Fuseable.QueueSubscription { + final CoreSubscriber actual; + final Publisher[] sources; final WhenInner[] subscribers; final boolean delayError; - volatile int done; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater DONE = - AtomicIntegerFieldUpdater.newUpdater(WhenCoordinator.class, "done"); + volatile long state; + static final AtomicLongFieldUpdater STATE = + AtomicLongFieldUpdater.newUpdater(WhenCoordinator.class, "state"); - @SuppressWarnings("unchecked") - WhenCoordinator(CoreSubscriber subscriber, + static final long INTERRUPTED_FLAG = + 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long REQUESTED_ONCE_FLAG = + 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long MAX_SIGNALS_VALUE = + 0b0000_0000_0000_0000_0000_0000_0000_0000_0111_1111_1111_1111_1111_1111_1111_1111L; + + WhenCoordinator( + Publisher[] sources, + CoreSubscriber actual, int n, boolean delayError) { - super(subscriber); + this.sources = sources; + this.actual = actual; this.delayError = delayError; subscribers = new WhenInner[n]; for (int i = 0; i < n; i++) { @@ -130,23 +142,24 @@ static final class WhenCoordinator extends Operators.MonoSubscriber actual() { + return this.actual; + } + @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.TERMINATED) { - return done == subscribers.length; - } - if (key == Attr.BUFFERED) { - return subscribers.length; - } - if (key == Attr.DELAY_ERROR) { - return delayError; - } - if (key == Attr.RUN_STYLE) { - return Attr.RunStyle.SYNC; + if (key == Attr.TERMINATED) return deliveredSignals(this.state) == subscribers.length; + if (key == Attr.BUFFERED) return subscribers.length; + if (key == Attr.DELAY_ERROR) return delayError; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.CANCELLED) { + final long state = this.state; + return isInterrupted(state) && deliveredSignals(state) != subscribers.length; } - return super.scanUnsafe(key); + return InnerProducer.super.scanUnsafe(key); } @Override @@ -154,32 +167,18 @@ public Stream inners() { return Stream.of(subscribers); } - void subscribe(Publisher[] sources) { - WhenInner[] a = subscribers; - for (int i = 0; i < a.length; i++) { - sources[i].subscribe(a[i]); - } - } + boolean signal() { + final WhenInner[] a = subscribers; + int n = a.length; - void signalError(Throwable t) { - if (delayError) { - signal(); - } - else { - int n = subscribers.length; - if (DONE.getAndSet(this, n) != n) { - cancel(); - actual.onError(t); - } + final long previousState = markDeliveredSignal(this); + final int deliveredSignals = deliveredSignals(previousState); + if (isInterrupted(previousState) || deliveredSignals == n) { + return false; } - } - @SuppressWarnings("unchecked") - void signal() { - WhenInner[] a = subscribers; - int n = a.length; - if (DONE.incrementAndGet(this) != n) { - return; + if ((deliveredSignals + 1) != n) { + return true; } Throwable error = null; @@ -200,7 +199,6 @@ else if (error != null) { error = e; } } - } if (compositeError != null) { @@ -212,17 +210,143 @@ else if (error != null) { else { actual.onComplete(); } + + return true; + } + + @Override + public void request(long n) { + final long previousState = markRequestedOnce(this); + if (isRequestedOnce(previousState) || isInterrupted(previousState)) { + return; + } + + final Publisher[] sources = this.sources; + final WhenInner[] subs = this.subscribers; + for (int i = 0; i < subscribers.length; i++) { + sources[i].subscribe(subs[i]); + } } @Override public void cancel() { - if (!isCancelled()) { - super.cancel(); - for (WhenInner ms : subscribers) { + final long previousState = markInterrupted(this); + if (isInterrupted(previousState) || !isRequestedOnce(previousState) || deliveredSignals(previousState) == subscribers.length) { + return; + } + + for (WhenInner ms : subscribers) { + ms.cancel(); + } + } + + void cancelExcept(WhenInner source) { + for (WhenInner ms : subscribers) { + if (ms != source) { ms.cancel(); } } } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public Void poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } + + static long markRequestedOnce(WhenCoordinator instance) { + for (;;) { + final long state = instance.state; + + if (isInterrupted(state) || isRequestedOnce(state)) { + return state; + } + + final long nextState = state | REQUESTED_ONCE_FLAG; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static long markDeliveredSignal(WhenCoordinator instance) { + final int n = instance.subscribers.length; + for (;;) { + final long state = instance.state; + + if (isInterrupted(state) || n == deliveredSignals(state)) { + return state; + } + + final long nextState = state + 1; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static long markForceTerminated(WhenCoordinator instance) { + final int n = instance.subscribers.length; + for (;;) { + final long state = instance.state; + + if (isInterrupted(state) || n == deliveredSignals(state)) { + return state; + } + + final long nextState = (state &~ MAX_SIGNALS_VALUE) | n | INTERRUPTED_FLAG; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static long markInterrupted(WhenCoordinator instance) { + final int n = instance.subscribers.length; + for (;;) { + final long state = instance.state; + + if (isInterrupted(state) || n == deliveredSignals(state)) { + return state; + } + + final long nextState = state | INTERRUPTED_FLAG; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static boolean isRequestedOnce(long state) { + return (state & REQUESTED_ONCE_FLAG) == REQUESTED_ONCE_FLAG; + } + + static int deliveredSignals(long state) { + return (int) (state & Integer.MAX_VALUE); + } + + static boolean isInterrupted(long state) { + return (state & INTERRUPTED_FLAG) == INTERRUPTED_FLAG; + } } static final class WhenInner implements InnerConsumer { @@ -230,7 +354,6 @@ static final class WhenInner implements InnerConsumer { final WhenCoordinator parent; volatile Subscription s; - @SuppressWarnings("rawtypes") static final AtomicReferenceFieldUpdater S = AtomicReferenceFieldUpdater.newUpdater(WhenInner.class, Subscription.class, @@ -265,7 +388,7 @@ public Object scanUnsafe(Attr key) { @Override public Context currentContext() { - return parent.currentContext(); + return parent.actual.currentContext(); } @Override @@ -273,19 +396,30 @@ public void onSubscribe(Subscription s) { if (Operators.setOnce(S, this, s)) { s.request(Long.MAX_VALUE); } - else { - s.cancel(); - } } @Override public void onNext(Object t) { + Operators.onDiscard(t, currentContext()); } @Override public void onError(Throwable t) { error = t; - parent.signalError(t); + if (parent.delayError) { + if (!parent.signal()) { + Operators.onErrorDropped(t, parent.actual.currentContext()); + } + } + else { + final long previousState = WhenCoordinator.markForceTerminated(parent); + if (WhenCoordinator.isInterrupted(previousState)) { + return; + } + + parent.cancelExcept(this); + parent.actual.onError(t); + } } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoZip.java b/reactor-core/src/main/java/reactor/core/publisher/MonoZip.java index 443349f2d7..e0fc3ea1ce 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoZip.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoZip.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,16 @@ package reactor.core.publisher; import java.util.Objects; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; +import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -41,16 +41,16 @@ final class MonoZip extends Mono implements SourceProducer { final boolean delayError; - final Publisher[] sources; + final Mono[] sources; - final Iterable> sourcesIterable; + final Iterable> sourcesIterable; final Function zipper; @SuppressWarnings("unchecked") MonoZip(boolean delayError, - Publisher p1, - Publisher p2, + Mono p1, + Mono p2, BiFunction zipper2) { this(delayError, new FluxZip.PairwiseZipper<>(new BiFunction[]{ @@ -61,7 +61,7 @@ MonoZip(boolean delayError, MonoZip(boolean delayError, Function zipper, - Publisher... sources) { + Mono... sources) { this.delayError = delayError; this.zipper = Objects.requireNonNull(zipper, "zipper"); this.sources = Objects.requireNonNull(sources, "sources"); @@ -70,7 +70,7 @@ MonoZip(boolean delayError, MonoZip(boolean delayError, Function zipper, - Iterable> sourcesIterable) { + Iterable> sourcesIterable) { this.delayError = delayError; this.zipper = Objects.requireNonNull(zipper, "zipper"); this.sources = null; @@ -79,11 +79,11 @@ MonoZip(boolean delayError, @SuppressWarnings("unchecked") @Nullable - Mono zipAdditionalSource(Publisher source, BiFunction zipper) { - Publisher[] oldSources = sources; + Mono zipAdditionalSource(Mono source, BiFunction zipper) { + Mono[] oldSources = sources; if (oldSources != null && this.zipper instanceof FluxZip.PairwiseZipper) { int oldLen = oldSources.length; - Publisher[] newSources = new Publisher[oldLen + 1]; + Mono[] newSources = new Mono[oldLen + 1]; System.arraycopy(oldSources, 0, newSources, 0, oldLen); newSources[oldLen] = source; @@ -98,17 +98,17 @@ Mono zipAdditionalSource(Publisher source, BiFunction zipper) { @SuppressWarnings("unchecked") @Override public void subscribe(CoreSubscriber actual) { - Publisher[] a; + Mono[] a; int n = 0; if (sources != null) { a = sources; n = a.length; } else { - a = new Publisher[8]; - for (Publisher m : sourcesIterable) { + a = new Mono[8]; + for (Mono m : sourcesIterable) { if (n == a.length) { - Publisher[] b = new Publisher[n + (n >> 2)]; + Mono[] b = new Mono[n + (n >> 2)]; System.arraycopy(a, 0, b, 0, n); a = b; } @@ -121,12 +121,7 @@ public void subscribe(CoreSubscriber actual) { return; } - ZipCoordinator parent = new ZipCoordinator<>(actual, n, delayError, zipper); - actual.onSubscribe(parent); - ZipInner[] subs = parent.subscribers; - for (int i = 0; i < n; i++) { - a[i].subscribe(subs[i]); - } + actual.onSubscribe(new ZipCoordinator<>(a, actual, n, delayError, zipper)); } @Override @@ -136,50 +131,89 @@ public Object scanUnsafe(Attr key) { return null; } - static final class ZipCoordinator extends Operators.MonoSubscriber { + static final class ZipCoordinator implements InnerProducer, + Fuseable, + Fuseable.QueueSubscription { + + + final Mono[] sources; final ZipInner[] subscribers; + final CoreSubscriber actual; + final boolean delayError; final Function zipper; - volatile int done; + volatile long state; + @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater DONE = - AtomicIntegerFieldUpdater.newUpdater(ZipCoordinator.class, "done"); + static final AtomicLongFieldUpdater STATE = + AtomicLongFieldUpdater.newUpdater(ZipCoordinator.class, "state"); + + static final long INTERRUPTED_FLAG = + 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long REQUESTED_ONCE_FLAG = + 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long MAX_SIGNALS_VALUE = + 0b0000_0000_0000_0000_0000_0000_0000_0000_0111_1111_1111_1111_1111_1111_1111_1111L; @SuppressWarnings("unchecked") - ZipCoordinator(CoreSubscriber subscriber, + ZipCoordinator( + Mono[] sources, + CoreSubscriber subscriber, int n, boolean delayError, Function zipper) { - super(subscriber); + this.sources = sources; + this.actual = subscriber; this.delayError = delayError; this.zipper = zipper; - subscribers = new ZipInner[n]; + final ZipInner[] ss = new ZipInner[n]; + this.subscribers = ss; for (int i = 0; i < n; i++) { - subscribers[i] = new ZipInner<>(this); + ss[i] = new ZipInner<>(this); } } + @Override + public CoreSubscriber actual() { + return this.actual; + } + @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.TERMINATED) { - return done == subscribers.length; - } - if (key == Attr.BUFFERED) { - return subscribers.length; - } - if (key == Attr.DELAY_ERROR) { - return delayError; + if (key == Attr.TERMINATED) return deliveredSignals(this.state) == subscribers.length; + if (key == Attr.BUFFERED) return subscribers.length; + if (key == Attr.DELAY_ERROR) return delayError; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.CANCELLED) { + final long state = this.state; + return isInterrupted(state) && deliveredSignals(state) != subscribers.length; } - if (key == Attr.RUN_STYLE) { - return Attr.RunStyle.SYNC; + + return InnerProducer.super.scanUnsafe(key); + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public void request(long n) { + final long previousState = markRequestedOnce(this); + if (isRequestedOnce(previousState) || isInterrupted(previousState)) { + return; } - return super.scanUnsafe(key); + final Mono[] monos = this.sources; + final ZipInner[] subs = this.subscribers; + for (int i = 0; i < subscribers.length; i++) { + monos[i].subscribe(subs[i]); + } } @Override @@ -187,12 +221,18 @@ public Stream inners() { return Stream.of(subscribers); } - @SuppressWarnings("unchecked") - void signal() { + boolean signal() { ZipInner[] a = subscribers; int n = a.length; - if (DONE.incrementAndGet(this) != n) { - return; + + final long previousState = markDeliveredSignal(this); + final int deliveredSignals = deliveredSignals(previousState); + if (isInterrupted(previousState) || deliveredSignals == n) { + return false; + } + + if ((deliveredSignals + 1) != n) { + return true; } Object[] o = new Object[n]; @@ -246,32 +286,133 @@ else if (hasEmpty) { t, o, actual.currentContext())); - return; + return true; } - complete(r); + actual.onNext(r); + actual.onComplete(); } + + return true; } @Override public void cancel() { - if (!isCancelled()) { - super.cancel(); - for (ZipInner ms : subscribers) { - ms.cancel(); + final long previousState = markInterrupted(this); + if (isInterrupted(previousState) || !isRequestedOnce(previousState) || deliveredSignals(previousState) == subscribers.length) { + return; + } + + final Context context = actual.currentContext(); + for (ZipInner ms : subscribers) { + if (ms.cancel()) { + Operators.onDiscard(ms.value, context); } } } void cancelExcept(ZipInner source) { - if (!isCancelled()) { - super.cancel(); - for (ZipInner ms : subscribers) { - if(ms != source) { - ms.cancel(); - } + final Context context = actual.currentContext(); + for (ZipInner ms : subscribers) { + if (ms != source && ms.cancel()) { + Operators.onDiscard(ms.value, context); } } } + + @Override + public R poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } + + static long markRequestedOnce(ZipCoordinator instance) { + for (;;) { + final long state = instance.state; + + if (isInterrupted(state) || isRequestedOnce(state)) { + return state; + } + + final long nextState = state | REQUESTED_ONCE_FLAG; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static long markDeliveredSignal(ZipCoordinator instance) { + final int n = instance.subscribers.length; + for (;;) { + final long state = instance.state; + + if (isInterrupted(state) || n == deliveredSignals(state)) { + return state; + } + + final long nextState = state + 1; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static long markForceTerminated(ZipCoordinator instance) { + final int n = instance.subscribers.length; + for (;;) { + final long state = instance.state; + + if (isInterrupted(state) || n == deliveredSignals(state)) { + return state; + } + + final long nextState = (state &~ MAX_SIGNALS_VALUE) | n | INTERRUPTED_FLAG; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static long markInterrupted(ZipCoordinator instance) { + final int n = instance.subscribers.length; + for (;;) { + final long state = instance.state; + + if (isInterrupted(state) || n == deliveredSignals(state)) { + return state; + } + + final long nextState = state | INTERRUPTED_FLAG; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static boolean isRequestedOnce(long state) { + return (state & REQUESTED_ONCE_FLAG) == REQUESTED_ONCE_FLAG; + } + + static int deliveredSignals(long state) { + return (int) (state & Integer.MAX_VALUE); + } + + static boolean isInterrupted(long state) { + return (state & INTERRUPTED_FLAG) == INTERRUPTED_FLAG; + } } static final class ZipInner implements InnerConsumer { @@ -281,9 +422,7 @@ static final class ZipInner implements InnerConsumer { volatile Subscription s; @SuppressWarnings("rawtypes") static final AtomicReferenceFieldUpdater S = - AtomicReferenceFieldUpdater.newUpdater(ZipInner.class, - Subscription.class, - "s"); + AtomicReferenceFieldUpdater.newUpdater(ZipInner.class, Subscription.class, "s"); Object value; Throwable error; @@ -316,7 +455,7 @@ public Object scanUnsafe(Attr key) { @Override public Context currentContext() { - return parent.currentContext(); + return parent.actual.currentContext(); } @Override @@ -324,9 +463,6 @@ public void onSubscribe(Subscription s) { if (Operators.setOnce(S, this, s)) { s.request(Long.MAX_VALUE); } - else { - s.cancel(); - } } @Override @@ -334,42 +470,76 @@ public void onNext(Object t) { if (value == null) { value = t; parent.signal(); + /* + We use cancelledSubscription() as a marker to detect whether a value is present in the cancelAll/cancelExcept parent phase. + This is done in order to deal with a single volatile. + Of course, it could also be set that way due to an early cancellation (in which case we very much want to discard the t). + Note the accumulator should already have seen and used the value in parent.signal(). + We don't really cancel the subscription, but we don't really care because that's a Mono. Having received onNext, we're sure + to get onComplete afterwards. + See also boolean cancel(). + */ + final Subscription a = this.s; + if (a != Operators.cancelledSubscription() && S.compareAndSet(this, a, Operators.cancelledSubscription())) { + return; + } + Operators.onDiscard(t, parent.actual.currentContext()); } } @Override public void onError(Throwable t) { + if (value != null) { + Operators.onErrorDropped(t, parent.actual.currentContext()); + return; + } + error = t; if (parent.delayError) { - parent.signal(); + if (!parent.signal()) { + Operators.onErrorDropped(t, parent.actual.currentContext()); + } } else { - int n = parent.subscribers.length; - if (ZipCoordinator.DONE.getAndSet(parent, n) != n) { - parent.cancelExcept(this); - parent.actual.onError(t); + final long previousState = ZipCoordinator.markForceTerminated(parent); + if (ZipCoordinator.isInterrupted(previousState)) { + return; } + + parent.cancelExcept(this); + parent.actual.onError(t); } } @Override public void onComplete() { - if (value == null) { - if (parent.delayError) { - parent.signal(); - } - else { - int n = parent.subscribers.length; - if (ZipCoordinator.DONE.getAndSet(parent, n) != n) { - parent.cancelExcept(this); - parent.actual.onComplete(); - } + if (value != null) { + return; + } + + if (parent.delayError) { + parent.signal(); + } + else { + final long previousState = ZipCoordinator.markForceTerminated(parent); + if (ZipCoordinator.isInterrupted(previousState)) { + return; } + + parent.cancelExcept(this); + parent.actual.onComplete(); } } - void cancel() { - Operators.terminate(S, this); + boolean cancel() { + /* + If S == cancelledSubscription, it means we've either already cancelled (nothing to do) or previously signalled an onNext. + In both cases, terminate will return false (having failed to swap to cancelledSubscription) and the method will return true. + This is to be interpreted by parent callers (cancelAll/cancelExcept) as an indicator that a value is likely present, + and that it should be discarded by the parent. Parent could try to discard twice, in the case of double cancellation, but + discard should be idempotent. + */ + return !Operators.terminate(S, this); } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Operators.java b/reactor-core/src/main/java/reactor/core/publisher/Operators.java index 18ec9c4979..9873dcd636 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Operators.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Operators.java @@ -1114,7 +1114,7 @@ public static Subscription scalarSubscription(CoreSubscriber subs T value, String stepName){ return new ScalarSubscription<>(subscriber, value, stepName); } - + /** * Safely gate a {@link Subscriber} by making sure onNext signals are delivered * sequentially (serialized). @@ -1982,6 +1982,151 @@ public int size() { } + static abstract class BaseFluxToMonoOperator implements InnerOperator, + Fuseable, + QueueSubscription { + final CoreSubscriber actual; + + Subscription s; + + boolean hasRequest; + + volatile int state; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater STATE = + AtomicIntegerFieldUpdater.newUpdater(BaseFluxToMonoOperator.class, "state"); + + BaseFluxToMonoOperator(CoreSubscriber actual) { + this.actual = actual; + } + + @Override + @Nullable + public Object scanUnsafe(Scannable.Attr key) { + if (key == Scannable.Attr.PREFETCH) return 0; + if (key == Scannable.Attr.PARENT) return s; + if (key == Scannable.Attr.RUN_STYLE) return Scannable.Attr.RunStyle.SYNC; + + return InnerOperator.super.scanUnsafe(key); + } + + @Override + public final CoreSubscriber actual() { + return this.actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + + actual.onSubscribe(this); + } + } + + @Override + public void request(long n) { + if (!hasRequest) { + hasRequest = true; + + final int state = this.state; + if ((state & 1) == 1) { + return; + } + + if (STATE.compareAndSet(this, state, state | 1)) { + if (state == 0) { + s.request(Long.MAX_VALUE); + } + else { + // completed before request means source was empty + final O value = accumulatedValue(); + + if (value == null) { + return; + } + + this.actual.onNext(value); + this.actual.onComplete(); + } + } + } + } + + @Override + public void cancel() { + s.cancel(); + } + + final void completePossiblyEmpty() { + if (hasRequest) { + final O value = accumulatedValue(); + + if (value == null) { + return; + } + + this.actual.onNext(value); + this.actual.onComplete(); + return; + } + + final int state = this.state; + if (state == 0 && STATE.compareAndSet(this, 0, 2)) { + return; + } + + final O value = accumulatedValue(); + + if (value == null) { + return; + } + + this.actual.onNext(value); + this.actual.onComplete(); + } + + /** + * This method is being called either during onComplete invocation in case request + * has happened before, or during request invocation in case onComplete with no + * values has happened before. + *

    + * Note, this method expectedly returns null if cancellation happened + * before + *

    + * + * @return accumulated/default value or null if cancelled before + */ + @Nullable + abstract O accumulatedValue(); + + @Override + public final I poll() { + return null; + } + + @Override + public final int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public final int size() { + return 0; + } + + @Override + public final boolean isEmpty() { + return true; + } + + @Override + public final void clear() { + + } + } + + /** * A subscription implementation that arbitrates request amounts between subsequent Subscriptions, including the * duration until the first Subscription is set. @@ -2284,7 +2429,7 @@ final void drainLoop() { } } else if (mr != 0L && a != null) { requestAmount = addCap(requestAmount, mr); - alreadyInRequestAmount += mr; + alreadyInRequestAmount += mr; requestTarget = a; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelCollect.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelCollect.java index fc4ca560c4..ff385e3a18 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelCollect.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelCollect.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.util.function.Supplier; import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.core.Scannable; @@ -107,14 +106,12 @@ public int parallelism() { } static final class ParallelCollectSubscriber - extends Operators.MonoSubscriber { + extends Operators.BaseFluxToMonoOperator { final BiConsumer collector; C collection; - Subscription s; - boolean done; ParallelCollectSubscriber(CoreSubscriber subscriber, @@ -125,17 +122,6 @@ static final class ParallelCollectSubscriber this.collector = collector; } - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); - - s.request(Long.MAX_VALUE); - } - } - @Override public void onNext(T t) { if (done) { @@ -144,10 +130,15 @@ public void onNext(T t) { } try { - collector.accept(collection, t); + synchronized (this) { + final C collection = this.collection; + if (collection != null) { + collector.accept(collection, t); + } + } } catch (Throwable ex) { - onError(Operators.onOperatorError(this, ex, t, actual.currentContext())); + onError(Operators.onOperatorError(this.s, ex, t, actual.currentContext())); } } @@ -158,7 +149,19 @@ public void onError(Throwable t) { return; } done = true; - collection = null; + + final C c; + synchronized (this) { + c = collection; + collection = null; + } + + if (c == null) { + return; + } + + Operators.onDiscard(c, actual.currentContext()); + actual.onError(t); } @@ -168,20 +171,40 @@ public void onComplete() { return; } done = true; - C c = collection; - collection = null; - complete(c); + + completePossiblyEmpty(); } @Override public void cancel() { - super.cancel(); s.cancel(); + + final C c; + synchronized (this) { + c = collection; + collection = null; + } + + if (c != null) { + Operators.onDiscard(c, actual.currentContext()); + } + } + + @Override + C accumulatedValue() { + final C c; + synchronized (this) { + c = collection; + collection = null; + } + return c; } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.TERMINATED) return done; + if (key == Attr.CANCELLED) return collection == null && !done; return super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeReduce.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeReduce.java index 6da97bdc5c..a4ea01aa5d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeReduce.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeReduce.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,24 +58,28 @@ public Object scanUnsafe(Attr key) { @Override public void subscribe(CoreSubscriber actual) { MergeReduceMain parent = - new MergeReduceMain<>(actual, source.parallelism(), reducer); + new MergeReduceMain<>(source, actual, source.parallelism(), reducer); actual.onSubscribe(parent); - - source.subscribe(parent.subscribers); } static final class MergeReduceMain - extends Operators.MonoSubscriber { + implements InnerProducer, + Fuseable, + QueueSubscription { + + final ParallelFlux source; + + final CoreSubscriber actual; final MergeReduceInner[] subscribers; final BiFunction reducer; volatile SlotPair current; + @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater - CURRENT = AtomicReferenceFieldUpdater.newUpdater( - MergeReduceMain.class, + static final AtomicReferenceFieldUpdater CURRENT = + AtomicReferenceFieldUpdater.newUpdater(MergeReduceMain.class, SlotPair.class, "current"); @@ -94,10 +98,13 @@ static final class MergeReduceMain Throwable.class, "error"); - MergeReduceMain(CoreSubscriber subscriber, + MergeReduceMain( + ParallelFlux source, + CoreSubscriber actual, int n, BiFunction reducer) { - super(subscriber); + this.actual = actual; + this.source = source; @SuppressWarnings("unchecked") MergeReduceInner[] a = new MergeReduceInner[n]; for (int i = 0; i < n; i++) { @@ -105,17 +112,23 @@ static final class MergeReduceMain } this.subscribers = a; this.reducer = reducer; - REMAINING.lazySet(this, n); + REMAINING.lazySet(this, n | Integer.MIN_VALUE); + } + + @Override + public CoreSubscriber actual() { + return actual; } @Override @Nullable public Object scanUnsafe(Attr key) { if (key == Attr.ERROR) return error; - if (key == Attr.TERMINATED) return REMAINING.get(this) == 0; + if (key == Attr.TERMINATED) return this.remaining == 0; + if (key == Attr.CANCELLED) return this.remaining == Integer.MIN_VALUE; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return super.scanUnsafe(key); + return InnerProducer.super.scanUnsafe(key); } @Nullable @@ -152,10 +165,24 @@ SlotPair addValue(T value) { @Override public void cancel() { - for (MergeReduceInner inner : subscribers) { - inner.cancel(); + int r = REMAINING.getAndSet(this, Integer.MIN_VALUE); + if ((r & Integer.MIN_VALUE) != Integer.MIN_VALUE) { + for (MergeReduceInner inner : subscribers) { + inner.cancel(); + } + } + } + + @Override + public void request(long n) { + final int r = this.remaining; + if ((r & Integer.MIN_VALUE) != Integer.MIN_VALUE) { + return; + } + + if (REMAINING.compareAndSet(this, r, r & Integer.MAX_VALUE)) { + source.subscribe(subscribers); } - super.cancel(); } void innerError(Throwable ex) { @@ -191,18 +218,56 @@ void innerComplete(@Nullable T value) { } } - if (REMAINING.decrementAndGet(this) == 0) { - SlotPair sp = current; + if (decrementAndGet(this) == 0) { + final SlotPair sp = current; CURRENT.lazySet(this, null); if (sp != null) { - complete(sp.first); + actual.onNext(sp.first); + actual.onComplete(); } else { actual.onComplete(); } } } + + static int decrementAndGet(MergeReduceMain instance) { + int prev, next; + do { + prev = instance.remaining; + if (prev == Integer.MIN_VALUE) { + return Integer.MIN_VALUE; + } + next = prev - 1; + } while (!REMAINING.compareAndSet(instance, prev, next)); + return next; + } + + @Override + public T poll() { + return null; + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } } static final class MergeReduceInner implements InnerConsumer { @@ -231,7 +296,7 @@ static final class MergeReduceInner implements InnerConsumer { @Override public Context currentContext() { - return parent.currentContext(); + return parent.actual.currentContext(); } @Override @@ -283,7 +348,7 @@ public void onNext(T t) { @Override public void onError(Throwable t) { if (done) { - Operators.onErrorDropped(t, parent.currentContext()); + Operators.onErrorDropped(t, parent.actual.currentContext()); return; } done = true; diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelReduceSeed.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelReduceSeed.java index 0e5982a10e..131f00b263 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelReduceSeed.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelReduceSeed.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.util.function.Supplier; import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.core.Scannable; @@ -107,14 +106,12 @@ public int parallelism() { static final class ParallelReduceSeedSubscriber - extends Operators.MonoSubscriber { + extends Operators.BaseFluxToMonoOperator { final BiFunction reducer; R accumulator; - Subscription s; - boolean done; ParallelReduceSeedSubscriber(CoreSubscriber subscriber, @@ -125,17 +122,6 @@ static final class ParallelReduceSeedSubscriber this.reducer = reducer; } - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - - actual.onSubscribe(this); - - s.request(Long.MAX_VALUE); - } - } - @Override public void onNext(T t) { if (done) { @@ -143,17 +129,23 @@ public void onNext(T t) { return; } - R v; - - try { - v = Objects.requireNonNull(reducer.apply(accumulator, t), "The reducer returned a null value"); - } - catch (Throwable ex) { - onError(Operators.onOperatorError(this, ex, t, actual.currentContext())); - return; + synchronized (this) { + R v; + try { + if (accumulator == null) { + return; + } + + v = Objects.requireNonNull(reducer.apply(accumulator, t), + "The reducer returned a null value"); + } + catch (Throwable ex) { + onError(Operators.onOperatorError(this.s, ex, t, actual.currentContext())); + return; + } + + accumulator = v; } - - accumulator = v; } @Override @@ -163,7 +155,21 @@ public void onError(Throwable t) { return; } done = true; - accumulator = null; + + final R a; + synchronized (this) { + a = accumulator; + if (a != null) { + accumulator = null; + } + } + + if (a == null) { + return; + } + + Operators.onDiscard(a, currentContext()); + actual.onError(t); } @@ -174,20 +180,45 @@ public void onComplete() { } done = true; - R a = accumulator; - accumulator = null; - complete(a); + completePossiblyEmpty(); + } + + @Override + R accumulatedValue() { + final R a; + synchronized (this) { + a = accumulator; + if (a != null) { + accumulator = null; + } + } + return a; } @Override public void cancel() { - super.cancel(); s.cancel(); + + final R a; + synchronized (this) { + a = accumulator; + if (a != null) { + accumulator = null; + } + } + + if (a == null) { + return; + } + + Operators.onDiscard(a, currentContext()); } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == Attr.TERMINATED) return done; + if (key == Attr.CANCELLED) return !done && accumulator == null; return super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelThen.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelThen.java index 8fb28ff33a..f8470b3312 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelThen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelThen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package reactor.core.publisher; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Subscription; @@ -48,75 +48,201 @@ public Object scanUnsafe(Attr key) { @Override public void subscribe(CoreSubscriber actual) { - ThenMain parent = new ThenMain(actual, source.parallelism()); + ThenMain parent = new ThenMain(actual, source); actual.onSubscribe(parent); - - source.subscribe(parent.subscribers); } - static final class ThenMain - extends Operators.MonoSubscriber { + static final class ThenMain implements InnerProducer, + Fuseable, //for constants only + QueueSubscription { final ThenInner[] subscribers; + final CoreSubscriber actual; + final ParallelFlux source; - volatile int remaining; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater - REMAINING = AtomicIntegerFieldUpdater.newUpdater( - ThenMain.class, - "remaining"); + volatile long state; - volatile Throwable error; @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater - ERROR = AtomicReferenceFieldUpdater.newUpdater( - ThenMain.class, - Throwable.class, - "error"); - - ThenMain(CoreSubscriber subscriber, int n) { - super(subscriber); + static final AtomicLongFieldUpdater STATE = + AtomicLongFieldUpdater.newUpdater(ThenMain.class, "state"); + + + static final long CANCELLED_FLAG = + 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long REQUESTED_FLAG = + 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + + static final long INNER_COMPLETED_MAX = + 0b0000_0000_0000_0000_0000_0000_0000_0000_0111_1111_1111_1111_1111_1111_1111_1111L; + + ThenMain(CoreSubscriber actual, final ParallelFlux source) { + this.actual = actual; + this.source = source; + + final int n = source.parallelism(); ThenInner[] a = new ThenInner[n]; for (int i = 0; i < n; i++) { a[i] = new ThenInner(this); } this.subscribers = a; - REMAINING.lazySet(this, n); + } + + @Override + public CoreSubscriber actual() { + return this.actual; } @Override @Nullable public Object scanUnsafe(Attr key) { - if (key == Attr.ERROR) return error; - if (key == Attr.TERMINATED) return REMAINING.get(this) == 0; + if (key == Attr.TERMINATED) return innersCompletedCount(this.state) == source.parallelism(); + if (key == Attr.CANCELLED) return isCancelled(this.state) && innersCompletedCount(this.state) != source.parallelism(); + if (key == Attr.PREFETCH) return 0; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return super.scanUnsafe(key); + return InnerProducer.super.scanUnsafe(key); } @Override public void cancel() { + final long previousState = markCancelled(this); + + if (isCancelled(previousState) || !isRequestedOnce(previousState)) { + return; + } + for (ThenInner inner : subscribers) { inner.cancel(); } - super.cancel(); } - void innerError(Throwable ex) { - if(ERROR.compareAndSet(this, null, ex)){ - cancel(); - actual.onError(ex); + @Override + public void request(long n) { + if (!STATE.compareAndSet(this, 0, REQUESTED_FLAG)) { + return; } - else if(error != ex) { - Operators.onErrorDropped(ex, actual.currentContext()); + + source.subscribe(this.subscribers); + } + + void innerError(Throwable ex, ThenInner innerCaller) { + final long previousState = markForceTerminated(this); + + final int n = this.source.parallelism(); + + if (isCancelled(previousState) || innersCompletedCount(previousState) == n) { + return; + } + + for (ThenInner inner : subscribers) { + if (inner != innerCaller) { + inner.cancel(); + } } + + actual.onError(ex); } void innerComplete() { - if (REMAINING.decrementAndGet(this) == 0) { + final long previousState = markInnerCompleted(this); + + final int n = this.source.parallelism(); + final int innersCompletedCount = innersCompletedCount(previousState); + + if (isCancelled(previousState) || innersCompletedCount == n) { + return; + } + + if ((innersCompletedCount + 1) == n) { actual.onComplete(); } } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public Void poll() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + + } + + static long markForceTerminated(ThenMain instance) { + final int n = instance.source.parallelism(); + for (;;) { + final long state = instance.state; + + if (isCancelled(state) || innersCompletedCount(state) == n) { + return state; + } + + final long nextState = (state & ~INNER_COMPLETED_MAX) | CANCELLED_FLAG | n; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static boolean isRequestedOnce(long state) { + return (state & REQUESTED_FLAG) == REQUESTED_FLAG; + } + + static long markCancelled(ThenMain instance) { + final int n = instance.source.parallelism(); + for(;;) { + final long state = instance.state; + + if (isCancelled(state) || innersCompletedCount(state) == n) { + return state; + } + + final long nextState = state | CANCELLED_FLAG; + if (STATE.weakCompareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static boolean isCancelled(long state) { + return (state & CANCELLED_FLAG) == CANCELLED_FLAG; + } + + static int innersCompletedCount(long state) { + return (int) (state & INNER_COMPLETED_MAX); + } + + static long markInnerCompleted(ThenMain instance) { + final int n = instance.source.parallelism(); + for (;;) { + final long state = instance.state; + + if (isCancelled(state) || innersCompletedCount(state) == n) { + return state; + } + + final long nextState = state + 1; + if (STATE.compareAndSet(instance, state, nextState)) { + return state; + } + } + } + } static final class ThenInner implements InnerConsumer { @@ -137,7 +263,7 @@ static final class ThenInner implements InnerConsumer { @Override public Context currentContext() { - return parent.currentContext(); + return parent.actual.currentContext(); } @Override @@ -162,12 +288,12 @@ public void onSubscribe(Subscription s) { @Override public void onNext(Object t) { //ignored - Operators.onDiscard(t, parent.currentContext()); + Operators.onDiscard(t, parent.actual.currentContext()); } @Override public void onError(Throwable t) { - parent.innerError(t); + parent.innerError(t, this); } @Override diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDefaultIfEmptyTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDefaultIfEmptyTest.java index db506473b2..0abfc6eddc 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDefaultIfEmptyTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDefaultIfEmptyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -190,7 +190,7 @@ public void scanSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -201,17 +201,4 @@ public void scanSubscriber() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); } - @Test - public void scanSubscriberCancelled() { - CoreSubscriber actual = new LambdaSubscriber<>(null, e -> {}, null, null); - FluxDefaultIfEmpty.DefaultIfEmptySubscriber test = - new FluxDefaultIfEmpty.DefaultIfEmptySubscriber<>(actual, "bar"); - Subscription parent = Operators.emptySubscription(); - test.onSubscribe(parent); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); - } - } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDetachTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDetachTest.java index 0349b351a3..d37b474bb0 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDetachTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDetachTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -114,7 +114,7 @@ public void backpressured() throws Exception { .onTerminateDetach() .subscribe(ts); - ts.assertNoValues(); + ts.assertNoValues(); ts.request(1); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDoOnEachTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDoOnEachTest.java index 241dcbec0b..729b76bbfc 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDoOnEachTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDoOnEachTest.java @@ -52,25 +52,6 @@ public class FluxDoOnEachTest { - - // see https://github.com/reactor/reactor-core/issues/3044 - @Test - void doOnEachAsyncFusionDoesntTriggerOnNextTwice() { - List signals = new ArrayList<>(); - StepVerifier.create(Flux.just("a", "b", "c") - .collectList() - .doOnEach(sig -> signals.add(sig.toString())) - ) - .expectFusion(Fuseable.ASYNC) - .expectNext(Arrays.asList("a", "b", "c")) - .verifyComplete(); - - assertThat(signals).containsExactly( - "doOnEach_onNext([a, b, c])", - "onComplete()" - ); - } - @Test public void nullSource() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoAllTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoAllTest.java index 1ebe03e65c..d0c734e2c6 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoAllTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoAllTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -125,7 +125,7 @@ public void scanSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -134,10 +134,6 @@ public void scanSubscriber() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoAnyTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoAnyTest.java index deab76e11f..3c2a6bc7c8 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoAnyTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoAnyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -195,16 +195,12 @@ public void scanSubscriber() { assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoCallableTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoCallableTest.java index 36fb07f964..594488e328 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoCallableTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoCallableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; +import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.test.StepVerifier; import reactor.test.subscriber.AssertSubscriber; @@ -54,8 +55,8 @@ public void callableReturnsNull() { } @Test - public void callableReturnsNullShortcircuitsBackpressure() { - AssertSubscriber ts = AssertSubscriber.create(0); + public void callableReturnsNullBackpressure() { + AssertSubscriber ts = AssertSubscriber.create(1); Mono.fromCallable(() -> null).subscribe(ts); @@ -64,6 +65,18 @@ public void callableReturnsNullShortcircuitsBackpressure() { .assertComplete(); } + @Test + public void callableReturnsNullShortcircuitsBackpressureSyncFusion() { + AssertSubscriber ts = AssertSubscriber.create(0) + .requestedFusionMode(Fuseable.SYNC); + + Mono.fromCallable(() -> null).subscribe(ts); + + ts.assertNoValues() + .assertNoError() + .assertComplete(); + } + @Test public void normal() { AssertSubscriber ts = AssertSubscriber.create(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoCollectListTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoCollectListTest.java index 8247b334c7..bd211fd77e 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoCollectListTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoCollectListTest.java @@ -177,7 +177,7 @@ public void scanBufferAllSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -186,10 +186,6 @@ public void scanBufferAllSubscriber() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoCollectTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoCollectTest.java index ce226a7b91..b800ccceef 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoCollectTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoCollectTest.java @@ -180,7 +180,7 @@ public void scanSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -189,10 +189,6 @@ public void scanSubscriber() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test @@ -221,23 +217,6 @@ public void discardElementAndBufferOnAccumulatorLateFailure() { .hasDiscardedExactly(1, 2, 3); } - @Test - public void discardElementAndBufferOnAccumulatorLateFailure_fused() { - Flux.just(1, 2, 3, 4) - .collect(ArrayList::new, (l, t) -> { - if (t == 3) { - throw new IllegalStateException("accumulator: boom"); - } - l.add(t); - }) - .as(StepVerifier::create) - //WARNING: we need to request fusion so this expectFusion is important - .expectFusion(Fuseable.ASYNC) - .expectErrorMessage("accumulator: boom") - .verifyThenAssertThat() - .hasDiscardedExactly(1, 2, 3); - } - @Test public void discardListElementsOnError() { Mono> test = diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoCompletionStageTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoCompletionStageTest.java index 825372b6d1..819ced9ede 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoCompletionStageTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoCompletionStageTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,35 @@ public void cancelThenFutureFails() { }) .thenCancel()//already cancelled but need to get to verification .verifyThenAssertThat() + .hasNotDroppedErrors(); + + assertThat(future).isCancelled(); + } + + @Test + public void cancelThenFutureFails1() { + CompletableFuture future = new CompletableFuture() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + // noops + return false; + } + }; + AtomicReference subRef = new AtomicReference<>(); + + Mono mono = Mono + .fromFuture(future) + .doOnSubscribe(subRef::set); + + StepVerifier.create(mono) + .expectSubscription() + .then(() -> { + subRef.get().cancel(); + future.completeExceptionally(new IllegalStateException("boom")); + future.complete(1); + }) + .thenCancel()//already cancelled but need to get to verification + .verifyThenAssertThat() .hasDroppedErrorWithMessage("boom"); } diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoCountTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoCountTest.java index 6c55573718..0ef4eefa25 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoCountTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoCountTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,20 +74,11 @@ public void scanCountSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); - - //only TERMINATED state evaluated is one from Operators: hasValue - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - test.onComplete(); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoDelayElementTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoDelayElementTest.java index ef5e27a272..75390847d6 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoDelayElementTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoDelayElementTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -174,7 +174,7 @@ public void cancelUpstreamOnceWhenCancelled() { VirtualTimeScheduler vts = VirtualTimeScheduler.create(); AtomicLong upstreamCancelCount = new AtomicLong(); - Mono source = Mono.just("foo").log().hide() + Mono source = Mono.empty().log().hide() .doOnCancel(() -> upstreamCancelCount.incrementAndGet()); StepVerifier.withVirtualTime( @@ -188,6 +188,26 @@ public void cancelUpstreamOnceWhenCancelled() { vts.advanceTimeBy(Duration.ofHours(1)); assertThat(upstreamCancelCount).hasValue(1); } + @Test + public void neverCancelUpstreamWhenValueResolvedOnce() { + VirtualTimeScheduler vts = VirtualTimeScheduler.create(); + AtomicLong upstreamCancelCount = new AtomicLong(); + + Mono source = Mono.just("foo").log().hide() + .doOnCancel(() -> upstreamCancelCount.incrementAndGet()); + + StepVerifier.withVirtualTime( + () -> new MonoDelayElement<>(source, 2, TimeUnit.SECONDS, vts), + () -> vts, Long.MAX_VALUE) + .expectSubscription() + .expectNoEvent(Duration.ofSeconds(1)) + .thenCancel() + .verifyThenAssertThat() + .hasDiscarded("foo"); + + vts.advanceTimeBy(Duration.ofHours(1)); + assertThat(upstreamCancelCount).hasValue(0); + } @Test public void cancelUpstreamOnceWhenRejected() { @@ -377,7 +397,7 @@ public void scanSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.RUN_ON)).isSameAs(Schedulers.single()); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.ASYNC); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoElementAtTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoElementAtTest.java index e9f39afb5f..a5212420bc 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoElementAtTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoElementAtTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -239,7 +239,7 @@ public void scanSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -248,10 +248,6 @@ public void scanSubscriber() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoFilterWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoFilterWhenTest.java index eb9fea3442..6b6621f0bd 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoFilterWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoFilterWhenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -332,7 +332,7 @@ public void scanSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -351,7 +351,7 @@ public void scanFilterWhenInner() { MonoFilterWhen.MonoFilterWhenMain main = new MonoFilterWhen.MonoFilterWhenMain<>( actual, s -> Mono.just(false)); - MonoFilterWhen.FilterWhenInner test = new MonoFilterWhen.FilterWhenInner(main, true); + MonoFilterWhen.FilterWhenInner test = new MonoFilterWhen.FilterWhenInner(main, true, null); Subscription innerSubscription = Operators.emptySubscription(); test.onSubscribe(innerSubscription); @@ -360,17 +360,13 @@ public void scanFilterWhenInner() { assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(innerSubscription); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(1L); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(0L); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoFlatMapTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoFlatMapTest.java index 0c49725507..6b29442dce 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoFlatMapTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoFlatMapTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ public void scanMain() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -75,10 +75,6 @@ public void scanMain() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test @@ -98,10 +94,6 @@ public void scanInner() { test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoHasElementsTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoHasElementsTest.java index 13dcf47f42..d30d8e8828 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoHasElementsTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoHasElementsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -218,7 +218,7 @@ public void scanHasElements() { assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); @@ -239,22 +239,6 @@ public void scanHasElementsNoTerminatedOnError() { assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); } - @Test - public void scanHasElementsCancelled() { - CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); - MonoHasElements.HasElementsSubscriber test = new MonoHasElements.HasElementsSubscriber<>(actual); - Subscription parent = Operators.emptySubscription(); - test.onSubscribe(parent); - - test.onError(new IllegalStateException("boom")); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - - test.cancel(); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); - } - @Test public void scanHasElement() { CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); @@ -264,7 +248,7 @@ public void scanHasElement() { assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); @@ -276,29 +260,14 @@ public void scanHasElement() { } @Test - public void scanHasElementNoTerminatedOnError() { + public void scanHasElementTerminatedOnError() { CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); MonoHasElement.HasElementSubscriber test = new MonoHasElement.HasElementSubscriber<>(actual); - test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - } - - @Test - public void scanHasElementCancelled() { - CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); - MonoHasElement.HasElementSubscriber test = new MonoHasElement.HasElementSubscriber<>(actual); - Subscription parent = Operators.emptySubscription(); - test.onSubscribe(parent); - test.onError(new IllegalStateException("boom")); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - - test.cancel(); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoPeekAfterTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoPeekAfterTest.java index dd3f6df0ae..367beca555 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoPeekAfterTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoPeekAfterTest.java @@ -90,53 +90,6 @@ public void onSuccessNormalConditional() { } - @Test - public void onSuccessFusion() { - LongAdder invoked = new LongAdder(); - AtomicBoolean hasNull = new AtomicBoolean(); - - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .doOnSuccess(v -> { - if (v == null) hasNull.set(true); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion(Fuseable.ASYNC) - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(hasNull.get()).as("unexpected call to onSuccess with null").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - } - - @Test - public void onSuccessFusionConditional() { - LongAdder invoked = new LongAdder(); - AtomicBoolean hasNull = new AtomicBoolean(); - - Mono mono = Flux - .range(1, 10) - .reduce((a, b) -> a + b) - .filter(v -> true) - .doOnSuccess(v -> { - if (v == null) hasNull.set(true); - invoked.increment(); - }); - - StepVerifier.create(mono) - .expectFusion() - .expectNext(55) - .expectComplete() - .verify(); - - assertThat(hasNull.get()).as("unexpected call to onSuccess with null").isFalse(); - assertThat(invoked.intValue()).isEqualTo(1); - } - @Test public void onAfterTerminateNormalConditional() { LongAdder invoked = new LongAdder(); @@ -344,30 +297,6 @@ void testCallbacksFusionSync() { assertThat(errorInvocation).hasValue(null); } - @Test - void testCallbacksFusionAsync() { - AtomicReference successInvocation = new AtomicReference<>(); - AtomicReference errorInvocation = new AtomicReference<>(); - - Mono source = Flux - .range(1, 10) - .reduce((a, b) -> a + b); - - Mono mono = new MonoPeekTerminal<>(source, - successInvocation::set, - errorInvocation::set, - null); //afterTerminate forces the negotiation of fusion mode NONE - - StepVerifier.create(mono) - .expectFusion(Fuseable.ASYNC) - .expectNext(55) - .expectComplete() - .verify(); - - assertThat((Object) successInvocation.get()).isEqualTo(55); - assertThat(errorInvocation).hasValue(null); - } - @Test public void should_reduce_to_10_events() { for (int i = 0; i < 20; i++) { diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoPublishMulticastTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoPublishMulticastTest.java index b4697274b1..f598262e84 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoPublishMulticastTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoPublishMulticastTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,28 +41,12 @@ void normal() { .publish(o -> o.flatMap(s -> Mono.just(2))); StepVerifier.create(m) - .expectFusion() - .expectNext(2) - .verifyComplete(); - - StepVerifier.create(m) - .expectFusion() - .expectNext(2) - .verifyComplete(); - } - - @Test - void normalHide() { - AtomicInteger i = new AtomicInteger(); - Mono m = Mono.fromCallable(i::incrementAndGet) - //actually, o isn't Fuseable to start with - .publish(o -> o.map(s -> 2).hide()); - - StepVerifier.create(m) + .expectNoFusionSupport() .expectNext(2) .verifyComplete(); StepVerifier.create(m) + .expectNoFusionSupport() .expectNext(2) .verifyComplete(); } @@ -141,30 +125,11 @@ void normalCancelBeforeComplete() { //see https://github.com/reactor/reactor-core/issues/2600 @Test - void errorFused() { + void errorPropagated() { final String errorMessage = "Error in Mono"; final Mono source = Mono.error(new RuntimeException(errorMessage)); final Mono published = source.publish(coordinator -> coordinator.flatMap(Mono::just)); - StepVerifier.create(published) - .expectFusion() - .expectErrorMessage(errorMessage) - .verify(); - - StepVerifier.create(published, StepVerifierOptions.create().scenarioName("second shared invocation")) - .expectFusion() - .expectErrorMessage(errorMessage) - .verify(); - } - - //see https://github.com/reactor/reactor-core/issues/2600 - @Test - void errorHide() { - final String errorMessage = "Error in Mono"; - final Mono source = Mono.error(new RuntimeException(errorMessage)); - //value passed to Function is not Fuseable - final Mono published = source.publish(Function.identity()); - StepVerifier.create(published) .expectNoFusionSupport() .expectErrorMessage(errorMessage) diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoReduceSeedTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoReduceSeedTest.java index 9b66661ab0..cc6502a1bf 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoReduceSeedTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoReduceSeedTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -198,7 +198,7 @@ public void discardAccumulatedOnCancel() { sub.onSubscribe(Operators. emptySubscription()); sub.onNext(1); - assertThat(sub.value).isEqualTo(1); + assertThat(sub.seed).isEqualTo(1); sub.cancel(); testSubscriber.assertNoError(); @@ -219,13 +219,13 @@ public void discardOnNextAfterCancel() { sub.cancel(); //discards seed - assertThat(sub.value).isNull(); + assertThat(sub.seed).isNull(); sub.onNext(1); //discards passed value since cancelled testSubscriber.assertNoError(); assertThat(discarded).containsExactly(0, 1); - assertThat(sub.value).isNull(); + assertThat(sub.seed).isNull(); } @Test @@ -241,7 +241,7 @@ public void discardOnError() { sub.onSubscribe(Operators. emptySubscription()); sub.onNext(1); - assertThat(sub.value).isEqualTo(1); + assertThat(sub.seed).isEqualTo(1); sub.onError(new RuntimeException("boom")); testSubscriber.assertErrorMessage("boom"); @@ -260,10 +260,10 @@ public void noRetainValueOnCancel() { sub.onNext(1); sub.onNext(2); - assertThat(sub.value).isEqualTo(3); + assertThat(sub.seed).isEqualTo(3); sub.cancel(); - assertThat(sub.value).isNull(); + assertThat(sub.seed).isNull(); testSubscriber.assertNoError(); } @@ -280,10 +280,10 @@ public void noRetainValueOnError() { sub.onNext(1); sub.onNext(2); - assertThat(sub.value).isEqualTo(3); + assertThat(sub.seed).isEqualTo(3); sub.onError(new RuntimeException("boom")); - assertThat(sub.value).isNull(); + assertThat(sub.seed).isNull(); testSubscriber.assertErrorMessage("boom"); } @@ -296,14 +296,14 @@ public void scanOperator(){ } @Test - public void scanSubscriber() { + public void scanSubscriberTerminatedScenario() { CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); MonoReduceSeed.ReduceSeedSubscriber test = new MonoReduceSeed.ReduceSeedSubscriber<>( actual, (s, i) -> s + i, "foo"); Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -312,6 +312,21 @@ public void scanSubscriber() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + } + + @Test + public void scanSubscriberCancelledScenario() { + CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); + MonoReduceSeed.ReduceSeedSubscriber test = new MonoReduceSeed.ReduceSeedSubscriber<>( + actual, (s, i) -> s + i, "foo"); + Subscription parent = Operators.emptySubscription(); + test.onSubscribe(parent); + + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); + + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); + assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.cancel(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoReduceTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoReduceTest.java index 3b7c47fede..2013069329 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoReduceTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoReduceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,6 @@ import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThat; public class MonoReduceTest extends ReduceOperatorTest{ @@ -231,7 +230,7 @@ public void discardAccumulatedOnCancel() { sub.onSubscribe(Operators. emptySubscription()); sub.onNext(1); - assertThat(sub.value).isEqualTo(1); + assertThat(sub.aggregate).isEqualTo(1); sub.cancel(); testSubscriber.assertNoError(); @@ -252,7 +251,7 @@ public void discardOnError() { sub.onSubscribe(Operators. emptySubscription()); sub.onNext(1); - assertThat(sub.value).isEqualTo(1); + assertThat(sub.aggregate).isEqualTo(1); sub.onError(new RuntimeException("boom")); testSubscriber.assertErrorMessage("boom"); @@ -271,11 +270,11 @@ public void noRetainValueOnComplete() { sub.onNext(1); sub.onNext(2); - assertThat(sub.value).isEqualTo(3); + assertThat(sub.aggregate).isEqualTo(3); sub.request(1); sub.onComplete(); - assertThat(sub.value).isNull(); + assertThat(sub.aggregate).isNull(); testSubscriber.assertNoError(); } @@ -292,10 +291,10 @@ public void noRetainValueOnError() { sub.onNext(1); sub.onNext(2); - assertThat(sub.value).isEqualTo(3); + assertThat(sub.aggregate).isEqualTo(3); sub.onError(new RuntimeException("boom")); - assertThat(sub.value).isNull(); + assertThat(sub.aggregate).isNull(); testSubscriber.assertErrorMessage("boom"); } @@ -308,13 +307,13 @@ public void scanOperator(){ } @Test - public void scanSubscriber() { + public void scanSubscriberTerminatedScenario() { CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); MonoReduce.ReduceSubscriber test = new MonoReduce.ReduceSubscriber<>(actual, (s1, s2) -> s1 + s2); Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -323,6 +322,20 @@ public void scanSubscriber() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + } + + @Test + public void scanSubscriberCancelledScenario() { + CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); + MonoReduce.ReduceSubscriber test = new MonoReduce.ReduceSubscriber<>(actual, (s1, s2) -> s1 + s2); + Subscription parent = Operators.emptySubscription(); + test.onSubscribe(parent); + + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); + + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); + assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.cancel(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoStreamCollectorTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoStreamCollectorTest.java index 7976bc3749..78bfd7f513 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoStreamCollectorTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoStreamCollectorTest.java @@ -139,7 +139,7 @@ public void scanStreamCollectorSubscriber() { test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -148,10 +148,6 @@ public void scanStreamCollectorSubscriber() { assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); test.onError(new IllegalStateException("boom")); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - test.cancel(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoSubscribeOnTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoSubscribeOnTest.java index f03547cd41..f67bb28e42 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoSubscribeOnTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoSubscribeOnTest.java @@ -174,9 +174,9 @@ public void classicWithTimeout() { } return 0; }) + .subscribeOn(timeoutScheduler) .timeout(Duration.ofMillis(100L)) - .onErrorResume(t -> Mono.fromCallable(() -> 1)) - .subscribeOn(timeoutScheduler), + .onErrorResume(t -> Mono.fromCallable(() -> 1)), 0) .expectSubscription() .thenRequest(1) diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoSupplierTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoSupplierTest.java index 22dcdaafd2..17fa10d96f 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoSupplierTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoSupplierTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,13 +49,6 @@ public void normalSupplyingNull() { .verifyComplete(); } - @Test - public void normalSupplyingNullBackpressuredShortcuts() { - StepVerifier.create(Mono.fromSupplier(() -> null), 0) - .expectSubscription() - .verifyComplete(); - } - @Test public void asyncSupplyingNull() { StepVerifier.create(Mono.fromSupplier(() -> null) diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoTakeLastOneTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoTakeLastOneTest.java index c0fcc6dfc1..919ea70a80 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoTakeLastOneTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoTakeLastOneTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -171,7 +171,7 @@ public void scanTakeLastOneSubscriber() { Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); @@ -179,7 +179,8 @@ public void scanTakeLastOneSubscriber() { //terminated is detected via state HAS_VALUE assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - test.complete("bar"); + test.onNext("bar"); + test.onComplete(); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java index de6d277e3b..20ba9d5a40 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -319,26 +319,6 @@ public void cleanupDropsThrowable_normalNotEager() { @Test public void smokeTestMapReduceGuardedByCleanup_normalEager() { - AtomicBoolean cleaned = new AtomicBoolean(); - Mono.using(() -> cleaned, - ab -> Flux.just("foo", "bar", "baz") - .delayElements(Duration.ofMillis(100)) - .count() - .map(i -> "" + i + ab.get()) - .hide(), - ab -> ab.set(true), - true) - .as(StepVerifier::create) - .expectNoFusionSupport() - .expectNext("3false") - .expectComplete() - .verify(); - - assertThat(cleaned).isTrue(); - } - - @Test - public void smokeTestMapReduceGuardedByCleanup_fusedEager() { AtomicBoolean cleaned = new AtomicBoolean(); Mono.using(() -> cleaned, ab -> Flux.just("foo", "bar", "baz") @@ -348,7 +328,7 @@ public void smokeTestMapReduceGuardedByCleanup_fusedEager() { ab -> ab.set(true), true) .as(StepVerifier::create) - .expectFusion() + .expectNoFusionSupport() .expectNext("3false") .expectComplete() .verify(); @@ -358,29 +338,6 @@ public void smokeTestMapReduceGuardedByCleanup_fusedEager() { @Test public void smokeTestMapReduceGuardedByCleanup_normalNotEager() { - AtomicBoolean cleaned = new AtomicBoolean(); - Mono.using(() -> cleaned, - ab -> Flux.just("foo", "bar", "baz") - .delayElements(Duration.ofMillis(100)) - .count() - .map(i -> "" + i + ab.get()) - .hide(), - ab -> ab.set(true), - false) - .as(StepVerifier::create) - .expectNoFusionSupport() - .expectNext("3false") - .expectComplete() - .verify(); - - //since the handler is executed after onComplete, we allow some delay - await().atMost(100, TimeUnit.MILLISECONDS) - .with().pollInterval(10, TimeUnit.MILLISECONDS) - .untilAsserted(assertThat(cleaned)::isTrue); - } - - @Test - public void smokeTestMapReduceGuardedByCleanup_fusedNotEager() { AtomicBoolean cleaned = new AtomicBoolean(); Mono.using(() -> cleaned, ab -> Flux.just("foo", "bar", "baz") @@ -390,7 +347,7 @@ public void smokeTestMapReduceGuardedByCleanup_fusedNotEager() { ab -> ab.set(true), false) .as(StepVerifier::create) - .expectFusion() + .expectNoFusionSupport() .expectNext("3false") .expectComplete() .verify(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoZipTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoZipTest.java index 06000b14f4..a913ae717a 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoZipTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoZipTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -457,10 +457,10 @@ public void scanOperator() { @Test public void scanCoordinator() { CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); - MonoZip.ZipCoordinator test = new MonoZip.ZipCoordinator<>( + MonoZip.ZipCoordinator test = new MonoZip.ZipCoordinator<>(new Mono[2], actual, 2, true, a -> String.valueOf(a[0])); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.BUFFERED)).isEqualTo(2); assertThat(test.scan(Scannable.Attr.DELAY_ERROR)).isTrue(); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); @@ -476,28 +476,28 @@ public void scanCoordinator() { @Test public void innerErrorIncrementsParentDone() { CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); - MonoZip.ZipCoordinator parent = new MonoZip.ZipCoordinator<>( + MonoZip.ZipCoordinator parent = new MonoZip.ZipCoordinator<>(new Mono[2], actual, 2, false, a -> String.valueOf(a[0])); MonoZip.ZipInner test = new MonoZip.ZipInner<>(parent); - assertThat(parent.done).isZero(); + assertThat(parent.state).isZero(); test.onError(new IllegalStateException("boom")); - assertThat(parent.done).isEqualTo(2); + assertThat(parent.state).isEqualTo(MonoZip.ZipCoordinator.INTERRUPTED_FLAG + 2); assertThat(parent.scan(Scannable.Attr.TERMINATED)).isTrue(); } @Test public void scanCoordinatorNotDoneUntilN() { CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); - MonoZip.ZipCoordinator test = new MonoZip.ZipCoordinator<>( + MonoZip.ZipCoordinator test = new MonoZip.ZipCoordinator<>(new Mono[2], actual, 10, true, a -> String.valueOf(a[0])); - test.done = 9; + test.state = 9; assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - test.done = 10; + test.state = 10; assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); } @@ -506,7 +506,8 @@ public void scanWhenInner() { CoreSubscriber actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); MonoZip.ZipCoordinator - coordinator = new MonoZip.ZipCoordinator<>(actual, 2, false, a -> null); + coordinator = new MonoZip.ZipCoordinator<>(new Mono[2], actual, 2, + false, a -> null); MonoZip.ZipInner test = new MonoZip.ZipInner<>(coordinator); Subscription innerSub = Operators.cancelledSubscription(); test.onSubscribe(innerSub); diff --git a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java index 9f2c771089..9b88d05f6d 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.AfterEach; @@ -58,6 +59,19 @@ public class OnDiscardShouldNotLeakTest { // add DiscardScenarios here to test more operators private static final DiscardScenario[] SCENARIOS = new DiscardScenario[] { DiscardScenario.allFluxSourceArray("merge", 4, Flux::merge), + DiscardScenario.allFluxSourceArray("when", 4, + sources -> Mono.when(sources.toArray(new Publisher[0])).thenReturn(Tracked.RELEASED)), + DiscardScenario.allFluxSourceArray("monoZip", 4, + sources -> Mono.zip( + sources.stream() + .map(Flux::last) + .collect(Collectors.toList()), + values -> Arrays.stream(values) + .map(v -> (Tracked) v) + .collect(Collectors.toList()) + ) + .doOnSuccess(l -> l.forEach(Tracked::safeRelease)) + .thenReturn(Tracked.RELEASED)), DiscardScenario.fluxSource("onBackpressureBuffer", Flux::onBackpressureBuffer), DiscardScenario.fluxSource("onBackpressureBufferAndPublishOn", f -> f .onBackpressureBuffer() @@ -70,6 +84,10 @@ public class OnDiscardShouldNotLeakTest { .publishOn(Schedulers.immediate())), DiscardScenario.rawSource("flatMapInner", raw -> Flux.just(1).flatMap(f -> raw)), DiscardScenario.fluxSource("flatMap", main -> main.flatMap(f -> Mono.just(f).hide().flux())), + DiscardScenario.fluxSource("monoFlatMap", main -> main.last().flatMap(f -> Mono.just(f).hide())), + DiscardScenario.fluxSource("monoFilterWhen", main -> main.last().filterWhen(__ -> Mono.just(true).hide())), + DiscardScenario.fluxSource("monoFilterWhenFalse", main -> main.last().filterWhen(__ -> Mono.just(false).hide())), + DiscardScenario.fluxSource("last", main -> main.last(new Tracked("default")).flatMap(f -> Mono.just(f).hide())), DiscardScenario.fluxSource("flatMapIterable", f -> f.flatMapIterable(Arrays::asList)), DiscardScenario.fluxSource("publishOnDelayErrors", f -> f.publishOn(Schedulers.immediate())), DiscardScenario.fluxSource("publishOnImmediateErrors", f -> f.publishOn(Schedulers.immediate(), false, Queues.SMALL_BUFFER_SIZE)), @@ -91,7 +109,26 @@ public class OnDiscardShouldNotLeakTest { .thenReturn(Tracked.RELEASED)), DiscardScenario.fluxSource("collectList", f -> f.collectList() .doOnSuccess(l -> l.forEach(Tracked::safeRelease)) - .thenReturn(Tracked.RELEASED)) + .thenReturn(Tracked.RELEASED)), + DiscardScenario.fluxSource("streamCollector", f -> f.collect(Collectors.toList()) + .doOnSuccess(l -> l.forEach(Tracked::safeRelease)) + .thenReturn(Tracked.RELEASED)), + DiscardScenario.fluxSource("reduce", f -> f.reduce((t1, t2) -> { + t1.release(); + return t2; + }) + .thenReturn(Tracked.RELEASED)), + DiscardScenario.fluxSource("reduceSeed", f -> f.reduce(new Tracked("seed"), (t1, t2) -> { + t1.release(); + return t2; + }) + .thenReturn(Tracked.RELEASED)), + DiscardScenario.fluxSource("reduceWith", f -> f.reduceWith(() -> new Tracked("seed"), (t1, t2) -> { + t1.release(); + return t2; + }) + .thenReturn(Tracked.RELEASED)), + DiscardScenario.fluxSource("reduceWith", f -> f.defaultIfEmpty(new Tracked("default"))) }; private static final boolean[][] CONDITIONAL_AND_FUSED = new boolean[][] { diff --git a/reactor-core/src/test/java/reactor/core/publisher/ParallelCollectTest.java b/reactor-core/src/test/java/reactor/core/publisher/ParallelCollectTest.java index 6ed6217fcc..9905743fff 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ParallelCollectTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ParallelCollectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.function.Supplier; @@ -114,7 +113,7 @@ public void scanOperator() { } @Test - public void scanSubscriber() { + public void scanSubscriberTerminatedScenario() { CoreSubscriber> subscriber = new LambdaSubscriber<>(null, e -> {}, null, null); ParallelCollectSubscriber> test = new ParallelCollectSubscriber<>( subscriber, new ArrayList<>(), List::add); @@ -122,12 +121,25 @@ public void scanSubscriber() { test.onSubscribe(s); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(subscriber); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - test.complete(Collections.emptyList()); + test.onComplete(); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + } + + @Test + public void scanSubscriberCancelledScenario() { + CoreSubscriber> subscriber = new LambdaSubscriber<>(null, e -> {}, null, null); + ParallelCollectSubscriber> test = new ParallelCollectSubscriber<>( + subscriber, new ArrayList<>(), List::add); + Subscription s = Operators.emptySubscription(); + test.onSubscribe(s); + + assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(subscriber); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.cancel(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/ParallelMergeReduceTest.java b/reactor-core/src/test/java/reactor/core/publisher/ParallelMergeReduceTest.java index 65bcf25b88..f62fcb5843 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ParallelMergeReduceTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ParallelMergeReduceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,12 +98,14 @@ public void scanOperator() { public void scanMainSubscriber() { CoreSubscriber subscriber = new LambdaSubscriber<>(null, e -> { }, null, sub -> sub.request(2)); - MergeReduceMain test = new MergeReduceMain<>(subscriber, 2, (a, b) -> a + b); + MergeReduceMain test = new MergeReduceMain<>(Flux.never() + .parallel(2), + subscriber, 2, (a, b) -> a + b); subscriber.onSubscribe(test); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(subscriber); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); @@ -118,11 +120,13 @@ public void scanMainSubscriber() { assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } + @Test public void scanMainSubscriberError() { CoreSubscriber subscriber = new LambdaSubscriber<>(null, e -> { }, null, sub -> sub.request(2)); - MergeReduceMain test = new MergeReduceMain<>(subscriber, 2, (a, b) -> a + b); + MergeReduceMain test = new MergeReduceMain<>(Flux.range(0, 10) + .parallel(2), subscriber, 2, (a, b) -> a + b); subscriber.onSubscribe(test); @@ -134,7 +138,9 @@ public void scanMainSubscriberError() { @Test public void scanInnerSubscriber() { CoreSubscriber subscriber = new LambdaSubscriber<>(null, e -> { }, null, null); - MergeReduceMain main = new MergeReduceMain<>(subscriber, 2, (a, b) -> a + b); + MergeReduceMain main = new MergeReduceMain<>(Flux.range(0, 10) + .parallel(2), + subscriber, 2, (a, b) -> a + b); MergeReduceInner test = new MergeReduceInner<>(main, (a, b) -> a + b); Subscription s = Operators.emptySubscription(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/ParallelReduceSeedTest.java b/reactor-core/src/test/java/reactor/core/publisher/ParallelReduceSeedTest.java index 489549a670..1dc96856a7 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ParallelReduceSeedTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ParallelReduceSeedTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -252,7 +252,7 @@ public void scanOperator() { @Test public void scanSubscriber() { - ParallelFlux source = Flux.just(500, 300).parallel(10); + ParallelFlux source = Flux.just(500, 300).parallel(2); LambdaSubscriber subscriber = new LambdaSubscriber<>(null, e -> { }, null, null); @@ -261,24 +261,20 @@ public void scanSubscriber() { subscriber, "", (s, i) -> s + i); @SuppressWarnings("unchecked") - final CoreSubscriber[] testSubscribers = new CoreSubscriber[1]; + final CoreSubscriber[] testSubscribers = new CoreSubscriber[2]; testSubscribers[0] = test; + testSubscribers[1] = new ParallelReduceSeed.ParallelReduceSeedSubscriber<>( + subscriber, "", (s, i) -> s + i);; source.subscribe(testSubscribers); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(subscriber); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); - test.state = Operators.MonoSubscriber.HAS_REQUEST_HAS_VALUE; + test.done = true; assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - - test.state = Operators.MonoSubscriber.HAS_REQUEST_NO_VALUE; - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - - test.state = Operators.MonoSubscriber.NO_REQUEST_HAS_VALUE; - assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - + test.done = false; assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.cancel(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/ParallelThenTest.java b/reactor-core/src/test/java/reactor/core/publisher/ParallelThenTest.java index f0d7643842..76f3e7a6a7 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ParallelThenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ParallelThenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,43 +85,52 @@ public void scanOperator() { public void scanMainSubscriber() { CoreSubscriber subscriber = new LambdaSubscriber<>(null, e -> { }, null, sub -> sub.request(2)); - ParallelThen.ThenMain test = new ParallelThen.ThenMain(subscriber, 2); + ParallelThen.ThenMain test = new ParallelThen.ThenMain(subscriber, new ParallelFlux() { + @Override + public int parallelism() { + return 2; + } + + @Override + public void subscribe(CoreSubscriber[] subscribers) { + + } + }); subscriber.onSubscribe(test); assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(subscriber); - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(0); assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.ERROR)).isNull(); test.innerComplete(); test.innerComplete(); assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + // clear state + test.state = 0; + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); test.cancel(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } - @Test - public void scanMainSubscriberError() { - CoreSubscriber subscriber = new LambdaSubscriber<>(null, e -> { }, null, - sub -> sub.request(2)); - ParallelThen.ThenMain test = new ParallelThen.ThenMain(subscriber, 2); - - subscriber.onSubscribe(test); - - assertThat(test.scan(Scannable.Attr.ERROR)).isNull(); - test.innerError(new IllegalStateException("boom")); - assertThat(test.scan(Scannable.Attr.ERROR)).hasMessage("boom"); - } - @Test public void scanInnerSubscriber() { CoreSubscriber subscriber = new LambdaSubscriber<>(null, e -> { }, null, null); - ParallelThen.ThenMain main = new ParallelThen.ThenMain(subscriber, 2); + ParallelThen.ThenMain main = new ParallelThen.ThenMain(subscriber, new ParallelFlux() { + @Override + public int parallelism() { + return 2; + } + + @Override + public void subscribe(CoreSubscriber[] subscribers) { + + } + }); ParallelThen.ThenInner test = new ParallelThen.ThenInner(main); Subscription s = Operators.emptySubscription(); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/MonoMetricsFuseableTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/MonoMetricsFuseableTest.java index e7ef429661..bebe89d11b 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/MonoMetricsFuseableTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/MonoMetricsFuseableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -327,32 +327,6 @@ public void completeEmptyNoFusion() { .satisfies(timer -> assertThat(timer.count()).as("timer count").isOne()); } - @Test - public void completeEmptyAsyncFusion() { - Mono source = Mono.fromCallable(() -> null); - - StepVerifier.create(new MonoMetricsFuseable<>(source)) - .expectFusion(Fuseable.ASYNC) - .verifyComplete(); - - Timer stcCompleteCounter = registry.find(REACTOR_DEFAULT_NAME + METER_FLOW_DURATION) - .tags(Tags.of(TAG_ON_COMPLETE)) - .timer(); - - Timer stcCompleteEmptyCounter = registry.find(REACTOR_DEFAULT_NAME + METER_FLOW_DURATION) - .tags(Tags.of(TAG_ON_COMPLETE_EMPTY)) - .timer(); - - assertThat(stcCompleteCounter) - .as("complete with element") - .isNull(); - - assertThat(stcCompleteEmptyCounter) - .as("complete without any element") - .isNotNull() - .satisfies(timer -> assertThat(timer.count()).as("timer count").isOne()); - } - @Test public void completeEmptySyncFusion() { MonoMetricsFuseable.MetricsFuseableSubscriber subscriber = @@ -409,33 +383,6 @@ public void completeWithElementNoFusion() { .isNull(); } - @Test - public void completeWithElementAsyncFusion() { - Mono source = Mono.fromCallable(() -> 1); - - StepVerifier.create(new MonoMetricsFuseable<>(source)) - .expectFusion(Fuseable.ASYNC) - .expectNext(1) - .verifyComplete(); - - Timer stcCompleteCounter = registry.find(REACTOR_DEFAULT_NAME + METER_FLOW_DURATION) - .tags(Tags.of(TAG_ON_COMPLETE)) - .timer(); - - Timer stcCompleteEmptyCounter = registry.find(REACTOR_DEFAULT_NAME + METER_FLOW_DURATION) - .tags(Tags.of(TAG_ON_COMPLETE_EMPTY)) - .timer(); - - assertThat(stcCompleteCounter) - .as("complete with element") - .isNotNull() - .satisfies(timer -> assertThat(timer.count()).as("timer count").isOne()); - - assertThat(stcCompleteEmptyCounter) - .as("complete without any element") - .isNull(); - } - @Test public void completeWithElementSyncFusion() { Mono source = Mono.just(1); @@ -463,46 +410,6 @@ public void completeWithElementSyncFusion() { .isNull(); } - @Test - public void subscribeToCompleteFuseable() { - Mono source = Mono.fromCallable(() -> { - Thread.sleep(100); - return "foo"; - }); - - StepVerifier.create(new MonoMetricsFuseable<>(source)) - .expectFusion(Fuseable.ASYNC) - .expectNext("foo") - .verifyComplete(); - - - Timer stcCompleteTimer = registry.find(REACTOR_DEFAULT_NAME + METER_FLOW_DURATION) - .tags(Tags.of(TAG_ON_COMPLETE)) - .timer(); - - Timer stcErrorTimer = registry.find(REACTOR_DEFAULT_NAME + METER_FLOW_DURATION) - .tags(Tags.of(TAG_ON_ERROR)) - .timer(); - - Timer stcCancelTimer = registry.find(REACTOR_DEFAULT_NAME + METER_FLOW_DURATION) - .tags(Tags.of(TAG_CANCEL)) - .timer(); - - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(stcCompleteTimer.max(TimeUnit.MILLISECONDS)) - .as("subscribe to complete timer") - .isGreaterThanOrEqualTo(100); - - softly.assertThat(stcErrorTimer) - .as("subscribe to error timer lazily registered") - .isNull(); - - softly.assertThat(stcCancelTimer) - .as("subscribe to cancel timer") - .isNull(); - }); - } - @Test public void subscribeToErrorFuseable() { Mono source = Mono.delay(Duration.ofMillis(100)) From 06729ec450467a0e85f33f7cb1aa1677b263281f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 4 Aug 2022 15:31:29 +0200 Subject: [PATCH 047/312] Add TimedScheduler to reactor-core-micrometer module (#3109) This commit introduces a TimedScheduler wrapper to reactor-core-micrometer module which wraps any Scheduler to add task metrics to it. This _replaces_ Micrometer.enableSchedulerMetrics from previous milestones, and is thus considered a breaking API change **vs milestones M1-M4**. The class itself is package-private, with a factory method in Micrometer that only present a `Scheduler` view of the `TimedScheduler`. The deprecation notice in core's `Schedulers#enableMetrics()` has been updated to reflect the new alternative. An old test in core was named `TimedSchedulerTest`. This is confusing now that there is an actual `TimedScheduler` so these tests have been melded into `SingleSchedulerTest` instead. This is a different approach from the core one which also Fixes #1201 again. --- reactor-core-micrometer/build.gradle | 1 + .../observability/micrometer/Micrometer.java | 46 +-- .../MicrometerSchedulerMetricsDecorator.java | 223 ------------ .../micrometer/TimedScheduler.java | 281 +++++++++++++++ .../micrometer/MicrometerTest.java | 31 ++ .../micrometer/TimedSchedulerTest.java | 322 ++++++++++++++++++ .../scheduler/SchedulerMetricDecorator.java | 2 +- .../reactor/core/scheduler/Schedulers.java | 4 +- .../core/scheduler/SingleSchedulerTest.java | 58 +++- .../core/scheduler/TimedSchedulerTest.java | 98 ------ 10 files changed, 717 insertions(+), 349 deletions(-) delete mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerSchedulerMetricsDecorator.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java create mode 100644 reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java delete mode 100644 reactor-core/src/test/java/reactor/core/scheduler/TimedSchedulerTest.java diff --git a/reactor-core-micrometer/build.gradle b/reactor-core-micrometer/build.gradle index a53bcca324..d4fade8344 100644 --- a/reactor-core-micrometer/build.gradle +++ b/reactor-core-micrometer/build.gradle @@ -71,6 +71,7 @@ dependencies { testRuntimeOnly libs.logback testImplementation libs.assertj testImplementation libs.mockito + testImplementation libs.awaitility } tasks.withType(Test).all { diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index 189c7d68bd..9c3b5856ae 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -16,26 +16,22 @@ package reactor.core.observability.micrometer; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; - import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import reactor.core.observability.SignalListener; import reactor.core.observability.SignalListenerFactory; import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; public final class Micrometer { - private static final String SCHEDULERS_DECORATOR_KEY = "reactor.core.observability.micrometer.schedulerDecorator"; - /** * The default "name" to use as a prefix for meter if the instrumented sequence doesn't define a {@link reactor.core.publisher.Flux#name(String) name}. */ @@ -94,29 +90,33 @@ public final class Micrometer { return new MicrometerObservationListenerFactory<>(registry); } - //FIXME: remove these and replace with an option to decorate an arbitrary Scheduler - /** - * Set-up a decorator that will instrument any {@link ExecutorService} that backs a reactor-core {@link Scheduler} - * (or scheduler implementations which use {@link Schedulers#decorateExecutorService(Scheduler, ScheduledExecutorService)}). - *

    - * The {@link MeterRegistry} to use can be configured via {@link reactor.util.Metrics.MicrometerConfiguration#useRegistry(MeterRegistry)} - * prior to using this method, the default being {@link io.micrometer.core.instrument.Metrics#globalRegistry}. + * Wrap a {@link Scheduler} in an instance that gathers various task-related metrics using + * the provided {@link MeterRegistry} and naming meters using the provided {@code metricsPrefix}. + * Note that no tags are set up for these meters. * - * @implNote Note that this is added as a decorator via Schedulers when enabling metrics for schedulers, - * which doesn't change the Factory. + * @param original the original {@link Scheduler} to decorate with metrics + * @param meterRegistry the {@link MeterRegistry} in which to register the various meters + * @param metricsPrefix the prefix to use in meter names. If needed, a dot is added at the end + * @return a {@link Scheduler} that is instrumented with dedicated metrics */ - @Deprecated - public static void enableSchedulersMetricsDecorator() { - Schedulers.addExecutorServiceDecorator(SCHEDULERS_DECORATOR_KEY, - new MicrometerSchedulerMetricsDecorator(reactor.util.Metrics.MicrometerConfiguration.getRegistry())); + public static Scheduler timedScheduler(Scheduler original, MeterRegistry meterRegistry, String metricsPrefix) { + return new TimedScheduler(original, meterRegistry, metricsPrefix, Tags.empty()); } /** - * If {@link #enableSchedulersMetricsDecorator()} has been previously called, removes the decorator. - * No-op if {@link #enableSchedulersMetricsDecorator()} hasn't been called. + * Wrap a {@link Scheduler} in an instance that gathers various task-related metrics using + * the provided {@link MeterRegistry} and naming meters using the provided {@code metricsPrefix}. + * User-provided collection of {@link Tag} (ie. {@link Tags}) can also be provided to be added to + * all the meters of that timed Scheduler. + * + * @param original the original {@link Scheduler} to decorate with metrics + * @param meterRegistry the {@link MeterRegistry} in which to register the various meters + * @param metricsPrefix the prefix to use in meter names. If needed, a dot is added at the end + * @param tags the tags to put on meters + * @return a {@link Scheduler} that is instrumented with dedicated metrics */ - public static void disableSchedulersMetricsDecorator() { - Schedulers.removeExecutorServiceDecorator(SCHEDULERS_DECORATOR_KEY); + public static Scheduler timedScheduler(Scheduler original, MeterRegistry meterRegistry, String metricsPrefix, Iterable tags) { + return new TimedScheduler(original, meterRegistry, metricsPrefix, tags); } } \ No newline at end of file diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerSchedulerMetricsDecorator.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerSchedulerMetricsDecorator.java deleted file mode 100644 index 2cad2a1317..0000000000 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerSchedulerMetricsDecorator.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.observability.micrometer; - -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; -import io.micrometer.core.instrument.search.Search; - -import reactor.core.Disposable; -import reactor.core.Scannable; -import reactor.core.scheduler.Scheduler; - -/** - * @author Simon Baslé - */ -final class MicrometerSchedulerMetricsDecorator implements BiFunction, - Disposable { - - //TODO expose keys and tags publicly? - static final String TAG_SCHEDULER_ID = "reactor.scheduler.id"; - - final WeakHashMap seenSchedulers = new WeakHashMap<>(); - final Map schedulerDifferentiator = new HashMap<>(); - final WeakHashMap executorDifferentiator = new WeakHashMap<>(); - final MeterRegistry registry; - - MicrometerSchedulerMetricsDecorator(MeterRegistry registry) { - this.registry = registry; - } - - @Override - public synchronized ScheduledExecutorService apply(Scheduler scheduler, ScheduledExecutorService service) { - //this is equivalent to `toString`, a detailed name like `parallel("foo", 3)` - String schedulerName = Scannable - .from(scheduler) - .scanOrDefault(Scannable.Attr.NAME, scheduler.getClass().getName()); - - //we hope that each NAME is unique enough, but we'll differentiate by Scheduler - String schedulerId = - seenSchedulers.computeIfAbsent(scheduler, s -> { - int schedulerDifferentiator = this.schedulerDifferentiator - .computeIfAbsent(schedulerName, k -> new AtomicInteger(0)) - .getAndIncrement(); - - return (schedulerDifferentiator == 0) ? schedulerName - : schedulerName + "#" + schedulerDifferentiator; - }); - - //we now want an executorId unique to a given scheduler - String executorId = schedulerId + "-" + - executorDifferentiator.computeIfAbsent(scheduler, key -> new AtomicInteger(0)) - .getAndIncrement(); - - Tag[] tags = new Tag[] { Tag.of(TAG_SCHEDULER_ID, schedulerId) }; - - /* - Design note: we assume that a given Scheduler won't apply the decorator twice to the - same ExecutorService. Even though, it would simply create an extraneous meter for - that ExecutorService, which we think is not that bad (compared to paying the price - upfront of also tracking executors instances to deduplicate). The main goal is to - detect Scheduler instances that have already started decorating their executors, - in order to avoid consider two calls in a row as duplicates (yet still being able - to distinguish between two instances with the same name and configuration). - */ - - - return new MetricsRemovingScheduledExecutorService(service, this.registry, executorId, tags); - } - - @Override - public void dispose() { - Search.in(registry) - .tagKeys(TAG_SCHEDULER_ID) - .meters() - .forEach(registry::remove); - - //note default isDisposed (returning false) is good enough, since the cleared - //collections can always be reused even though they probably won't - this.seenSchedulers.clear(); - this.schedulerDifferentiator.clear(); - this.executorDifferentiator.clear(); - } - - static class MetricsRemovingScheduledExecutorService implements ScheduledExecutorService { - - final ScheduledExecutorService scheduledExecutorService; - final MeterRegistry registry; - final String executorId; - - MetricsRemovingScheduledExecutorService(ScheduledExecutorService service, MeterRegistry registry, String executorId, Tag[] tags) { - this.scheduledExecutorService = ExecutorServiceMetrics.monitor(registry, service, executorId, tags); - this.registry = registry; - this.executorId = executorId; - } - - @Override - public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { - return scheduledExecutorService.awaitTermination(timeout, unit); - } - - @Override - public void execute(Runnable command) { - scheduledExecutorService.execute(command); - } - - @Override - public List> invokeAll(Collection> tasks) throws InterruptedException { - return scheduledExecutorService.invokeAll(tasks); - } - - @Override - public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) - throws InterruptedException { - return scheduledExecutorService.invokeAll(tasks, timeout, unit); - } - - @Override - public T invokeAny(Collection> tasks) throws InterruptedException, - ExecutionException { - return scheduledExecutorService.invokeAny(tasks); - } - - @Override - public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - return scheduledExecutorService.invokeAny(tasks, timeout, unit); - } - - @Override - public boolean isShutdown() { - return scheduledExecutorService.isShutdown(); - } - - @Override - public boolean isTerminated() { - return scheduledExecutorService.isTerminated(); - } - - @Override - public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { - return scheduledExecutorService.schedule(command, delay, unit); - } - - @Override - public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { - return scheduledExecutorService.schedule(callable, delay, unit); - } - - @Override - public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { - return scheduledExecutorService.scheduleAtFixedRate(command, initialDelay, period, unit); - } - - @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { - return scheduledExecutorService.scheduleWithFixedDelay(command, initialDelay, delay, unit); - } - - @Override - public List shutdownNow() { - removeMetrics(); - return scheduledExecutorService.shutdownNow(); - } - - @Override - public void shutdown() { - removeMetrics(); - scheduledExecutorService.shutdown(); - } - - @Override - public Future submit(Callable task) { - return scheduledExecutorService.submit(task); - } - - @Override - public Future submit(Runnable task, T result) { - return scheduledExecutorService.submit(task, result); - } - - @Override - public Future submit(Runnable task) { - return scheduledExecutorService.submit(task); - } - - void removeMetrics() { - Search.in(registry) - .tag("name", executorId) - .meters() - .forEach(registry::remove); - } - } -} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java new file mode 100644 index 0000000000..5cd2611e2b --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; + +import reactor.core.Disposable; +import reactor.core.scheduler.Scheduler; + +/** + * An instrumented {@link Scheduler} wrapping an original {@link Scheduler} + * and gathering metrics around submitted tasks. + * + * @author Simon Baslé + */ +final class TimedScheduler implements Scheduler { + + //FIXME document all the tags/meters/etc... somewhere public? + + /** + * {@link Timer} reflecting tasks that have finished execution. Note that this reflects all types of + * active tasks, including tasks scheduled {@link #schedule(Runnable, long, TimeUnit) with a delay} + * or {@link #schedulePeriodically(Runnable, long, long, TimeUnit) periodically} (each + * iteration being considered a separate completed task). + */ + static final String METER_TASKS_COMPLETED = "scheduler.tasks.completed"; + /** + * {@link LongTaskTimer} reflecting tasks currently running. Note that this reflects all types of + * active tasks, including tasks scheduled {@link #schedule(Runnable, long, TimeUnit) with a delay} + * or {@link #schedulePeriodically(Runnable, long, long, TimeUnit) periodically} (each + * iteration being considered an active task). + */ + static final String METER_TASKS_ACTIVE = "scheduler.tasks.active"; + /** + * {@link LongTaskTimer} reflecting tasks that were submitted for immediate execution but + * couldn't be started immediately because the scheduler is already at max capacity. + * Note that only immediate submissions via {@link Scheduler#schedule(Runnable)} and + * {@link Worker#schedule(Runnable)} are considered. + */ + static final String METER_TASKS_PENDING = "scheduler.tasks.pending"; + + /** + * The type of submission: + *

      + *
    • {@link #SUBMISSION_DIRECT} for {@link Scheduler#schedule(Runnable)}
    • + *
    • {@link #SUBMISSION_DELAYED} for {@link Scheduler#schedule(Runnable, long, TimeUnit)}
    • + *
    • {@link #SUBMISSION_PERIODIC_INITIAL} for {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} after the initial delay
    • + *
    • {@link #SUBMISSION_PERIODIC_ITERATION} for {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} further periodic iterations
    • + *
    + */ + static final String TAG_SUBMISSION = "submission.type"; + + /** + * {@link Counter} that increments by one each time a task is submitted (via any of the + * schedule methods on both {@link Scheduler} and {@link Worker}). + *

    + * Note that there are actually 4 counters, which can be differentiated by the {@link #TAG_SUBMISSION} tag. + * The sum of all these can thus be compared with the {@link #METER_TASKS_COMPLETED} counter. + */ + static final String METER_SUBMITTED = "scheduler.tasks.submitted"; + + /** + * {@link Counter} that increments by one each time a task is submitted for immediate execution + * (ie. {@link Scheduler#schedule(Runnable)} or {@link Worker#schedule(Runnable)}). + */ + static final String SUBMISSION_DIRECT = "direct"; + /** + * {@link Counter} that increments by one each time a task is submitted with a delay + * (ie. {@link Scheduler#schedule(Runnable, long, TimeUnit)} + * or {@link Worker#schedule(Runnable, long, TimeUnit)}). + */ + static final String SUBMISSION_DELAYED = "delayed"; + /** + * {@link Counter} that increments when a task is initially submitted with a period + * (ie. {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} + * or {@link Worker#schedulePeriodically(Runnable, long, long, TimeUnit)}). This isn't + * incremented on further iterations of the periodic task. + */ + static final String SUBMISSION_PERIODIC_INITIAL = "periodic_initial"; + /** + * {@link Counter} that increments by one each time a task is re-executed due to the periodic + * nature of {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} + * or {@link Worker#schedulePeriodically(Runnable, long, long, TimeUnit)} (ie. iterations + * past the initial one). + */ + static final String SUBMISSION_PERIODIC_ITERATION = "periodic_iteration"; + + final Scheduler delegate; + + final MeterRegistry registry; + + final Counter submittedDirect; + final Counter submittedDelayed; + final Counter submittedPeriodicInitial; + final Counter submittedPeriodicIteration; + final LongTaskTimer pendingTasks; + final LongTaskTimer activeTasks; + final Timer completedTasks; + + + TimedScheduler(Scheduler delegate, MeterRegistry registry, String metricPrefix, Iterable tagsList) { + this.delegate = delegate; + this.registry = registry; + if (!metricPrefix.endsWith(".")) { + metricPrefix = metricPrefix + "."; + } + Tags tags = Tags.of(tagsList); + + String submittedName = metricPrefix + METER_SUBMITTED; + this.submittedDirect = registry.counter(submittedName, tags.and(TAG_SUBMISSION, SUBMISSION_DIRECT)); + this.submittedDelayed = registry.counter(submittedName, tags.and(TAG_SUBMISSION, SUBMISSION_DELAYED)); + this.submittedPeriodicInitial = registry.counter(submittedName, tags.and(TAG_SUBMISSION, SUBMISSION_PERIODIC_INITIAL)); + this.submittedPeriodicIteration = registry.counter(submittedName, tags.and(TAG_SUBMISSION, SUBMISSION_PERIODIC_ITERATION)); + + this.pendingTasks = LongTaskTimer.builder(metricPrefix + METER_TASKS_PENDING) + .tags(tags).register(registry); + this.activeTasks = LongTaskTimer.builder(metricPrefix + METER_TASKS_ACTIVE) + .tags(tags).register(registry); + this.completedTasks = registry.timer(metricPrefix + METER_TASKS_COMPLETED, tags); + + } + + Runnable wrap(Runnable task) { + return new TimedRunnable(registry, this, task); + } + + Runnable wrapPeriodic(Runnable task) { + return new TimedRunnable(registry, this, task, true); + } + + @Override + public Disposable schedule(Runnable task) { + this.submittedDirect.increment(); + return delegate.schedule(wrap(task)); + } + + @Override + public Disposable schedule(Runnable task, long delay, TimeUnit unit) { + this.submittedDelayed.increment(); + return delegate.schedule(wrap(task), delay, unit); + } + + @Override + public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { + this.submittedPeriodicInitial.increment(); + return delegate.schedulePeriodically(wrapPeriodic(task), initialDelay, period, unit); + } + + @Override + public Worker createWorker() { + return new TimedWorker(this, delegate.createWorker()); + } + + @Override + public boolean isDisposed() { + return delegate.isDisposed(); + } + + @Override + public long now(TimeUnit unit) { + return delegate.now(unit); + } + + @Override + public void dispose() { + delegate.dispose(); + } + + @Override + public void start() { + delegate.start(); + } + + static final class TimedWorker implements Worker { + + final TimedScheduler parent; + final Worker delegate; + + TimedWorker(TimedScheduler parent, Worker delegate) { + this.parent = parent; + this.delegate = delegate; + } + + @Override + public void dispose() { + delegate.dispose(); + } + + @Override + public boolean isDisposed() { + return delegate.isDisposed(); + } + + @Override + public Disposable schedule(Runnable task) { + parent.submittedDirect.increment(); + return delegate.schedule(parent.wrap(task)); + } + + @Override + public Disposable schedule(Runnable task, long delay, TimeUnit unit) { + parent.submittedDelayed.increment(); + return delegate.schedule(parent.wrap(task), delay, unit); + } + + @Override + public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { + parent.submittedPeriodicInitial.increment(); + return delegate.schedulePeriodically(parent.wrapPeriodic(task), initialDelay, period, unit); + } + } + + static final class TimedRunnable implements Runnable { + + final MeterRegistry registry; + final TimedScheduler parent; + final Runnable task; + + final LongTaskTimer.Sample pendingSample; + + boolean isRerun; + + TimedRunnable(MeterRegistry registry, TimedScheduler parent, Runnable task) { + this(registry, parent, task, false); + } + + TimedRunnable(MeterRegistry registry, TimedScheduler parent, Runnable task, boolean periodic) { + this.registry = registry; + this.parent = parent; + this.task = task; + + if (periodic) { + this.pendingSample = null; + } + else { + this.pendingSample = parent.pendingTasks.start(); + } + this.isRerun = false; //will be ignored if not periodic + } + + @Override + public void run() { + if (this.pendingSample != null) { + //NOT periodic + this.pendingSample.stop(); + } + else { + if (!isRerun) { + this.isRerun = true; + } + else { + parent.submittedPeriodicIteration.increment(); + } + } + + Runnable completionTrackingTask = parent.completedTasks.wrap(this.task); + this.parent.activeTasks.record(completionTrackingTask); + } + } +} diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java index f0099ea1f2..1be3adb5d5 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java @@ -16,10 +16,16 @@ package reactor.core.observability.micrometer; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import reactor.core.scheduler.Scheduler; import static org.assertj.core.api.Assertions.assertThat; @@ -41,4 +47,29 @@ void observationContextKeySmokeTest() { assertThat(MicrometerObservationListener.CONTEXT_KEY_OBSERVATION) .isEqualTo(ObservationThreadLocalAccessor.KEY); } + + @Test + void timedSchedulerReturnsAConfiguredTimedScheduler() { + Scheduler mockScheduler = Mockito.mock(Scheduler.class); + Scheduler.Worker mockWorker = Mockito.mock(Scheduler.Worker.class); + Mockito.when(mockScheduler.createWorker()).thenReturn(mockWorker); + + final MeterRegistry registry = new SimpleMeterRegistry(); + final Tags tags = Tags.of("1", "A", "2", "B"); + final String prefix = "testSchedulerMetrics"; + + Scheduler test = Micrometer.timedScheduler(mockScheduler, registry, prefix, tags); + + assertThat(test).isInstanceOfSatisfying(TimedScheduler.class, ts -> { + assertThat(ts.delegate).as("delegate").isSameAs(mockScheduler); + assertThat(ts.registry).as("registry").isSameAs(registry); + //we verify the tags and prefix we passed made it to at least one meter. + //this is more about the Micrometer passing down the params than it is about checking _all_ meters in the actual class. + Meter.Id id = ts.submittedDirect.getId(); + assertThat(id.getName()).as("prefix used") + .isEqualTo("testSchedulerMetrics.scheduler.tasks.submitted"); + assertThat(id.getTags()).as("tags") + .containsExactlyElementsOf(tags.and(TimedScheduler.TAG_SUBMISSION, TimedScheduler.SUBMISSION_DIRECT)); + }); + } } \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java new file mode 100644 index 0000000000..183637b95f --- /dev/null +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import reactor.core.Disposable; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.test.AutoDisposingExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Simon Baslé + */ +class TimedSchedulerTest { + + @RegisterExtension + AutoDisposingExtension afterTest = new AutoDisposingExtension(); + + private SimpleMeterRegistry registry; + + @BeforeEach + void setUp() { + registry = new SimpleMeterRegistry(); + } + + @AfterEach + void closeRegistry() { + registry.close(); + } + + @Test + void constructorAddsDotToPrefixIfNeeded() { + TimedScheduler test = new TimedScheduler(Schedulers.immediate(), registry, "noDot", Tags.empty()); + + assertThat(registry.getMeters()) + .map(m -> m.getId().getName()) + .allSatisfy(name -> assertThat(name).startsWith("noDot.")); + } + + @Test + void constructorDoesntAddTwoDots() { + TimedScheduler test = new TimedScheduler(Schedulers.immediate(), registry, "dot.", Tags.empty()); + + assertThat(registry.getMeters()) + .map(m -> m.getId().getName()) + .allSatisfy(name -> assertThat(name).doesNotContain("..")); + } + + @Test + void constructorRegistersSevenMetersWithFourSimilarCountersWithSubmissionTypeTag() { + MeterRegistryAssert.assertThat(registry).as("before constructor").hasNoMetrics(); + + new TimedScheduler(Schedulers.immediate(), registry, "test", Tags.empty()); + + assertThat(registry.getMeters()) + .map(m -> { + String name = m.getId().getName(); + String type = m.getId().getTag("submission.type"); + return name + (type == null ? "" : " submission.type=" + type); + }) + .containsExactlyInAnyOrder( + "test.scheduler.tasks.active", + "test.scheduler.tasks.completed", + "test.scheduler.tasks.pending", + //technically 4 different submitted counters + "test.scheduler.tasks.submitted submission.type=direct", + "test.scheduler.tasks.submitted submission.type=delayed", + "test.scheduler.tasks.submitted submission.type=periodic_initial", + "test.scheduler.tasks.submitted submission.type=periodic_iteration" + ); + } + + @Test + void timingOfActiveAndPendingTasks() throws InterruptedException { + MockClock virtualClock = new MockClock(); + SimpleMeterRegistry registryWithVirtualClock = new SimpleMeterRegistry(SimpleConfig.DEFAULT, virtualClock); + afterTest.autoDispose(registryWithVirtualClock::close); + TimedScheduler test = new TimedScheduler(Schedulers.single(), registryWithVirtualClock, "test", Tags.empty()); + + /* + This test schedules two tasks in a Schedulers.single(), using latches to "pause" and "resume" the + tasks at points where we can make predictable assertions. As a result, task2 will be pending until + task1 is un-paused. + + LongTaskTimer only report timings and counts of Runnable that are being executed. Once the Runnable + finishes, LTT won't report any activity. + + Timer on the other hand will report cumulative times and counts AFTER the Runnable has finished. + This is used for the completedTasks metric, which is asserted at the end. + */ + + final CountDownLatch firstTaskPause = new CountDownLatch(1); + final CountDownLatch secondTaskDone = new CountDownLatch(1); + test.schedule(() -> { + try { + firstTaskPause.await(1, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + test.schedule(() -> { + try { + virtualClock.addSeconds(1); + } + finally { + secondTaskDone.countDown(); + } + }); + + //there might be a slight hiccup when the registry doesn't see task1 as active + Awaitility.await().atMost(Duration.ofSeconds(1)).untilAsserted( + () -> assertThat(test.activeTasks.activeTasks()).as("one active").isOne()); + assertThat(test.pendingTasks.activeTasks()).as("one idle").isOne(); + + //we advance time by 2s, expecting that pendingTasks and activeTasks both reflect these 2 seconds (for task2 and task1 respectively) + virtualClock.addSeconds(2); + + assertThat(test.pendingTasks.duration(TimeUnit.SECONDS)) + .as("after 1st idle totalTime SECONDS") + .isEqualTo(2); + assertThat(test.activeTasks.duration(TimeUnit.SECONDS)) + .as("after 1st active totalTime SECONDS") + .isEqualTo(2); + + // we "resume" both tasks and let them finish, at which point the LongTaskTimers will stop recording + firstTaskPause.countDown(); + secondTaskDone.await(1, TimeUnit.SECONDS); + + //again, there might be a slight hiccup before registry sees 2nd task as done + Awaitility.await().atMost(Duration.ofSeconds(1)).untilAsserted( + () -> assertThat(test.activeTasks.duration(TimeUnit.SECONDS)) + .as("once 2nd done, no active timing") + .isEqualTo(0)); + assertThat(test.pendingTasks.duration(TimeUnit.SECONDS)) + .as("once 2nd done, no pending timing") + .isEqualTo(0); + assertThat(test.pendingTasks.activeTasks()).as("at end pendingTasks").isZero(); + assertThat(test.activeTasks.activeTasks()).as("at end activeTasks").isZero(); + + //now we assert that the completedTasks timer reflects a history of all Runnable#run + assertThat(test.completedTasks.count()) + .as("#completed") + .isEqualTo(2L); + assertThat(test.completedTasks.totalTime(TimeUnit.MILLISECONDS)) + .as("total duration of tasks") + .isEqualTo(3000); + } + + @Test + void schedulePeriodicallyTimesOneRunInActiveAndAllRunsInCompleted() throws InterruptedException { + MockClock virtualClock = new MockClock(); + SimpleMeterRegistry registryWithVirtualClock = new SimpleMeterRegistry(SimpleConfig.DEFAULT, virtualClock); + TimedScheduler test = new TimedScheduler(Schedulers.single(), registryWithVirtualClock, "test", Tags.empty()); + + //schedule a periodic task for which one run takes 500ms. we cancel after 3 runs + CountDownLatch latch = new CountDownLatch(3); + Disposable d = test.schedulePeriodically( + () -> { + try { + virtualClock.add(Duration.ofMillis(500)); + } + finally { + latch.countDown(); + } + }, + 100, 100, TimeUnit.MILLISECONDS); + latch.await(1, TimeUnit.SECONDS); + d.dispose(); + + //now we assert that the completedTasks timer reflects a history of all Runnable#run + assertThat(test.submittedDirect.count()).as("#submittedDirect").isZero(); + assertThat(test.submittedPeriodicInitial.count()).as("#submittedPeriodicInitial").isOne(); + assertThat(test.submittedPeriodicIteration.count()).as("#submittedPeriodicIteration").isEqualTo(2); + assertThat(test.completedTasks.count()) + .as("#completed") + .isEqualTo(3L); + assertThat(test.completedTasks.totalTime(TimeUnit.MILLISECONDS)) + .as("total duration of tasks") + .isEqualTo(1500); + } + + @Test + void scheduleIncrementDirectCounterOnly() { + TimedScheduler test = new TimedScheduler(Schedulers.immediate(), registry, "test", Tags.empty()); + + test.schedule(() -> {}); + + assertThat(test.submittedDirect.count()).as("submittedDirect.count").isOne(); + assertThat(test.submittedDelayed.count()).as("submittedDelayed.count").isZero(); + assertThat(test.submittedPeriodicInitial.count()).as("submittedPeriodicInitial.count").isZero(); + assertThat(test.submittedPeriodicIteration.count()).as("submittedPeriodicIteration.count").isZero(); + } + + @Test + void scheduleDelayIncrementsDelayedCounter() throws InterruptedException { + TimedScheduler test = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); + + test.schedule(() -> {}, 100, TimeUnit.MILLISECONDS); + + assertThat(test.submittedDirect.count()).as("submittedDirect.count").isZero(); + assertThat(test.submittedDelayed.count()).as("submittedDelayed.count").isOne(); + assertThat(test.submittedPeriodicInitial.count()).as("submittedPeriodicInitial.count").isZero(); + assertThat(test.submittedPeriodicIteration.count()).as("submittedPeriodicIteration.count").isZero(); + } + + @Test + void schedulePeriodicallyIsCorrectlyMetered() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(5); + TimedScheduler test = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); + + Disposable d = test.schedulePeriodically(latch::countDown, 100, 100, TimeUnit.MILLISECONDS); + + latch.await(10, TimeUnit.SECONDS); + d.dispose(); + + assertThat(test.submittedDirect.count()).as("submittedDirect.count").isZero(); + assertThat(test.submittedDelayed.count()).as("submittedDelayed.count").isZero(); + assertThat(test.submittedPeriodicInitial.count()).as("submittedPeriodicInitial.count").isOne(); + assertThat(test.submittedPeriodicIteration.count()).as("submittedPeriodicIteration.count").isEqualTo(4); + assertThat(test.completedTasks.count()) + .as("completed counter tracks all iterations") + .isEqualTo(5) + .matches(l -> l == test.submittedDirect.count() + test.submittedDelayed.count() + test.submittedPeriodicInitial.count() + + test.submittedPeriodicIteration.count(), "completed tasks == sum of all timer counts"); + } + + @Test + void createWorkerDelegatesToAnOriginalWorker() { + Scheduler mockScheduler = Mockito.mock(Scheduler.class); + Scheduler.Worker mockWorker = Mockito.mock(Scheduler.Worker.class); + Mockito.when(mockScheduler.createWorker()).thenReturn(mockWorker); + + TimedScheduler test = new TimedScheduler(mockScheduler, registry, "test", Tags.empty()); + + TimedScheduler.TimedWorker worker = (TimedScheduler.TimedWorker) test.createWorker(); + + assertThat(worker.delegate).as("worker delegate").isSameAs(mockWorker); + } + + @Test + void workerScheduleIncrementsDirectCounterOnly() { + TimedScheduler testScheduler = new TimedScheduler(Schedulers.immediate(), registry, "test", Tags.empty()); + Scheduler.Worker test = testScheduler.createWorker(); + + test.schedule(() -> {}); + + assertThat(testScheduler.submittedDirect.count()).as("submittedDirect.count").isOne(); + assertThat(testScheduler.submittedDelayed.count()).as("submittedDelayed.count").isZero(); + assertThat(testScheduler.submittedPeriodicInitial.count()).as("submittedPeriodicInitial.count").isZero(); + assertThat(testScheduler.submittedPeriodicIteration.count()).as("submittedPeriodicIteration.count").isZero(); + } + + @Test + void workerScheduleDelayIncrementsDelayedCounter() throws InterruptedException { + TimedScheduler testScheduler = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); + Scheduler.Worker test = testScheduler.createWorker(); + + test.schedule(() -> {}, 100, TimeUnit.MILLISECONDS); + + assertThat(testScheduler.submittedDirect.count()).as("submittedDirect.count").isZero(); + assertThat(testScheduler.submittedDelayed.count()).as("submittedDelayed.count").isOne(); + assertThat(testScheduler.submittedPeriodicInitial.count()).as("submittedPeriodicInitial.count").isZero(); + assertThat(testScheduler.submittedPeriodicIteration.count()).as("submittedPeriodicIteration.count").isZero(); + } + + @Test + void workerSchedulePeriodicallyIsCorrectlyMetered() throws InterruptedException { + Scheduler original = Schedulers.single(); + CountDownLatch latch = new CountDownLatch(5); + TimedScheduler testScheduler = new TimedScheduler(original, registry, "test", Tags.empty()); + Scheduler.Worker test = testScheduler.createWorker(); + + Disposable d = test.schedulePeriodically(latch::countDown, 100, 100, TimeUnit.MILLISECONDS); + + latch.await(10, TimeUnit.SECONDS); + d.dispose(); + + assertThat(testScheduler.submittedDirect.count()).as("submittedDirect.count").isZero(); + assertThat(testScheduler.submittedDelayed.count()).as("submittedDelayed.count").isZero(); + assertThat(testScheduler.submittedPeriodicInitial.count()).as("submittedPeriodicInitial.count").isOne(); + assertThat(testScheduler.submittedPeriodicIteration.count()).as("submittedPeriodicIteration.count").isEqualTo(4); + assertThat(testScheduler.completedTasks.count()) + .as("completed counter tracks all iterations") + .isEqualTo(5) + .matches(l -> l == testScheduler.submittedDirect.count() + + testScheduler.submittedDelayed.count() + + testScheduler.submittedPeriodicInitial.count() + + testScheduler.submittedPeriodicIteration.count(), "completed tasks == sum of all timer counts"); + } +} \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java b/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java index adbdd1fac1..b9d32465a9 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/SchedulerMetricDecorator.java @@ -34,7 +34,7 @@ import reactor.core.Scannable.Attr; import reactor.util.Metrics; -@Deprecated //this class is duplicated in reactor-core-micrometer +@Deprecated final class SchedulerMetricDecorator implements BiFunction, Disposable { diff --git a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java index 5ba30fdd3c..2b962c1b11 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java @@ -606,7 +606,7 @@ public static boolean isNonBlockingThread(Thread t) { *

    * * @implNote Note that this is added as a decorator via Schedulers when enabling metrics for schedulers, which doesn't change the Factory. - * @deprecated prefer using the equivalent method in reactor-core-micrometer module. To be removed at the earliest in 3.6.0. + * @deprecated prefer using Micrometer#timedScheduler from the reactor-core-micrometer module. To be removed at the earliest in 3.6.0. */ @Deprecated public static void enableMetrics() { @@ -619,7 +619,7 @@ public static void enableMetrics() { * If {@link #enableMetrics()} has been previously called, removes the decorator. * No-op if {@link #enableMetrics()} hasn't been called. * - * @deprecated prefer using the equivalent method in reactor-core-micrometer module. To be removed at the earliest in 3.6.0. + * @deprecated prefer using Micrometer#timedScheduler from the reactor-core-micrometer module. To be removed at the earliest in 3.6.0. */ @Deprecated public static void disableMetrics() { diff --git a/reactor-core/src/test/java/reactor/core/scheduler/SingleSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/SingleSchedulerTest.java index 56d10a149f..4a17fb50c6 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/SingleSchedulerTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/SingleSchedulerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,17 @@ package reactor.core.scheduler; import static java.lang.String.format; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.lang.management.ManagementFactory; import java.time.Duration; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.junit.jupiter.api.Test; + +import reactor.core.Disposable; import reactor.core.Scannable; import reactor.core.publisher.*; import reactor.core.scheduler.Scheduler.Worker; @@ -130,6 +133,57 @@ public void lotsOfTasks() throws Exception { } + @Test + void independentWorkers() throws InterruptedException { + Scheduler timer = afterTest.autoDispose(Schedulers.newSingle("test-timer")); + + Worker w1 = timer.createWorker(); + + Worker w2 = timer.createWorker(); + + CountDownLatch cdl = new CountDownLatch(1); + + w1.dispose(); + + assertThatExceptionOfType(Throwable.class).isThrownBy(() -> { + w1.schedule(() -> { }); + }); + + w2.schedule(cdl::countDown); + + if (!cdl.await(1, TimeUnit.SECONDS)) { + fail("Worker 2 didn't execute in time"); + } + w2.dispose(); + } + + @Test + void massCancel() { + Scheduler timer = afterTest.autoDispose(Schedulers.newSingle("test-timer")); + Worker w1 = timer.createWorker(); + + AtomicInteger counter = new AtomicInteger(); + + Runnable task = counter::getAndIncrement; + + int tasks = 10; + + Disposable[] c = new Disposable[tasks]; + + for (int i = 0; i < tasks; i++) { + c[i] = w1.schedulePeriodically(task, 500, 500, TimeUnit.MILLISECONDS); + } + + w1.dispose(); + + for (int i = 0; i < tasks; i++) { + assertThat(c[i].isDisposed()).isTrue(); + } + + assertThat(counter).hasValue(0); + } + + @Test public void scanName() { Scheduler withNamedFactory = Schedulers.newSingle("scanName"); diff --git a/reactor-core/src/test/java/reactor/core/scheduler/TimedSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/TimedSchedulerTest.java deleted file mode 100644 index 5f7dd1d3a8..0000000000 --- a/reactor-core/src/test/java/reactor/core/scheduler/TimedSchedulerTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.core.scheduler; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Test; -import reactor.core.Disposable; -import reactor.core.scheduler.Scheduler.Worker; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; - -public class TimedSchedulerTest extends AbstractSchedulerTest { - - @Override - protected Scheduler scheduler() { - return Schedulers.newSingle("TimedSchedulerTest"); - } - - @Test - public void independentWorkers() throws InterruptedException { - Scheduler timer = Schedulers.newSingle("test-timer"); - - try { - Worker w1 = timer.createWorker(); - - Worker w2 = timer.createWorker(); - - CountDownLatch cdl = new CountDownLatch(1); - - w1.dispose(); - - assertThatExceptionOfType(Throwable.class).isThrownBy(() -> { - w1.schedule(() -> { }); - }); - - w2.schedule(cdl::countDown); - - if (!cdl.await(1, TimeUnit.SECONDS)) { - fail("Worker 2 didn't execute in time"); - } - w2.dispose(); - } finally { - timer.dispose(); - } - } - - @Test - public void massCancel() throws InterruptedException { - Scheduler timer = Schedulers.newSingle("test-timer"); - - try { - Worker w1 = timer.createWorker(); - - AtomicInteger counter = new AtomicInteger(); - - Runnable task = counter::getAndIncrement; - - int tasks = 10; - - Disposable[] c = new Disposable[tasks]; - - for (int i = 0; i < tasks; i++) { - c[i] = w1.schedulePeriodically(task, 500, 500, TimeUnit.MILLISECONDS); - } - - w1.dispose(); - - for (int i = 0; i < tasks; i++) { - assertThat(c[i].isDisposed()).isTrue(); - } - - assertThat(counter).hasValue(0); - } - finally { - timer.dispose(); - } - } - -} From 394a5382bd28ca8a713378e1c7cdd127cd4a1e0a Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 9 Aug 2022 10:11:44 +0300 Subject: [PATCH 048/312] Update Micrometer dependency to latest milestone releases (#3143) Update Micrometer Tracing dependency to version 1.0.0-M7 Update Context Propagation dependency to version 1.0.0-M4 Update Micrometer dependency to version 1.10.0-M4 --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10e939c55d..418858f6da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.12" jmh = "1.35" junit = "5.8.2" #note that context-propagation has a different version directly set in libraries -micrometer = "1.10.0-SNAPSHOT" # was -M3 +micrometer = "1.10.0-M4" reactiveStreams = "1.0.4" [libraries] @@ -35,9 +35,9 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" # was -M3 +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-M4" micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" # was -M6 +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-M7" micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.6.1" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 68fd8ff6d4687b4b77e360996d53e00a96fceaf7 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 9 Aug 2022 14:21:16 +0200 Subject: [PATCH 049/312] [release] Prepare and release 3.5.0-M5 --- README.md | 6 +++--- gradle.properties | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 78b7847941..f2159c5a6e 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.0-M4" - testCompile "io.projectreactor:reactor-test:3.5.0-M4" + compile "io.projectreactor:reactor-core:3.5.0-M5" + testCompile "io.projectreactor:reactor-test:3.5.0-M5" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.5.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.5.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.0-M4" + // implementation "io.projectreactor:reactor-tools:3.5.0-M5" } ``` diff --git a/gradle.properties b/gradle.properties index 6ef2f59482..ef57c97230 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-SNAPSHOT -bomVersion=2022.0.0-M4 -metricsMicrometerVersion=1.0.0-SNAPSHOT +version=3.5.0-M5 +bomVersion=2022.0.0-M5 +metricsMicrometerVersion=1.0.0-M5 From c7729f13aff5182d9272a5bc7f2fd96359f07630 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 9 Aug 2022 15:15:07 +0200 Subject: [PATCH 050/312] [release] Next development version 3.5.0-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index ef57c97230..00798b2bcd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-M5 +version=3.5.0-SNAPSHOT bomVersion=2022.0.0-M5 -metricsMicrometerVersion=1.0.0-M5 +metricsMicrometerVersion=1.0.0-SNAPSHOT From cd75a8ea29b1d89fa65ee5599e6fa4a2dc43b4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 26 Aug 2022 10:59:34 +0200 Subject: [PATCH 051/312] Removing outdated references to Exceptions#bubble(Throwable) (#3160) --- .../src/main/java/reactor/core/publisher/Operators.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Operators.java b/reactor-core/src/main/java/reactor/core/publisher/Operators.java index 9873dcd636..61365fd0f5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Operators.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Operators.java @@ -622,7 +622,7 @@ public static void onDiscardMultiple(@Nullable Iterator multiple, boolean kno * An unexpected exception is about to be dropped. *

    * If no hook is registered for {@link Hooks#onErrorDropped(Consumer)}, the dropped - * error is logged at ERROR level and thrown (via {@link Exceptions#bubble(Throwable)}. + * error is logged at ERROR level. * * @param e the dropped exception * @param context a context that might hold a local error consumer @@ -746,7 +746,7 @@ public static Throwable onOperatorError(@Nullable Subscription subscription, * operator. This exception denotes that an execution was rejected by a * {@link reactor.core.scheduler.Scheduler}, notably when it was already disposed. *

    - * Wrapping is done by calling both {@link Exceptions#bubble(Throwable)} and + * Wrapping is done by calling both {@link Exceptions#failWithRejected(Throwable)} and * {@link #onOperatorError(Subscription, Throwable, Object, Context)}. * * @param original the original execution error From 089b8dfe0b03df8f28ed8c5d4c5407c24bc6336c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 29 Aug 2022 17:50:27 +0200 Subject: [PATCH 052/312] SourceException wrapper for main stream errors delivered to windows (#3167) In case of window operators, it might be beneficial to distinguish between errors coming from the window chain of operators and errors coming from the source sequence. `Exceptions.SourceException` has been added and `Throwable`s coming from either the source sequence, or, in case of window operators that accept `Publisher`s to drive windowing, their errors also are wrapped. This is a behavior change and if existing logic relied on the properties of the delivered `Throwable` it will require reaching for the `Throwable#getCause()` in case of `Exceptions.SourceException`. An additional behavior change is in case of `FluxWindowWhen`, which did not deliver source errors to the windows, despite seemingly being intended by the implementation. Resolves #2041 --- .../main/java/reactor/core/Exceptions.java | 29 +++++ .../java/reactor/core/publisher/Flux.java | 100 ++++++++++++++++++ .../reactor/core/publisher/FluxWindow.java | 10 +- .../core/publisher/FluxWindowBoundary.java | 7 +- .../core/publisher/FluxWindowPredicate.java | 6 +- .../core/publisher/FluxWindowTimeout.java | 4 +- .../core/publisher/FluxWindowWhen.java | 11 +- .../publisher/FluxWindowBoundaryTest.java | 12 ++- .../publisher/FluxWindowPredicateTest.java | 20 ++-- .../core/publisher/FluxWindowTest.java | 14 +-- .../core/publisher/FluxWindowTimeoutTest.java | 31 +++++- .../core/publisher/FluxWindowWhenTest.java | 32 ++++++ .../test/publisher/BaseOperatorTest.java | 6 +- 13 files changed, 242 insertions(+), 40 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/Exceptions.java b/reactor-core/src/main/java/reactor/core/Exceptions.java index bfacf990ea..8328ca05a6 100644 --- a/reactor-core/src/main/java/reactor/core/Exceptions.java +++ b/reactor-core/src/main/java/reactor/core/Exceptions.java @@ -102,6 +102,18 @@ public static boolean addThrowable(AtomicReferenceFieldUpdater } } + /** + * Wrap a {@link Throwable} delivered via {@link org.reactivestreams.Subscriber#onError(Throwable)} + * from an upstream {@link org.reactivestreams.Publisher} that itself + * emits {@link org.reactivestreams.Publisher}s to distinguish the error signal from + * the inner sequence's processing errors. + * @param throwable the source sequence {@code error} signal + * @return {@link SourceException} + */ + public static Throwable wrapSource(Throwable throwable) { + return new SourceException(throwable); + } + /** * Create a composite exception that wraps the given {@link Throwable Throwable(s)}, * as suppressed exceptions. Instances create by this method can be detected using the @@ -725,6 +737,23 @@ public synchronized Throwable fillInStackTrace() { private static final long serialVersionUID = 2491425227432776143L; } + /** + * A {@link Throwable} that wraps the actual {@code cause} delivered via + * {@link org.reactivestreams.Subscriber#onError(Throwable)} in case of + * {@link org.reactivestreams.Publisher}s that themselves emit items of type + * {@link org.reactivestreams.Publisher}. This wrapper is used to distinguish + * {@code error}s delivered by the upstream sequence from the ones that happen via + * the inner sequence processing chain. + */ + public static class SourceException extends ReactiveException { + + SourceException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = 5747581575202629465L; + } + static final class ErrorCallbackNotImplemented extends UnsupportedOperationException { ErrorCallbackNotImplemented(Throwable cause) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index da8aff60dc..fe3fafedf7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -9629,6 +9629,11 @@ public final Flux transformDeferredContextual(BiFunction, * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. * @@ -9664,6 +9669,11 @@ public final Flux> window(int maxSize) { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: The overlapping variant DOES NOT discard elements, as they might be part of another still valid window. * The exact window and dropping window variants bot discard elements they internally queued for backpressure * upon cancellation or error triggered by a data signal. The dropping window variant also discards elements in between windows. @@ -9695,6 +9705,11 @@ public final Flux> window(int maxSize, int skip) { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors and those emitted by the {@code boundary} delivered to the window + * {@link Flux} are wrapped in {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. * @@ -9722,6 +9737,11 @@ public final Flux> window(Publisher boundary) { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. * @@ -9760,6 +9780,11 @@ public final Flux> window(Duration windowingTimespan) { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: The overlapping variant DOES NOT discard elements, as they might be part of another still valid window. * The exact window and dropping window variants bot discard elements they internally queued for backpressure * upon cancellation or error triggered by a data signal. The dropping window variant also discards elements in between windows. @@ -9789,6 +9814,11 @@ public final Flux> window(Duration windowingTimespan, Duration openWindo * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. * @@ -9828,6 +9858,11 @@ public final Flux> window(Duration windowingTimespan, Scheduler timer) { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: The overlapping variant DOES NOT discard elements, as they might be part of another still valid window. * The exact window and dropping window variants bot discard elements they internally queued for backpressure * upon cancellation or error triggered by a data signal. The dropping window variant also discards elements in between windows. @@ -9863,6 +9898,11 @@ public final Flux> window(Duration windowingTimespan, Duration openWindo * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. * @@ -9892,6 +9932,11 @@ public final Flux> windowTimeout(int maxSize, Duration maxTime) { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. * @@ -9923,6 +9968,11 @@ public final Flux> windowTimeout(int maxSize, Duration maxTime, boolean * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. * @@ -9953,6 +10003,11 @@ public final Flux> windowTimeout(int maxSize, Duration maxTime, Schedule * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. * @@ -9988,6 +10043,11 @@ public final Flux> windowTimeout(int maxSize, Duration maxTime, Schedule * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. Upon cancellation of the current window, * it also discards the remaining elements that were bound for it until the main sequence completes @@ -10025,6 +10085,11 @@ public final Flux> windowUntil(Predicate boundaryTrigger) { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. Upon cancellation of the current window, * it also discards the remaining elements that were bound for it until the main sequence completes @@ -10064,6 +10129,11 @@ public final Flux> windowUntil(Predicate boundaryTrigger, boolean cut * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. Upon cancellation of the current window, * it also discards the remaining elements that were bound for it until the main sequence completes @@ -10098,6 +10168,11 @@ public final Flux> windowUntil(Predicate boundaryTrigger, boolean cut * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. Upon cancellation of the current window, * it also discards the remaining elements that were bound for it until the main sequence completes @@ -10124,6 +10199,11 @@ public final Flux> windowUntilChanged() { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. Upon cancellation of the current window, * it also discards the remaining elements that were bound for it until the main sequence completes @@ -10152,6 +10232,11 @@ public final Flux> windowUntilChanged(Function * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal. Upon cancellation of the current window, * it also discards the remaining elements that were bound for it until the main sequence completes @@ -10187,6 +10272,11 @@ public final Flux> windowUntilChanged(Function + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal, as well as the triggering element(s) (that doesn't match * the predicate). Upon cancellation of the current window, it also discards the remaining elements @@ -10220,6 +10310,11 @@ public final Flux> windowWhile(Predicate inclusionPredicate) { * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator discards elements it internally queued for backpressure * upon cancellation or error triggered by a data signal, as well as the triggering element(s) (that doesn't match * the predicate). Upon cancellation of the current window, it also discards the remaining elements @@ -10259,6 +10354,11 @@ public final Flux> windowWhile(Predicate inclusionPredicate, int pref * This is most noticeable when trying to {@link #retry()} or {@link #repeat()} a window, * as these operators are based on re-subscription. * + *

    + * To distinguish errors emitted by the processing of individual windows, source + * sequence errors delivered to the window {@link Flux} are wrapped in + * {@link reactor.core.Exceptions.SourceException}. + * *

    Discard Support: This operator DOES NOT discard elements. * * @param bucketOpening a {@link Publisher} that opens a new window when it emits any item diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindow.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindow.java index eb0bb29e48..229aa4cc41 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindow.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindow.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,8 @@ import reactor.util.annotation.Nullable; import reactor.util.context.Context; +import static reactor.core.Exceptions.wrapSource; + /** * Splits the source sequence into possibly overlapping publishers. * @@ -195,7 +197,7 @@ public void onError(Throwable t) { Sinks.Many w = window; if (w != null) { window = null; - w.emitError(t, Sinks.EmitFailureHandler.FAIL_FAST); + w.emitError(wrapSource(t), Sinks.EmitFailureHandler.FAIL_FAST); } actual.onError(t); @@ -376,7 +378,7 @@ public void onError(Throwable t) { Sinks.Many w = window; if (w != null) { window = null; - w.emitError(t, Sinks.EmitFailureHandler.FAIL_FAST); + w.emitError(wrapSource(t), Sinks.EmitFailureHandler.FAIL_FAST); } actual.onError(t); @@ -586,7 +588,7 @@ public void onError(Throwable t) { done = true; for (Sinks.Many w : this) { - w.emitError(t, Sinks.EmitFailureHandler.FAIL_FAST); + w.emitError(wrapSource(t), Sinks.EmitFailureHandler.FAIL_FAST); } clear(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowBoundary.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowBoundary.java index 646f396cce..f3a1954c5d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowBoundary.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowBoundary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,8 @@ import reactor.util.concurrent.Queues; import reactor.util.context.Context; +import static reactor.core.Exceptions.wrapSource; + /** * Splits the source sequence into continuous, non-overlapping windowEnds * where the window boundary is signalled by another Publisher @@ -294,7 +296,8 @@ void drain() { q.clear(); Throwable e = Exceptions.terminate(ERROR, this); if (e != Exceptions.TERMINATED) { - w.emitError(e, Sinks.EmitFailureHandler.FAIL_FAST); + w.emitError(wrapSource(e), + Sinks.EmitFailureHandler.FAIL_FAST); a.onError(e); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowPredicate.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowPredicate.java index 29d4d3180a..53d44cdc13 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowPredicate.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowPredicate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,8 @@ import reactor.util.annotation.Nullable; import reactor.util.context.Context; +import static reactor.core.Exceptions.wrapSource; + /** * Cut a sequence into non-overlapping windows where each window boundary is determined by * a {@link Predicate} on the values. The predicate can be used in several modes: @@ -347,7 +349,7 @@ void signalAsyncError() { windowCount = 0; WindowFlux g = window; if (g != null) { - g.onError(e); + g.onError(wrapSource(e)); } actual.onError(e); window = null; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowTimeout.java index 530b311616..3b1c346c66 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowTimeout.java @@ -36,6 +36,8 @@ import reactor.util.concurrent.Queues; import reactor.util.context.Context; +import static reactor.core.Exceptions.wrapSource; + /** * @author David Karnok */ @@ -207,7 +209,7 @@ public void onError(Throwable t) { final InnerWindow window = this.window; if (window != null) { - window.sendError(t); + window.sendError(wrapSource(t)); if (hasUnsentWindow(previousState)) { return; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowWhen.java index 62b91168af..cc92466e16 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,8 @@ import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; +import static reactor.core.Exceptions.wrapSource; + /** * Splits the source sequence into potentially overlapping windowEnds controlled by items * of a start Publisher and end Publishers derived from the start values. @@ -235,15 +237,16 @@ void drainLoop() { dispose(); Throwable e = error; if (e != null) { - actual.onError(e); for (Sinks.Many w : ws) { - w.emitError(e, Sinks.EmitFailureHandler.FAIL_FAST); + w.emitError(wrapSource(e), + Sinks.EmitFailureHandler.FAIL_FAST); } + actual.onError(e); } else { - actual.onComplete(); for (Sinks.Many w : ws) { w.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST); } + actual.onComplete(); } ws.clear(); return; diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowBoundaryTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowBoundaryTest.java index aedcdd569b..a4e877cdbb 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowBoundaryTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowBoundaryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -152,8 +152,9 @@ public void mainError() { toList(ts.values() .get(1)).assertValues(4, 5) - .assertError(RuntimeException.class) - .assertErrorMessage("forced failure") + .assertErrorWith(e -> assertThat(e.getCause()) + .isInstanceOf(RuntimeException.class) + .hasMessage("forced failure")) .assertNotComplete(); ts.assertError(RuntimeException.class) @@ -194,8 +195,9 @@ public void otherError() { toList(ts.values() .get(1)).assertValues(4, 5) - .assertError(RuntimeException.class) - .assertErrorMessage("forced failure") + .assertErrorWith(e -> assertThat(e.getCause()) + .isInstanceOf(RuntimeException.class) + .hasMessage("forced failure")) .assertNotComplete(); ts.assertError(RuntimeException.class) diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowPredicateTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowPredicateTest.java index e5d0f62aa2..58c19b955a 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowPredicateTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowPredicateTest.java @@ -538,7 +538,7 @@ public void mainErrorUntilIsPropagatedToBothWindowAndMain() { .expectNext(Signal.next(4)) .then(() -> sp1.emitError(new RuntimeException("forced failure"), FAIL_FAST)) //this is the error in the window: - .expectNextMatches(signalErrorMessage("forced failure")) + .expectNextMatches(signalSourceErrorMessage("forced failure")) //this is the error in the main: .expectErrorMessage("forced failure") .verify(); @@ -567,7 +567,7 @@ public void predicateErrorUntil() { .expectNext(Signal.next(4)) .then(() -> sp1.emitNext(5, FAIL_FAST)) //error in the window: - .expectNextMatches(signalErrorMessage("predicate failure")) + .expectNextMatches(signalSourceErrorMessage("predicate failure")) .expectErrorMessage("predicate failure") .verify(); assertThat(sp1.currentSubscriberCount()).as("sp1 has subscriber").isZero(); @@ -624,7 +624,7 @@ public void mainErrorUntilCutBeforeIsPropagatedToBothWindowAndMain() { .expectNext(Signal.next(4)) .then(() -> sp1.emitError(new RuntimeException("forced failure"), FAIL_FAST)) //this is the error in the window: - .expectNextMatches(signalErrorMessage("forced failure")) + .expectNextMatches(signalSourceErrorMessage("forced failure")) //this is the error in the main: .expectErrorMessage("forced failure") .verify(); @@ -653,16 +653,18 @@ public void predicateErrorUntilCutBefore() { .expectNext(Signal.next(4)) .then(() -> sp1.emitNext(5, FAIL_FAST)) //error in the window: - .expectNextMatches(signalErrorMessage("predicate failure")) + .expectNextMatches(signalSourceErrorMessage("predicate failure")) + //this is the error in the main: .expectErrorMessage("predicate failure") .verify(); assertThat(sp1.currentSubscriberCount()).as("sp1 has subscriber").isZero(); } - private Predicate> signalErrorMessage(String expectedMessage) { + private Predicate> signalSourceErrorMessage(String expectedMessage) { return signal -> signal.isOnError() && signal.getThrowable() != null - && expectedMessage.equals(signal.getThrowable().getMessage()); + && signal.getThrowable().getCause() != null + && expectedMessage.equals(signal.getThrowable().getCause().getMessage()); } @Test @@ -770,7 +772,7 @@ public void normalWhileDoesntMatch() { } @Test - public void mainErrorWhileIsPropagatedToBothWindowAndMain() { + public void mainErrorWhileIsPropagatedOnlyToMain() { Sinks.Many sp1 = Sinks.unsafe().many().multicast().directBestEffort(); FluxWindowPredicate windowWhile = new FluxWindowPredicate<>( sp1.asFlux(), Queues.small(), Queues.unbounded(), Queues.SMALL_BUFFER_SIZE, @@ -837,8 +839,8 @@ public void predicateErrorWhile() { .expectNext(Signal.next(3)) .then(() -> sp1.emitNext(4, FAIL_FAST)) //previous window closes, new (empty) window .expectNext(Signal.complete()) - .then(() -> sp1.emitNext(5, FAIL_FAST)) //fails, the empty window receives onError - //error in the window: + .then(() -> sp1.emitNext(5, FAIL_FAST)) //fails, the empty window is discarded + //error in the main: .expectErrorMessage("predicate failure") .verify(ofMillis(100)); assertThat(sp1.currentSubscriberCount()).as("sp1 has subscriber").isZero(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTest.java index 753183006a..221c68ec56 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -466,8 +466,8 @@ public void exactError() { toList(ts.values() .get(0)).assertValues(1) .assertNotComplete() - .assertError(RuntimeException.class) - .assertErrorMessage("forced failure"); + .assertErrorWith(t -> assertThat(t.getCause()) + .isInstanceOf(RuntimeException.class).hasMessage("forced failure")); } @Test @@ -495,8 +495,8 @@ public void skipError() { toList(ts.values() .get(0)).assertValues(1) .assertNotComplete() - .assertError(RuntimeException.class) - .assertErrorMessage("forced failure"); + .assertErrorWith(t -> assertThat(t.getCause()) + .isInstanceOf(RuntimeException.class).hasMessage("forced failure")); } @Test @@ -550,8 +550,8 @@ public void overlapError() { toList(ts.values() .get(0)).assertValues(1) .assertNotComplete() - .assertError(RuntimeException.class) - .assertErrorMessage("forced failure"); + .assertErrorWith(t -> assertThat(t.getCause()) + .isInstanceOf(RuntimeException.class).hasMessage("forced failure")); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTimeoutTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTimeoutTest.java index 08737ea734..985d35ef1b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTimeoutTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowTimeoutTest.java @@ -17,11 +17,11 @@ package reactor.core.publisher; import java.time.Duration; -import java.util.List; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.stream.LongStream; import org.assertj.core.api.Assertions; @@ -35,6 +35,7 @@ import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -469,4 +470,32 @@ public void scanMainSubscriber() { Assertions.assertThat(test.scan(Scannable.Attr.CANCELLED)) .isTrue(); } + + @Test + public void sourceError() { + TestPublisher source = TestPublisher.create(); + + StepVerifier.create(source.flux() + .windowTimeout(2, Duration.ofSeconds(1), true) + .flatMap(Flux::materialize) + ) + .then(() -> source.next(1)) + .expectNext(Signal.next(1)) + .then(() -> source.error( + new IllegalStateException("expected failure"))) + // failure observed by window + .expectNextMatches(signalSourceErrorMessage("expected failure")) + // failure observed by main Flux + .expectErrorMessage("expected failure") + .verify(); + + source.assertNoSubscribers(); + } + + private Predicate> signalSourceErrorMessage(String expectedMessage) { + return signal -> signal.isOnError() + && signal.getThrowable() != null + && signal.getThrowable().getCause() != null + && expectedMessage.equals(signal.getThrowable().getCause().getMessage()); + } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowWhenTest.java index e9e1eb171d..e92448a0f8 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxWindowWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxWindowWhenTest.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.LongAdder; +import java.util.function.Predicate; import org.assertj.core.api.Assertions; import org.assertj.core.api.Condition; @@ -355,6 +356,37 @@ public void windowWillSubdivideAnInputFluxGapTime() { .verifyComplete(); } + @Test + public void sourceError() { + TestPublisher source = TestPublisher.create(); + TestPublisher start = TestPublisher.create(); + TestPublisher end = TestPublisher.create(); + + StepVerifier.create(source.flux() + .windowWhen(start, v -> end) + .flatMap(Flux::materialize) + ) + .then(() -> start.next(1)) + .then(() -> source.error( + new IllegalStateException("expected failure"))) + // failure observerd by window + .expectNextMatches(signalSourceErrorMessage("expected failure")) + // failure observed by main Flux + .expectErrorMessage("expected failure") + .verify(); + + source.assertNoSubscribers(); + start.assertNoSubscribers(); + end.assertNoSubscribers(); + } + + private Predicate> signalSourceErrorMessage(String expectedMessage) { + return signal -> signal.isOnError() + && signal.getThrowable() != null + && signal.getThrowable().getCause() != null + && expectedMessage.equals(signal.getThrowable().getCause().getMessage()); + } + @Test public void startError() { TestPublisher source = TestPublisher.create(); diff --git a/reactor-core/src/test/java/reactor/test/publisher/BaseOperatorTest.java b/reactor-core/src/test/java/reactor/test/publisher/BaseOperatorTest.java index cd6cc68d40..ded2f2a375 100644 --- a/reactor-core/src/test/java/reactor/test/publisher/BaseOperatorTest.java +++ b/reactor-core/src/test/java/reactor/test/publisher/BaseOperatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,6 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import reactor.ReactorTestExecutionListener; import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; @@ -179,9 +178,6 @@ public final Stream sequenceOfNextWithCallbackError() { } throw Exceptions.propagate(e); }); -// step.expectErrorMessage(m) -// .verifyThenAssertThat() -// .hasOperatorErrorWithMessage(m); } catch (Throwable e) { if (e instanceof AssertionError) { From 544fa823c4591dc841eb9301b7d13bb57cbf66f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 6 Sep 2022 14:28:34 +0200 Subject: [PATCH 053/312] switches to all the Micrometer SNAPSHOTs (#3179) --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41244c5fcb..68610c45bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.14" jmh = "1.35" junit = "5.9.0" #note that context-propagation has a different version directly set in libraries -micrometer = "1.10.0-M4" +micrometer = "1.10.0-SNAPSHOT" # was M4 reactiveStreams = "1.0.4" [libraries] @@ -35,9 +35,9 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-M4" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" # was M4 micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-M7" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" # was M7 micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.7.0" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 40fd0275337fc0dec180557b32598773a322a6f4 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 13 Sep 2022 09:56:09 +0300 Subject: [PATCH 054/312] updates Micrometer dependency to latest milestone releases (#3190) --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 68610c45bd..c27f0a94f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.14" jmh = "1.35" junit = "5.9.0" #note that context-propagation has a different version directly set in libraries -micrometer = "1.10.0-SNAPSHOT" # was M4 +micrometer = "1.10.0-M5" reactiveStreams = "1.0.4" [libraries] @@ -35,9 +35,9 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" # was M4 +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-M5" micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" # was M7 +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-M8" micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.7.0" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From be618bb2ca7b8118762e9cc2f1f7b572e43b4c92 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Sep 2022 13:43:08 +0300 Subject: [PATCH 055/312] [release] Prepare and release 3.5.0-M6 Signed-off-by: Oleh Dokuka --- README.md | 6 +++--- gradle.properties | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f2159c5a6e..22c1889c11 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.0-M5" - testCompile "io.projectreactor:reactor-test:3.5.0-M5" + compile "io.projectreactor:reactor-core:3.5.0-M6" + testCompile "io.projectreactor:reactor-test:3.5.0-M6" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.5.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.5.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.0-M5" + // implementation "io.projectreactor:reactor-tools:3.5.0-M6" } ``` diff --git a/gradle.properties b/gradle.properties index 00798b2bcd..ff51b247de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-SNAPSHOT -bomVersion=2022.0.0-M5 -metricsMicrometerVersion=1.0.0-SNAPSHOT +version=3.5.0-M6 +bomVersion=2022.0.0-M6 +metricsMicrometerVersion=1.0.0-M8 From 74b2f20a841a3f08c35ce8b0fef1fbe83c315c9d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Sep 2022 14:33:16 +0300 Subject: [PATCH 056/312] [release] Next development version 3.5.0-SNAPSHOT Signed-off-by: Oleh Dokuka --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index ff51b247de..229ef99c70 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-M6 +version=3.5.0-SNAPSHOT bomVersion=2022.0.0-M6 -metricsMicrometerVersion=1.0.0-M8 +metricsMicrometerVersion=1.0.0-SNAPSHOT From 34c3d08ad4fe60524e9dfb0a1ff1242ff4f0dba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 16 Sep 2022 14:13:08 +0200 Subject: [PATCH 057/312] Update Micrometer dependency to 1.10.0-M6 (#3194) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d11b05763e..0485170173 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.14" jmh = "1.35" junit = "5.9.0" #note that context-propagation has a different version directly set in libraries -micrometer = "1.10.0-M5" +micrometer = "1.10.0-M6" reactiveStreams = "1.0.4" [libraries] From 169bd9d64fb3c8e41a1528c3f8102706753ec579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 23 Sep 2022 10:22:41 +0200 Subject: [PATCH 058/312] Document reactor-Core-Micrometer obs/meters/tags via enums (#3175) This commit uses DocumentedMeter API to replace magic String constants and make the meters and tags in Reactor-Core-Micrometer documented and public. The asciidoc generation now takes advantage of this to generate a section in the reference guide, through the provided Micrometer tool. Each of the 3 features (meter listener, observation listener and timed Scheduler wrapper) get their own DocumentedMeter and one associated doc generation task. We use a Copy task trick on the three resulting asciidoc files to remove some of the hardcoded boilerplate text for a better inclusion in the reference guide. Relates to #3108. --- docs/asciidoc/index.asciidoc | 1 + docs/asciidoc/metrics-details.adoc | 30 +++ gradle/asciidoc.gradle | 73 +++++- gradle/libs.versions.toml | 4 +- .../DocumentedMeterListenerMeters.java | 220 ++++++++++++++++++ .../DocumentedObservationListenerTags.java | 99 ++++++++ .../DocumentedTimedSchedulerMeters.java | 162 +++++++++++++ .../observability/micrometer/Micrometer.java | 19 +- .../micrometer/MicrometerMeterListener.java | 79 ++----- .../MicrometerObservationListener.java | 33 +-- ...meterObservationListenerConfiguration.java | 8 +- .../micrometer/TimedScheduler.java | 95 ++------ .../MicrometerMeterListenerTest.java | 22 +- .../MicrometerObservationIntegrationTest.java | 1 - ...rObservationListenerConfigurationTest.java | 4 +- .../MicrometerObservationListenerTest.java | 10 +- .../micrometer/MicrometerTest.java | 2 +- .../micrometer/TimedSchedulerTest.java | 11 +- 18 files changed, 684 insertions(+), 189 deletions(-) create mode 100644 docs/asciidoc/metrics-details.adoc create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedMeterListenerMeters.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedObservationListenerTags.java create mode 100644 reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedTimedSchedulerMeters.java diff --git a/docs/asciidoc/index.asciidoc b/docs/asciidoc/index.asciidoc index 8476b501f6..67f4172443 100644 --- a/docs/asciidoc/index.asciidoc +++ b/docs/asciidoc/index.asciidoc @@ -56,6 +56,7 @@ ifeval::["{backend}" == "html5"] https://github.com/reactor/reactor-core/edit/main/docs/asciidoc/metrics.adoc[Suggest Edit^, title="Suggest an edit to the above section via github", role="fa fa-edit"] to "<>" endif::[] +include::metrics-details.adoc[] include::advancedFeatures.adoc[leveloffset=1] ifeval::["{backend}" == "html5"] diff --git a/docs/asciidoc/metrics-details.adoc b/docs/asciidoc/metrics-details.adoc new file mode 100644 index 0000000000..e72be84d62 --- /dev/null +++ b/docs/asciidoc/metrics-details.adoc @@ -0,0 +1,30 @@ + +:root-target: ./../../build/documentedMetrics/ + +### Meters and tags for Reactor-Core-Micrometer module + +#### `Micrometer.metrics()` +Below is the list of meters used by the metrics tap listener feature, as exposed via +`Micrometer.metrics(MeterRegistry meterRegistry)`. + +IMPORTANT: Please note that metrics below use a dynamic `%s` prefix. +When applied on a `Flux` or `Mono` that uses the `name(String n)` operator, this is replaced with `n`. +Otherwise, this is replaced by the default value of `"reactor"`. + +include::{root-target}meterListener.adoc[leveloffset=4] + +#### `Micrometer.timedScheduler()` +Below is the list of meters used by the TimedScheduler feature, as exposed via +`Micrometer.timedScheduler(Scheduler original, MeterRegistry meterRegistry, String metricsPrefix)`. + +IMPORTANT: Please note that metrics below use a dynamic `%s` prefix. This is replaced with the provided `metricsPrefix` in practice. + +include::{root-target}timedScheduler.adoc[leveloffset=4] + +#### `Micrometer.observation()` +Below is the list of meters used by the observation tap listener feature, as exposed via +`Micrometer.observation(ObservationRegistry registry)`. + +This is the ANONYMOUS observation, but you can create a similar Observation with a custom name by using the `name(String)` operator. + +include::{root-target}observation.adoc[leveloffset=4] \ No newline at end of file diff --git a/gradle/asciidoc.gradle b/gradle/asciidoc.gradle index e3e503bf90..36d9c2f026 100644 --- a/gradle/asciidoc.gradle +++ b/gradle/asciidoc.gradle @@ -18,6 +18,10 @@ configure(rootProject) { apply plugin: 'org.asciidoctor.jvm.convert' apply plugin: 'org.asciidoctor.jvm.pdf' + repositories { + maven { url 'https://repo.spring.io/milestone' } + } + // This configuration applies both to the asciidoctor & asciidoctorPdf tasks asciidoctorj { options = [doctype: 'book'] @@ -28,6 +32,8 @@ configure(rootProject) { } asciidoctor { + dependsOn "generateObservabilityDocs" + inputs.dir("$buildDir/generatedMetricsDocs/") // force the task to consider changes in this folder, making it not UP-TO-DATE sourceDir "docs/asciidoc/" sources { include "index.asciidoc" @@ -52,6 +58,7 @@ configure(rootProject) { asciidoctorPdf { onlyIf { isCiServer || !rootProject.version.toString().endsWith("-SNAPSHOT") || rootProject.hasProperty("forcePdf") } + dependsOn "generateObservabilityDocs" sourceDir "docs/asciidoc/" sources { include "index.asciidoc" @@ -62,7 +69,9 @@ configure(rootProject) { attributes 'source-highlighter': 'rouge' } - task docsZip(type: Zip, dependsOn: [asciidoctor, asciidoctorPdf]) { + task asciidocs(dependsOn: [asciidoctor, asciidoctorPdf], group: "documentation") { } + + task docsZip(type: Zip, dependsOn: asciidocs) { archiveBaseName.set("reactor-core") archiveClassifier.set('docs') afterEvaluate() { @@ -74,5 +83,67 @@ configure(rootProject) { } from(asciidoctor) { into("docs/") } } + + configurations { + adoc + } + + dependencies { + adoc platform(libs.micrometer.docsGenerator.bom) + adoc libs.micrometer.docsGenerator.metrics + } + + task generateObservabilityDocs(dependsOn: [ + "generateMeterListenerDocs", + "generateTimedSchedulerDocs", + "generateObservationDocs", + "polishGeneratedMetricsDocs"]) { + } + + task generateMeterListenerDocs(type: JavaExec) { + mainClass.set("io.micrometer.docs.metrics.DocsFromSources") + classpath configurations.adoc + args project.rootDir.getAbsolutePath(), ".*DocumentedMeter.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/meterListener").toAbsolutePath().toString() + } + + task generateTimedSchedulerDocs(type: JavaExec) { + mainClass.set("io.micrometer.docs.metrics.DocsFromSources") + classpath configurations.adoc + args project.rootDir.getAbsolutePath(), ".*DocumentedTimedScheduler.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/timedScheduler").toAbsolutePath().toString() + } + + task generateObservationDocs(type: JavaExec) { + mainClass.set("io.micrometer.docs.metrics.DocsFromSources") + classpath configurations.adoc + args project.rootDir.getAbsolutePath(), ".*DocumentedObservation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/observation").toAbsolutePath().toString() + } + + task polishGeneratedMetricsDocs(type: Copy) { + mustRunAfter "generateMeterListenerDocs" + mustRunAfter "generateTimedSchedulerDocs" + mustRunAfter "generateObservationDocs" + from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/meterListener/") { + include "_*.adoc" + rename '_metrics.adoc', 'meterListener.adoc' + } + from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/timedScheduler/") { + include "_*.adoc" + rename '_metrics.adoc', 'timedScheduler.adoc' + } + from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/observation/") { + include "_*.adoc" + rename '_metrics.adoc', 'observation.adoc' + } + into project.rootProject.buildDir.toString() + "/documentedMetrics" + filter { String line -> + line.startsWith('[[observability-metrics]]') || + line.startsWith('=== Observability - Metrics') || + line.startsWith('Below you can find a list of all samples ') || + line.startsWith("Fully qualified name of the enclosing class ") + ? null : line + } + filter { String line -> line.startsWith("====") ? line.replaceFirst("====", "=") : line } + } + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0485170173..87dd11a14c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ asciidoctor = "3.3.2" bytebuddy = "1.12.14" jmh = "1.35" junit = "5.9.0" -#note that context-propagation has a different version directly set in libraries +#note that some micrometer artifacts like context-propagation has a different version directly set in libraries below micrometer = "1.10.0-M6" reactiveStreams = "1.0.4" @@ -36,6 +36,8 @@ micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micro micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-M5" +micrometer-docsGenerator-bom = { module = "io.micrometer:micrometer-docs-generator-bom", version = "1.0.0-M7"} +micrometer-docsGenerator-metrics = { module = "io.micrometer:micrometer-docs-generator-metrics" } micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-M8" micrometer-test = { module = "io.micrometer:micrometer-test" } diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedMeterListenerMeters.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedMeterListenerMeters.java new file mode 100644 index 0000000000..4c25295128 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedMeterListenerMeters.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.docs.DocumentedMeter; + +/** + * Meters and tags used by {@link Micrometer#metrics(MeterRegistry)}. + */ +/* +NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when +the docs are generated by Micrometer's tool +*/ +public enum DocumentedMeterListenerMeters implements DocumentedMeter { + + /** + * Counts the number of events received from a malformed source (ie an onNext after an onComplete). + */ + MALFORMED_SOURCE_EVENTS { + @Override + public KeyName[] getKeyNames() { + return CommonTags.values(); + } + + @Override + public String getName() { + return "%s.malformed.source"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.COUNTER; + } + }, + + /** + * Measures the delay between each onNext (or between the first onNext and the onSubscribe event). + */ + ON_NEXT_DELAY { + @Override + public String getBaseUnit() { + return ""; //FIXME nanoseconds? milliseconds + } + + @Override + public KeyName[] getKeyNames() { + return CommonTags.values(); + } + + @Override + public String getName() { + return "%s.onNext.delay"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.TIMER; + } + }, + + /** + * Counts the amount requested to a named sequence (eg. Flux.name(String)) by all subscribers, until at least one requests an unbounded amount. + */ + REQUESTED_AMOUNT { + @Override + public KeyName[] getKeyNames() { + return CommonTags.values(); + } + + @Override + public String getName() { + return "%s.requested"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.DISTRIBUTION_SUMMARY; + } + }, + + /** + * Counts the number of subscriptions to a sequence. + */ + SUBSCRIBED { + @Override + public KeyName[] getKeyNames() { + return CommonTags.values(); + } + + @Override + public String getName() { + return "%s.subscribed"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.COUNTER; + } + }, + + /** + * Times the duration elapsed between a subscription and the termination or cancellation of the sequence. + * A TerminationTags#STATUS tag is added to specify what event caused the timer to end (completed, completedEmpty, error, cancelled). + */ + FLOW_DURATION { + @Override + public KeyName[] getKeyNames() { + return KeyName.merge( + CommonTags.values(), + TerminationTags.values() + ); + } + + @Override + public String getName() { + return "%s.flow.duration"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.DISTRIBUTION_SUMMARY; + } + }; + + /** + * Tags that are common to all PublisherMeters. + */ + public enum CommonTags implements KeyName { + + /** + * The type of the sequence (Flux or Mono). + */ + TYPE { + @Override + public String asString() { + return "type"; + } + }; + + /** + * TYPE for reactor.core.publisher.Flux + */ + public static final String TAG_TYPE_FLUX = "Flux"; + /** + * TYPE for reactor.core.publisher.Mono + */ + public static final String TAG_TYPE_MONO = "Mono"; + + } + + /** + * Additional tags for PublisherMeters#FLOW_DURATION that reflects the termination + * of the sequence. + */ + public enum TerminationTags implements KeyName { + + /** + * The termination status: + *

      + *
    • TAG_STATUS_COMPLETED for a sequence that terminates with an onComplete, with onNext(s)
    • + *
    • TAG_STATUS_COMPLETED_EMPTY for a sequence that terminates without any onNext before the onComplete
    • + *
    • TAG_STATUS_ERROR for a sequence that terminates with an onError
    • + *
    • TAG_STATUS_CANCELLED for a sequence that has cancelled its subscription
    • + *
    + */ + STATUS { + @Override + public String asString() { + return "status"; + } + }, + + /** + * Tag used by FLOW_DURATION when STATUS is TAG_STATUS_ERROR, to store the + * exception that occurred. + */ + EXCEPTION { + @Override + public String asString() { + return "exception"; + } + }; + + //Status + /** + * Status for a sequence that has cancelled its subscription. + */ + public static final String TAG_STATUS_CANCELLED = "cancelled"; + /** + * Status for a sequence that terminates with an onComplete, with onNext(s). + */ + public static final String TAG_STATUS_COMPLETED = "completed"; + /** + * Status for a sequence that terminates without any onNext before the onComplete. + */ + public static final String TAG_STATUS_COMPLETED_EMPTY = "completedEmpty"; + /** + * Status for a sequence that terminates with an onError. + */ + public static final String TAG_STATUS_ERROR = "error"; + + } +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedObservationListenerTags.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedObservationListenerTags.java new file mode 100644 index 0000000000..d78b68c1cd --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedObservationListenerTags.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.DocumentedObservation; + +/** + * Documentation of {@link Micrometer#observation(ObservationRegistry)} tags and of the anonymous variant + * of the observation (no {@link reactor.core.publisher.Flux#name(String)}). + */ +/* +NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when +the docs are generated by Micrometer's tool +*/ +public enum DocumentedObservationListenerTags implements DocumentedObservation { + + /** + * Anonymous version of the Micrometer.observation(), when the sequence hasn't been + * explicitly named via e.g. Flux#name(String) operator. + */ + ANONYMOUS { + @Override + public String getName() { + return "reactor.observation"; + } + + @Override + public String getContextualName() { + return "reactor anonymous observation"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ObservationTags.values(); + } + } + ; + + /** + * Tags used in the Observation set up by Micrometer.observation() tap listeners. + */ + public static enum ObservationTags implements KeyName { + /** + * The status of the sequence, which indicates how it terminated (completed, completedEmpty, + * error or cancelled). + */ + STATUS { + @Override + public String asString() { + return "reactor.status"; + } + }, + /** + * The type of the sequence, i.e. Flux or Mono. + */ + TYPE { + @Override + public String asString() { + return "reactor.type"; + } + }; + + /** + * {@link #STATUS} for when the subscription to the sequence was cancelled. + */ + public static final String TAG_STATUS_CANCELLED = DocumentedMeterListenerMeters.TerminationTags.TAG_STATUS_CANCELLED; + /** + * {@link #STATUS} for when the sequence completed with values. + */ + public static final String TAG_STATUS_COMPLETED = DocumentedMeterListenerMeters.TerminationTags.TAG_STATUS_COMPLETED; + /** + * {@link #STATUS} for when the sequence completed without value (no onNext). + */ + public static final String TAG_STATUS_COMPLETED_EMPTY = DocumentedMeterListenerMeters.TerminationTags.TAG_STATUS_COMPLETED_EMPTY; + /** + * {@link #STATUS} for when the sequence terminated with an error. The {@link io.micrometer.observation.Observation#error(Throwable)} + * method is used to capture the exception. + */ + public static final String TAG_STATUS_ERROR = DocumentedMeterListenerMeters.TerminationTags.TAG_STATUS_ERROR; + } + + +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedTimedSchedulerMeters.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedTimedSchedulerMeters.java new file mode 100644 index 0000000000..b1d01bd436 --- /dev/null +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedTimedSchedulerMeters.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.observability.micrometer; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.docs.DocumentedMeter; + +import reactor.core.scheduler.Scheduler; + +/** + * Meters and tags used by {@link Micrometer#timedScheduler(Scheduler, MeterRegistry, String)}. + */ +/* +NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when +the docs are generated by Micrometer's tool +*/ +public enum DocumentedTimedSchedulerMeters implements DocumentedMeter { + + /** + * Counter that increments by one each time a task is submitted (via any of the + * schedule methods on both Scheduler and Scheduler.Worker). + *

    + * Note that there are actually 4 counters, which can be differentiated by the SubmittedTags#SUBMISSION tag. + * The sum of all these can thus be compared with the TASKS_COMPLETED counter. + */ + TASKS_SUBMITTED { + @Override + public KeyName[] getKeyNames() { + return SubmittedTags.values(); + } + + @Override + public String getName() { + return "%s.scheduler.tasks.submitted"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.COUNTER; + } + }, + + /** + * LongTaskTimer reflecting tasks currently running. Note that this reflects all types of + * active tasks, including tasks scheduled with a delay or periodically (each + * iteration being considered an active task). + */ + TASKS_ACTIVE { + @Override + public String getName() { + return "%s.scheduler.tasks.active"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.LONG_TASK_TIMER; + } + }, + + /** + * Timer reflecting tasks that have finished execution. Note that this reflects all types of + * active tasks, including tasks with a delay or periodically (each iteration being considered + * a separate completed task). + */ + TASKS_COMPLETED { + @Override + public String getName() { + return "%s.scheduler.tasks.completed"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.TIMER; + } + }, + + /** + * LongTaskTimer reflecting tasks that were submitted for immediate execution but + * couldn't be started immediately because the scheduler is already at max capacity. + * Note that only immediate submissions via Scheduler#schedule(Runnable) and + * Scheduler.Worker#schedule(Runnable) are considered. + */ + TASKS_PENDING { + @Override + public String getName() { + return "%s.scheduler.tasks.pending"; + } + + @Override + public Meter.Type getType() { + return Meter.Type.LONG_TASK_TIMER; + } + + } + ; + + /** + * Tag for the SchedulerMeters#TASKS_SUBMITTED meter. + */ + public enum SubmittedTags implements KeyName { + + /** + * The type of submission: + *

      + *
    • #SUBMISSION_DIRECT for Scheduler#schedule(Runnable)
    • + *
    • #SUBMISSION_DELAYED for Scheduler#schedule(Runnable, long, TimeUnit)
    • + *
    • #SUBMISSION_PERIODIC_INITIAL for Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit) after the initial delay
    • + *
    • #SUBMISSION_PERIODIC_ITERATION for Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit) further periodic iterations
    • + *
    + */ + SUBMISSION { + @Override + public String asString() { + return "submission.type"; + } + }; + + /** + * Counter that increments by one each time a task is submitted for immediate execution + * (ie. Scheduler#schedule(Runnable) or Scheduler.Worker#schedule(Runnable)). + */ + public static final String SUBMISSION_DIRECT = "direct"; + /** + * Counter that increments by one each time a task is submitted with a delay + * (ie. Scheduler#schedule(Runnable, long, TimeUnit) + * or Scheduler.Worker#schedule(Runnable, long, TimeUnit)). + */ + public static final String SUBMISSION_DELAYED = "delayed"; + /** + * Counter that increments when a task is initially submitted with a period + * (ie. Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit) + * or Scheduler.Worker#schedulePeriodically(Runnable, long, long, TimeUnit)). This isn't + * incremented on further iterations of the periodic task. + */ + public static final String SUBMISSION_PERIODIC_INITIAL = "periodic_initial"; + /** + * Counter that increments by one each time a task is re-executed due to the periodic + * nature of Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit) + * or Scheduler.Worker#schedulePeriodically(Runnable, long, long, TimeUnit) (ie. iterations + * past the initial one). + */ + public static final String SUBMISSION_PERIODIC_ITERATION = "periodic_iteration"; + + } + +} diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index 9c3b5856ae..b000ffc298 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -20,7 +20,6 @@ import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import io.micrometer.observation.Observation; @@ -48,6 +47,7 @@ public final class Micrometer { * the {@link reactor.core.publisher.Flux#name(String)} set upstream of the tap as id prefix if applicable * or default to {@link #DEFAULT_METER_PREFIX}. Similarly, upstream tags are gathered and added * to the default set of tags for meters. + * See {@link DocumentedMeterListenerMeters} for a documentation of the default set of meters and tags. *

    * Note that some monitoring systems like Prometheus require to have the exact same set of * tags for each meter bearing the same name. @@ -55,6 +55,7 @@ public final class Micrometer { * @param the type of onNext in the target publisher * @param meterRegistry the {@link MeterRegistry} in which to register and publish metrics * @return a {@link SignalListenerFactory} to record metrics + * @see DocumentedMeterListenerMeters */ public static SignalListenerFactory metrics(MeterRegistry meterRegistry) { return new MicrometerMeterListenerFactory(meterRegistry); @@ -82,9 +83,11 @@ public final class Micrometer { * Similarly, Reactor tags defined upstream via eg. {@link reactor.core.publisher.Flux#tag(String, String)}) * are gathered and added to the default set of {@link io.micrometer.common.KeyValues} used by the Observation * as {@link Observation#lowCardinalityKeyValues(KeyValues) low cardinality keyValues}. + * See {@link DocumentedObservationListenerTags} for a documentation of the default set of tags. * - * @param the type of onNext in the target publisher + * @param the type of onNext in the target publisher * @return a {@link SignalListenerFactory} to record observations + * @see DocumentedObservationListenerTags */ public static SignalListenerFactory observation(ObservationRegistry registry) { return new MicrometerObservationListenerFactory<>(registry); @@ -93,12 +96,14 @@ public final class Micrometer { /** * Wrap a {@link Scheduler} in an instance that gathers various task-related metrics using * the provided {@link MeterRegistry} and naming meters using the provided {@code metricsPrefix}. - * Note that no tags are set up for these meters. + * Note that no common tags are set up for these meters. + * See {@link DocumentedTimedSchedulerMeters} for a documentation of the default set of meters and tags. * * @param original the original {@link Scheduler} to decorate with metrics * @param meterRegistry the {@link MeterRegistry} in which to register the various meters - * @param metricsPrefix the prefix to use in meter names. If needed, a dot is added at the end + * @param metricsPrefix the prefix to use in meter names. Must not end with a dot, which is automatically added. * @return a {@link Scheduler} that is instrumented with dedicated metrics + * @see DocumentedTimedSchedulerMeters */ public static Scheduler timedScheduler(Scheduler original, MeterRegistry meterRegistry, String metricsPrefix) { return new TimedScheduler(original, meterRegistry, metricsPrefix, Tags.empty()); @@ -107,14 +112,16 @@ public static Scheduler timedScheduler(Scheduler original, MeterRegistry meterRe /** * Wrap a {@link Scheduler} in an instance that gathers various task-related metrics using * the provided {@link MeterRegistry} and naming meters using the provided {@code metricsPrefix}. - * User-provided collection of {@link Tag} (ie. {@link Tags}) can also be provided to be added to + * User-provided collection of common tags (ie. {@link Tags}) can also be provided to be added to * all the meters of that timed Scheduler. + * See {@link DocumentedTimedSchedulerMeters} for a documentation of the default set of meters and tags. * * @param original the original {@link Scheduler} to decorate with metrics * @param meterRegistry the {@link MeterRegistry} in which to register the various meters - * @param metricsPrefix the prefix to use in meter names. If needed, a dot is added at the end + * @param metricsPrefix the prefix to use in meter names. Must not end with a dot, which is automatically added. * @param tags the tags to put on meters * @return a {@link Scheduler} that is instrumented with dedicated metrics + * @see DocumentedTimedSchedulerMeters */ public static Scheduler timedScheduler(Scheduler original, MeterRegistry meterRegistry, String metricsPrefix, Iterable tags) { return new TimedScheduler(original, meterRegistry, metricsPrefix, tags); diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java index 4fd0c0204f..1ba4a915b6 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java @@ -26,10 +26,12 @@ import io.micrometer.core.instrument.Timer; import reactor.core.observability.SignalListener; -import reactor.core.publisher.Flux; import reactor.core.publisher.SignalType; import reactor.util.annotation.Nullable; +import static reactor.core.observability.micrometer.DocumentedMeterListenerMeters.CommonTags.*; +import static reactor.core.observability.micrometer.DocumentedMeterListenerMeters.TerminationTags.*; + /** * A {@link SignalListener} that activates metrics gathering using Micrometer 1.x. * @@ -59,17 +61,13 @@ final class MicrometerMeterListener implements SignalListener { requestedCounter = null; } else { - this.onNextIntervalTimer = Timer.builder(configuration.sequenceName + METER_ON_NEXT_DELAY) + this.onNextIntervalTimer = Timer.builder(DocumentedMeterListenerMeters.ON_NEXT_DELAY.getName(configuration.sequenceName)) .tags(configuration.commonTags) - .description( - "Measures delays between onNext signals (or between onSubscribe and first onNext)") .register(configuration.registry); if (!Micrometer.DEFAULT_METER_PREFIX.equals(configuration.sequenceName)) { - this.requestedCounter = DistributionSummary.builder(configuration.sequenceName + METER_REQUESTED) + this.requestedCounter = DistributionSummary.builder(DocumentedMeterListenerMeters.REQUESTED_AMOUNT.getName(configuration.sequenceName)) .tags(configuration.commonTags) - .description( - "Counts the amount requested to a named Flux by all subscribers, until at least one requests an unbounded amount") .register(configuration.registry); } else { @@ -182,49 +180,16 @@ public void handleListenerError(Throwable listenerError) { // NO-OP } - /** - * Meter that counts the number of events received from a malformed source (ie an onNext after an onComplete). - */ - static final String METER_MALFORMED = ".malformed.source"; - /** - * Meter that counts the number of subscriptions to a sequence. - */ - static final String METER_SUBSCRIBED = ".subscribed"; - /** - * Meter that times the duration elapsed between a subscription and the termination or cancellation of the sequence. - * A status tag is added to specify what event caused the timer to end (completed, completedEmpty, error, cancelled). - */ - static final String METER_FLOW_DURATION = ".flow.duration"; - /** - * Meter that times the delays between each onNext (or between the first onNext and the onSubscribe event). - */ - static final String METER_ON_NEXT_DELAY = ".onNext.delay"; - /** - * Meter that tracks the request amount, in {@link Flux#name(String) named} sequences only. - */ - static final String METER_REQUESTED = ".requested"; - /** - * Tag used by {@link #METER_FLOW_DURATION} when "status" is {@link #TAG_ON_ERROR}, to store the - * exception that occurred. - */ - static final String TAG_KEY_EXCEPTION = "exception"; - /** - * Tag bearing the sequence's name, as given by the {@link Flux#name(String)} operator. - */ - static final Tags DEFAULT_TAGS_FLUX = Tags.of("type", "Flux"); - static final Tags DEFAULT_TAGS_MONO = Tags.of("type", "Mono"); - // === Operator === - static final String TAG_KEY_STATUS = "status"; - static final String TAG_STATUS_CANCELLED = "cancelled"; - static final String TAG_STATUS_COMPLETED = "completed"; - static final String TAG_STATUS_COMPLETED_EMPTY = "completedEmpty"; - static final String TAG_STATUS_ERROR = "error"; + // == from KeyNames to Tags + static final Tags DEFAULT_TAGS_FLUX = Tags.of(TYPE.asString(), TAG_TYPE_FLUX); + static final Tags DEFAULT_TAGS_MONO = Tags.of(TYPE.asString(), TAG_TYPE_MONO); + static final Tag TAG_ON_ERROR = Tag.of(STATUS.asString(), TAG_STATUS_ERROR); + static final Tags TAG_ON_COMPLETE = Tags.of(STATUS.asString(), TAG_STATUS_COMPLETED, EXCEPTION.asString(), ""); + static final Tags TAG_ON_COMPLETE_EMPTY = Tags.of(STATUS.asString(), TAG_STATUS_COMPLETED_EMPTY, EXCEPTION.asString(), ""); + static final Tags TAG_CANCEL = Tags.of(STATUS.asString(), TAG_STATUS_CANCELLED, EXCEPTION.asString(), ""); - static final Tag TAG_ON_ERROR = Tag.of(TAG_KEY_STATUS, TAG_STATUS_ERROR); - static final Tags TAG_ON_COMPLETE = Tags.of(TAG_KEY_STATUS, TAG_STATUS_COMPLETED, TAG_KEY_EXCEPTION, ""); - static final Tags TAG_ON_COMPLETE_EMPTY = Tags.of(TAG_KEY_STATUS, TAG_STATUS_COMPLETED_EMPTY, TAG_KEY_EXCEPTION, ""); - static final Tags TAG_CANCEL = Tags.of(TAG_KEY_STATUS, TAG_STATUS_CANCELLED, TAG_KEY_EXCEPTION, ""); + // === Record methods === /* * This method calls the registry, which can be costly. However the cancel signal is only expected @@ -233,7 +198,7 @@ public void handleListenerError(Throwable listenerError) { * cost only in case of cancellation. */ static void recordCancel(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { - Timer timer = Timer.builder(name + METER_FLOW_DURATION) + Timer timer = Timer.builder(DocumentedMeterListenerMeters.FLOW_DURATION.getName(name)) .tags(commonTags.and(TAG_CANCEL)) .description( "Times the duration elapsed between a subscription and the cancellation of the sequence") @@ -249,7 +214,7 @@ static void recordCancel(String name, Tags commonTags, MeterRegistry registry, T * with the added benefit of paying that cost only in case of onNext/onError after termination. */ static void recordMalformed(String name, Tags commonTags, MeterRegistry registry) { - registry.counter(name + METER_MALFORMED, commonTags) + registry.counter(DocumentedMeterListenerMeters.MALFORMED_SOURCE_EVENTS.getName(name), commonTags) .increment(); } @@ -261,11 +226,10 @@ static void recordMalformed(String name, Tags commonTags, MeterRegistry registry */ static void recordOnError(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration, Throwable e) { - Timer timer = Timer.builder(name + METER_FLOW_DURATION) + Timer timer = Timer.builder(DocumentedMeterListenerMeters.FLOW_DURATION.getName(name)) .tags(commonTags.and(TAG_ON_ERROR)) - .tag(TAG_KEY_EXCEPTION, - e.getClass() - .getName()) + .tag(DocumentedMeterListenerMeters.TerminationTags.EXCEPTION.asString(), + e.getClass().getName()) .description( "Times the duration elapsed between a subscription and the onError termination of the sequence, with the exception name as a tag.") .register(registry); @@ -280,7 +244,7 @@ static void recordOnError(String name, Tags commonTags, MeterRegistry registry, * that cost only in case of completion (which is not always occurring). */ static void recordOnComplete(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { - Timer timer = Timer.builder(name + METER_FLOW_DURATION) + Timer timer = Timer.builder(DocumentedMeterListenerMeters.FLOW_DURATION.getName(name)) .tags(commonTags.and(TAG_ON_COMPLETE)) .description( "Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements") @@ -296,7 +260,7 @@ static void recordOnComplete(String name, Tags commonTags, MeterRegistry registr * that cost only in case of completion (which is not always occurring). */ static void recordOnCompleteEmpty(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { - Timer timer = Timer.builder(name + METER_FLOW_DURATION) + Timer timer = Timer.builder(DocumentedMeterListenerMeters.FLOW_DURATION.getName(name)) .tags(commonTags.and(TAG_ON_COMPLETE_EMPTY)) .description( "Times the duration elapsed between a subscription and the onComplete termination of a sequence that didn't emit any element") @@ -312,9 +276,8 @@ static void recordOnCompleteEmpty(String name, Tags commonTags, MeterRegistry re * that cost only in case of subscription. */ static void recordOnSubscribe(String name, Tags commonTags, MeterRegistry registry) { - Counter.builder(name + METER_SUBSCRIBED) + Counter.builder(DocumentedMeterListenerMeters.SUBSCRIBED.getName(name)) .tags(commonTags) - .description("Counts how many Reactor sequences have been subscribed to") .register(registry) .increment(); } diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java index 387ba8e031..b0a9ec5cea 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java @@ -19,6 +19,7 @@ import io.micrometer.observation.Observation; import reactor.core.observability.SignalListener; +import reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags; import reactor.core.publisher.SignalType; import reactor.util.Logger; import reactor.util.Loggers; @@ -26,6 +27,12 @@ import reactor.util.context.Context; import reactor.util.context.ContextView; +import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.STATUS; +import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.TAG_STATUS_CANCELLED; +import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.TAG_STATUS_COMPLETED; +import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.TAG_STATUS_COMPLETED_EMPTY; +import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.TAG_STATUS_ERROR; + /** * A {@link SignalListener} that makes timings using the {@link io.micrometer.observation.Observation} API from Micrometer 1.10. *

    @@ -38,14 +45,6 @@ final class MicrometerObservationListener implements SignalListener { private static final Logger LOGGER = Loggers.getLogger(MicrometerObservationListener.class); - static final String ANONYMOUS_OBSERVATION = "reactor.observation"; - static final String KEY_STATUS = "reactor.status"; - static final String KEY_TYPE = "reactor.type"; - static final String STATUS_CANCELLED = MicrometerMeterListener.TAG_STATUS_CANCELLED; - static final String STATUS_COMPLETED = MicrometerMeterListener.TAG_STATUS_COMPLETED; - static final String STATUS_COMPLETED_EMPTY = MicrometerMeterListener.TAG_STATUS_COMPLETED_EMPTY; - static final String STATUS_ERROR = MicrometerMeterListener.TAG_STATUS_ERROR; - /** * The key to use to store {@link Observation} in context (same as the one from {@code ObservationThreadLocalAccessor}). * @@ -57,7 +56,7 @@ final class MicrometerObservationListener implements SignalListener { /** * A value for the status tag, to be used when a Mono completes from onNext. - * In production, this is set to {@link #STATUS_COMPLETED}. + * In production, this is set to {@link ObservationTags#TAG_STATUS_COMPLETED}. * In some tests, this can be overridden as a way to assert {@link #doOnComplete()} is no-op. */ final String completedOnNextStatus; @@ -71,7 +70,7 @@ final class MicrometerObservationListener implements SignalListener { boolean valued; MicrometerObservationListener(ContextView subscriberContext, MicrometerObservationListenerConfiguration configuration) { - this(subscriberContext, configuration, STATUS_COMPLETED); + this(subscriberContext, configuration, TAG_STATUS_COMPLETED); } //for test purposes, we can pass in a value for the status tag, to be used when a Mono completes from onNext @@ -84,6 +83,8 @@ final class MicrometerObservationListener implements SignalListener { //creation of the listener matches subscription (Publisher.subscribe(Subscriber) / doFirst) //while doOnSubscription matches the moment where the Publisher acknowledges said subscription + //NOTE: we don't use the `DocumentedObservation` features to create the Observation, even for the ANONYMOUS case, + //because the discovered tags could be more than the documented defaults tapObservation = Observation.createNotStarted( configuration.sequenceName, configuration.registry @@ -153,7 +154,7 @@ public Context addToContext(Context originalContext) { @Override public void doOnCancel() { Observation observation = tapObservation - .lowCardinalityKeyValue(KEY_STATUS, STATUS_CANCELLED); + .lowCardinalityKeyValue(STATUS.asString(), TAG_STATUS_CANCELLED); observation.stop(); } @@ -163,16 +164,16 @@ public void doOnComplete() { // We differentiate between empty completion and value completion via tags. String status = null; if (!valued) { - status = STATUS_COMPLETED_EMPTY; + status = TAG_STATUS_COMPLETED_EMPTY; } else if (!configuration.isMono) { - status = STATUS_COMPLETED; + status = TAG_STATUS_COMPLETED; } // if status == null, recording with OnComplete tag is done directly in onNext for the Mono(valued) case if (status != null) { Observation completeObservation = tapObservation - .lowCardinalityKeyValue(KEY_STATUS, status); + .lowCardinalityKeyValue(STATUS.asString(), status); completeObservation.stop(); } @@ -181,7 +182,7 @@ else if (!configuration.isMono) { @Override public void doOnError(Throwable e) { Observation errorObservation = tapObservation - .lowCardinalityKeyValue(KEY_STATUS, STATUS_ERROR) + .lowCardinalityKeyValue(STATUS.asString(), TAG_STATUS_ERROR) .error(e); errorObservation.stop(); @@ -193,7 +194,7 @@ public void doOnNext(T t) { if (configuration.isMono) { //record valued completion directly Observation completeObservation = tapObservation - .lowCardinalityKeyValue(KEY_STATUS, completedOnNextStatus); + .lowCardinalityKeyValue(STATUS.asString(), completedOnNextStatus); completeObservation.stop(); } diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java index ae63ea00d3..922b6b86a7 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java @@ -38,14 +38,14 @@ */ final class MicrometerObservationListenerConfiguration { - static final KeyValues DEFAULT_KV_FLUX = KeyValues.of(MicrometerObservationListener.KEY_TYPE, "Flux"); - static final KeyValues DEFAULT_KV_MONO = KeyValues.of(MicrometerObservationListener.KEY_TYPE, "Mono"); + static final KeyValues DEFAULT_KV_FLUX = KeyValues.of(DocumentedObservationListenerTags.ObservationTags.TYPE.asString(), "Flux"); + static final KeyValues DEFAULT_KV_MONO = KeyValues.of(DocumentedObservationListenerTags.ObservationTags.TYPE.asString(), "Mono"); private static final Logger LOGGER = Loggers.getLogger(MicrometerObservationListenerConfiguration.class); static MicrometerObservationListenerConfiguration fromFlux(Flux source, ObservationRegistry observationRegistry) { KeyValues defaultKeyValues = DEFAULT_KV_FLUX; - final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, MicrometerObservationListener.ANONYMOUS_OBSERVATION); + final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, DocumentedObservationListenerTags.ANONYMOUS.getName()); final KeyValues keyValues = resolveKeyValues(source, defaultKeyValues); return new MicrometerObservationListenerConfiguration(name, keyValues, observationRegistry, false); @@ -53,7 +53,7 @@ static MicrometerObservationListenerConfiguration fromFlux(Flux source, Obser static MicrometerObservationListenerConfiguration fromMono(Mono source, ObservationRegistry observationRegistry) { KeyValues defaultKeyValues = DEFAULT_KV_MONO; - final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, MicrometerObservationListener.ANONYMOUS_OBSERVATION); + final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, DocumentedObservationListenerTags.ANONYMOUS.getName()); final KeyValues keyValues = resolveKeyValues(source, defaultKeyValues); return new MicrometerObservationListenerConfiguration(name, keyValues, observationRegistry, true); diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java index 5cd2611e2b..a81ad2374e 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java @@ -26,86 +26,21 @@ import io.micrometer.core.instrument.Timer; import reactor.core.Disposable; +import reactor.core.observability.micrometer.DocumentedTimedSchedulerMeters.SubmittedTags; import reactor.core.scheduler.Scheduler; +import static reactor.core.observability.micrometer.DocumentedTimedSchedulerMeters.*; + /** * An instrumented {@link Scheduler} wrapping an original {@link Scheduler} * and gathering metrics around submitted tasks. + *

    + * See {@link DocumentedTimedSchedulerMeters} for the various metrics and tags associated with this class. * * @author Simon Baslé */ final class TimedScheduler implements Scheduler { - //FIXME document all the tags/meters/etc... somewhere public? - - /** - * {@link Timer} reflecting tasks that have finished execution. Note that this reflects all types of - * active tasks, including tasks scheduled {@link #schedule(Runnable, long, TimeUnit) with a delay} - * or {@link #schedulePeriodically(Runnable, long, long, TimeUnit) periodically} (each - * iteration being considered a separate completed task). - */ - static final String METER_TASKS_COMPLETED = "scheduler.tasks.completed"; - /** - * {@link LongTaskTimer} reflecting tasks currently running. Note that this reflects all types of - * active tasks, including tasks scheduled {@link #schedule(Runnable, long, TimeUnit) with a delay} - * or {@link #schedulePeriodically(Runnable, long, long, TimeUnit) periodically} (each - * iteration being considered an active task). - */ - static final String METER_TASKS_ACTIVE = "scheduler.tasks.active"; - /** - * {@link LongTaskTimer} reflecting tasks that were submitted for immediate execution but - * couldn't be started immediately because the scheduler is already at max capacity. - * Note that only immediate submissions via {@link Scheduler#schedule(Runnable)} and - * {@link Worker#schedule(Runnable)} are considered. - */ - static final String METER_TASKS_PENDING = "scheduler.tasks.pending"; - - /** - * The type of submission: - *

      - *
    • {@link #SUBMISSION_DIRECT} for {@link Scheduler#schedule(Runnable)}
    • - *
    • {@link #SUBMISSION_DELAYED} for {@link Scheduler#schedule(Runnable, long, TimeUnit)}
    • - *
    • {@link #SUBMISSION_PERIODIC_INITIAL} for {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} after the initial delay
    • - *
    • {@link #SUBMISSION_PERIODIC_ITERATION} for {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} further periodic iterations
    • - *
    - */ - static final String TAG_SUBMISSION = "submission.type"; - - /** - * {@link Counter} that increments by one each time a task is submitted (via any of the - * schedule methods on both {@link Scheduler} and {@link Worker}). - *

    - * Note that there are actually 4 counters, which can be differentiated by the {@link #TAG_SUBMISSION} tag. - * The sum of all these can thus be compared with the {@link #METER_TASKS_COMPLETED} counter. - */ - static final String METER_SUBMITTED = "scheduler.tasks.submitted"; - - /** - * {@link Counter} that increments by one each time a task is submitted for immediate execution - * (ie. {@link Scheduler#schedule(Runnable)} or {@link Worker#schedule(Runnable)}). - */ - static final String SUBMISSION_DIRECT = "direct"; - /** - * {@link Counter} that increments by one each time a task is submitted with a delay - * (ie. {@link Scheduler#schedule(Runnable, long, TimeUnit)} - * or {@link Worker#schedule(Runnable, long, TimeUnit)}). - */ - static final String SUBMISSION_DELAYED = "delayed"; - /** - * {@link Counter} that increments when a task is initially submitted with a period - * (ie. {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} - * or {@link Worker#schedulePeriodically(Runnable, long, long, TimeUnit)}). This isn't - * incremented on further iterations of the periodic task. - */ - static final String SUBMISSION_PERIODIC_INITIAL = "periodic_initial"; - /** - * {@link Counter} that increments by one each time a task is re-executed due to the periodic - * nature of {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} - * or {@link Worker#schedulePeriodically(Runnable, long, long, TimeUnit)} (ie. iterations - * past the initial one). - */ - static final String SUBMISSION_PERIODIC_ITERATION = "periodic_iteration"; - final Scheduler delegate; final MeterRegistry registry; @@ -122,22 +57,22 @@ final class TimedScheduler implements Scheduler { TimedScheduler(Scheduler delegate, MeterRegistry registry, String metricPrefix, Iterable tagsList) { this.delegate = delegate; this.registry = registry; - if (!metricPrefix.endsWith(".")) { - metricPrefix = metricPrefix + "."; + if (metricPrefix.endsWith(".")) { + metricPrefix = metricPrefix.substring(0, metricPrefix.length() - 1); } Tags tags = Tags.of(tagsList); - String submittedName = metricPrefix + METER_SUBMITTED; - this.submittedDirect = registry.counter(submittedName, tags.and(TAG_SUBMISSION, SUBMISSION_DIRECT)); - this.submittedDelayed = registry.counter(submittedName, tags.and(TAG_SUBMISSION, SUBMISSION_DELAYED)); - this.submittedPeriodicInitial = registry.counter(submittedName, tags.and(TAG_SUBMISSION, SUBMISSION_PERIODIC_INITIAL)); - this.submittedPeriodicIteration = registry.counter(submittedName, tags.and(TAG_SUBMISSION, SUBMISSION_PERIODIC_ITERATION)); + String submittedName = TASKS_SUBMITTED.getName(metricPrefix); + this.submittedDirect = registry.counter(submittedName, tags.and(SubmittedTags.SUBMISSION.asString(), SubmittedTags.SUBMISSION_DIRECT)); + this.submittedDelayed = registry.counter(submittedName, tags.and(SubmittedTags.SUBMISSION.asString(), SubmittedTags.SUBMISSION_DELAYED)); + this.submittedPeriodicInitial = registry.counter(submittedName, tags.and(SubmittedTags.SUBMISSION.asString(), SubmittedTags.SUBMISSION_PERIODIC_INITIAL)); + this.submittedPeriodicIteration = registry.counter(submittedName, tags.and(SubmittedTags.SUBMISSION.asString(), SubmittedTags.SUBMISSION_PERIODIC_ITERATION)); - this.pendingTasks = LongTaskTimer.builder(metricPrefix + METER_TASKS_PENDING) + this.pendingTasks = LongTaskTimer.builder(TASKS_PENDING.getName(metricPrefix)) .tags(tags).register(registry); - this.activeTasks = LongTaskTimer.builder(metricPrefix + METER_TASKS_ACTIVE) + this.activeTasks = LongTaskTimer.builder(TASKS_ACTIVE.getName(metricPrefix)) .tags(tags).register(registry); - this.completedTasks = registry.timer(metricPrefix + METER_TASKS_COMPLETED, tags); + this.completedTasks = registry.timer(TASKS_COMPLETED.getName(metricPrefix), tags); } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java index 47ee415827..2ba131f210 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerMeterListenerTest.java @@ -80,7 +80,7 @@ void initialStateFluxWithDefaultName() { assertThat(registry.getMetersAsString().split("\n")) .as("registered meters: onNextIntervalTimer") .containsExactly( - Micrometer.DEFAULT_METER_PREFIX + ".onNext.delay(TIMER)[testTag1='testTagValue1', testTag2='testTagValue2']; count=0.0, total_time=0.0 seconds, max=0.0 seconds" + "reactor.onNext.delay(TIMER)[testTag1='testTagValue1', testTag2='testTagValue2']; count=0.0, total_time=0.0 seconds, max=0.0 seconds" ); assertThat(registry.remove(listener.onNextIntervalTimer)) @@ -163,7 +163,7 @@ void timerSampleInitializedInSubscription() { assertThat(registry.getMeters()) .as("meters post subscription") .hasSize(3); - assertThat(registry.find("testName" + MicrometerMeterListener.METER_SUBSCRIBED).counter()) + assertThat(registry.find("testName.subscribed").counter()) .as("meter .subscribed") .isNotNull() .satisfies(meter -> assertThat(meter.count()).isEqualTo(1d)); @@ -178,7 +178,7 @@ void doOnCancelTimesFlowDurationMeter() { virtualClockTime.set(100); listener.doOnCancel(); - Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName.flow.duration") .timer(); assertThat(registry.getMeters()).hasSize(4); @@ -199,7 +199,7 @@ void doOnCompleteTimesFlowDurationMeter_completeEmpty() { virtualClockTime.set(100); listener.doOnComplete(); - Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName.flow.duration") .timer(); assertThat(registry.getMeters()).hasSize(4); @@ -221,7 +221,7 @@ void doOnCompleteTimesFlowDurationMeter_completeValued() { listener.valued = true; listener.doOnComplete(); - Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName.flow.duration") .timer(); assertThat(registry.getMeters()).hasSize(4); @@ -242,7 +242,7 @@ void doOnErrorTimesFlowDurationMeter() { virtualClockTime.set(100); listener.doOnError(new IllegalStateException("expected")); - Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName.flow.duration") .timer(); assertThat(registry.getMeters()).hasSize(4); @@ -265,7 +265,7 @@ void doOnNextRecordsInterval() { assertThat(listener.valued).as("valued").isTrue(); assertThat(listener.lastNextEventNanos).as("lastEventNanos recorded").isEqualTo(100); - assertThat(registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION).meters()) + assertThat(registry.find("testName.flow.duration").meters()) .as("no flow.duration meter yet") .isEmpty(); @@ -291,7 +291,7 @@ void doOnNextRecordsInterval_defaultName() { assertThat(listener.valued).as("valued").isTrue(); assertThat(listener.lastNextEventNanos).as("lastEventNanos recorded").isEqualTo(100); - assertThat(registry.find(Micrometer.DEFAULT_METER_PREFIX + MicrometerMeterListener.METER_FLOW_DURATION).meters()) + assertThat(registry.find("reactor.flow.duration").meters()) .as("no flow.duration meter yet") .isEmpty(); @@ -317,7 +317,7 @@ void doOnNext_monoRecordsCompletionOnly() { assertThat(listener.valued).as("valued").isTrue(); assertThat(listener.lastNextEventNanos).as("no lastEventNanos recorded").isZero(); - Timer timer = registry.find("testName" + MicrometerMeterListener.METER_FLOW_DURATION) + Timer timer = registry.find("testName.flow.duration") .timer(); assertThat(timer.getId().toString()) @@ -384,12 +384,12 @@ void doOnRequestDefaultNameIgnoresRequest() { void malformedCounterCapturesNextCompleteError() { MicrometerMeterListener listener = new MicrometerMeterListener<>(configuration); - Counter malformedCounter = registry.find("testName" + MicrometerMeterListener.METER_MALFORMED).counter(); + Counter malformedCounter = registry.find("testName.malformed.source").counter(); assertThat(malformedCounter).as("counter not registered").isNull(); listener.doOnMalformedOnNext(123); - malformedCounter = registry.find("testName" + MicrometerMeterListener.METER_MALFORMED).counter(); + malformedCounter = registry.find("testName.malformed.source").counter(); assertThat(malformedCounter).as("lazy counter registration").isNotNull(); assertThat(malformedCounter.count()).as("onNext malformed").isOne(); diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java index f5b266b017..aea4150f2a 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java @@ -78,7 +78,6 @@ public SampleTestRunnerConsumer yourCode() throws Exception { spansAssert.hasSize(4); assertThatMain - //FIXME reactor-defined Tags and KeyValues should be Documented .hasTag("reactor.status", "error") .hasTag("reactor.type", "Flux") .hasTag("interval", "500ms") diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java index 0530418c59..ee096fe8a1 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfigurationTest.java @@ -61,7 +61,7 @@ void fromFlux(@Nullable String name, @Nullable String tag) { assertThat(configuration.sequenceName) .as("sequenceName") - .isEqualTo(name == null ? MicrometerObservationListener.ANONYMOUS_OBSERVATION : name); + .isEqualTo(name == null ? "reactor.observation" : name); if (tag == null) { assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) @@ -101,7 +101,7 @@ void fromMono(@Nullable String name, @Nullable String tag) { assertThat(configuration.sequenceName) .as("sequenceName") - .isEqualTo(name == null ? MicrometerObservationListener.ANONYMOUS_OBSERVATION : name); + .isEqualTo(name == null ? "reactor.observation": name); if (tag == null) { assertThat(configuration.commonKeyValues.stream().map(t -> t.getKey() + "=" + t.getValue())) diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java index ade08c1d31..5faa41aefd 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java @@ -72,7 +72,7 @@ public long monotonicTime() { @Test void whenStartedFluxWithDefaultName() { configuration = new MicrometerObservationListenerConfiguration( - MicrometerObservationListener.ANONYMOUS_OBSERVATION, + DocumentedObservationListenerTags.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -131,7 +131,7 @@ void whenStartedFluxWithCustomName() { @Test void whenStartedMono() { configuration = new MicrometerObservationListenerConfiguration( - MicrometerObservationListener.ANONYMOUS_OBSERVATION, + DocumentedObservationListenerTags.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromMono (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -374,7 +374,7 @@ void observationStoppedByError() { @Test void observationGetsParentFromContext() { configuration = new MicrometerObservationListenerConfiguration( - MicrometerObservationListener.ANONYMOUS_OBSERVATION, + DocumentedObservationListenerTags.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -427,7 +427,7 @@ void observationGetsParentFromContext() { @Test void observationWithEmptyContextHasNoParent() { configuration = new MicrometerObservationListenerConfiguration( - MicrometerObservationListener.ANONYMOUS_OBSERVATION, + DocumentedObservationListenerTags.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -468,7 +468,7 @@ void observationWithEmptyContextHasNoParent() { @Test void observationWithEmptyContextHasParentWhenExternalScopeOpened() { configuration = new MicrometerObservationListenerConfiguration( - MicrometerObservationListener.ANONYMOUS_OBSERVATION, + DocumentedObservationListenerTags.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java index 1be3adb5d5..51fd5ac69b 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerTest.java @@ -69,7 +69,7 @@ void timedSchedulerReturnsAConfiguredTimedScheduler() { assertThat(id.getName()).as("prefix used") .isEqualTo("testSchedulerMetrics.scheduler.tasks.submitted"); assertThat(id.getTags()).as("tags") - .containsExactlyElementsOf(tags.and(TimedScheduler.TAG_SUBMISSION, TimedScheduler.SUBMISSION_DIRECT)); + .containsExactlyElementsOf(tags.and("submission.type", "direct")); }); } } \ No newline at end of file diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java index 183637b95f..c288626799 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java @@ -38,6 +38,7 @@ import reactor.test.AutoDisposingExtension; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * @author Simon Baslé @@ -60,21 +61,25 @@ void closeRegistry() { } @Test - void constructorAddsDotToPrefixIfNeeded() { + void aDotIsAddedToPrefix() { TimedScheduler test = new TimedScheduler(Schedulers.immediate(), registry, "noDot", Tags.empty()); assertThat(registry.getMeters()) .map(m -> m.getId().getName()) + .isNotEmpty() .allSatisfy(name -> assertThat(name).startsWith("noDot.")); } @Test - void constructorDoesntAddTwoDots() { + void constructorIgnoresDotAtEndOfMetricPrefix() { TimedScheduler test = new TimedScheduler(Schedulers.immediate(), registry, "dot.", Tags.empty()); assertThat(registry.getMeters()) .map(m -> m.getId().getName()) - .allSatisfy(name -> assertThat(name).doesNotContain("..")); + .isNotEmpty() + .allSatisfy(name -> assertThat(name) + .startsWith("dot.") + .doesNotContain("..")); } @Test From 72037b15276d9550e92621187abee66a72ce4c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Oct 2022 14:18:11 +0200 Subject: [PATCH 059/312] Adapt to Micrometer SNAPSHOTs before RC1 (#3215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit includes the following adaptations to a couple breaking changes: - context-propagation: ContextAccessor checks return a Class - micrometer: MeterDocumentation instead of DocumentedMeter Co-authored-by: Dariusz Jędrzejczyk --- gradle/asciidoc.gradle | 1 + gradle/libs.versions.toml | 8 +++---- .../observability/micrometer/Micrometer.java | 16 +++++++------- .../micrometer/MicrometerMeterListener.java | 22 +++++++++---------- ...MicrometerMeterListenerDocumentation.java} | 4 ++-- .../MicrometerObservationListener.java | 12 +++++----- ...meterObservationListenerConfiguration.java | 8 +++---- ...eterObservationListenerDocumentation.java} | 12 +++++----- .../micrometer/TimedScheduler.java | 6 ++--- ... => TimedSchedulerMeterDocumentation.java} | 4 ++-- .../MicrometerObservationListenerTest.java | 11 +++++----- .../util/context/ReactorContextAccessor.java | 8 +++---- .../context/ReactorContextAccessorTest.java | 8 +++---- 13 files changed, 60 insertions(+), 60 deletions(-) rename reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/{DocumentedMeterListenerMeters.java => MicrometerMeterListenerDocumentation.java} (97%) rename reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/{DocumentedObservationListenerTags.java => MicrometerObservationListenerDocumentation.java} (79%) rename reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/{DocumentedTimedSchedulerMeters.java => TimedSchedulerMeterDocumentation.java} (97%) diff --git a/gradle/asciidoc.gradle b/gradle/asciidoc.gradle index 36d9c2f026..b2b183880e 100644 --- a/gradle/asciidoc.gradle +++ b/gradle/asciidoc.gradle @@ -19,6 +19,7 @@ configure(rootProject) { apply plugin: 'org.asciidoctor.jvm.pdf' repositories { + maven { url 'https://repo.spring.io/snapshot' } maven { url 'https://repo.spring.io/milestone' } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41c7bd8387..e9015eb8a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.14" jmh = "1.35" junit = "5.9.0" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.0-M6" +micrometer = "1.10.0-SNAPSHOT" # was -M6 reactiveStreams = "1.0.4" [libraries] @@ -35,11 +35,11 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-M5" -micrometer-docsGenerator-bom = { module = "io.micrometer:micrometer-docs-generator-bom", version = "1.0.0-M7"} +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" # was -M5 +micrometer-docsGenerator-bom = { module = "io.micrometer:micrometer-docs-generator-bom", version = "1.0.0-SNAPSHOT"} # was -M7 micrometer-docsGenerator-metrics = { module = "io.micrometer:micrometer-docs-generator-metrics" } micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-M8" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" # was -M8 micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.7.0" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index b000ffc298..4a7e06aac1 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -47,7 +47,7 @@ public final class Micrometer { * the {@link reactor.core.publisher.Flux#name(String)} set upstream of the tap as id prefix if applicable * or default to {@link #DEFAULT_METER_PREFIX}. Similarly, upstream tags are gathered and added * to the default set of tags for meters. - * See {@link DocumentedMeterListenerMeters} for a documentation of the default set of meters and tags. + * See {@link MicrometerMeterListenerDocumentation} for a documentation of the default set of meters and tags. *

    * Note that some monitoring systems like Prometheus require to have the exact same set of * tags for each meter bearing the same name. @@ -55,7 +55,7 @@ public final class Micrometer { * @param the type of onNext in the target publisher * @param meterRegistry the {@link MeterRegistry} in which to register and publish metrics * @return a {@link SignalListenerFactory} to record metrics - * @see DocumentedMeterListenerMeters + * @see MicrometerMeterListenerDocumentation */ public static SignalListenerFactory metrics(MeterRegistry meterRegistry) { return new MicrometerMeterListenerFactory(meterRegistry); @@ -83,11 +83,11 @@ public final class Micrometer { * Similarly, Reactor tags defined upstream via eg. {@link reactor.core.publisher.Flux#tag(String, String)}) * are gathered and added to the default set of {@link io.micrometer.common.KeyValues} used by the Observation * as {@link Observation#lowCardinalityKeyValues(KeyValues) low cardinality keyValues}. - * See {@link DocumentedObservationListenerTags} for a documentation of the default set of tags. + * See {@link MicrometerObservationListenerDocumentation} for a documentation of the default set of tags. * * @param the type of onNext in the target publisher * @return a {@link SignalListenerFactory} to record observations - * @see DocumentedObservationListenerTags + * @see MicrometerObservationListenerDocumentation */ public static SignalListenerFactory observation(ObservationRegistry registry) { return new MicrometerObservationListenerFactory<>(registry); @@ -97,13 +97,13 @@ public final class Micrometer { * Wrap a {@link Scheduler} in an instance that gathers various task-related metrics using * the provided {@link MeterRegistry} and naming meters using the provided {@code metricsPrefix}. * Note that no common tags are set up for these meters. - * See {@link DocumentedTimedSchedulerMeters} for a documentation of the default set of meters and tags. + * See {@link TimedSchedulerMeterDocumentation} for a documentation of the default set of meters and tags. * * @param original the original {@link Scheduler} to decorate with metrics * @param meterRegistry the {@link MeterRegistry} in which to register the various meters * @param metricsPrefix the prefix to use in meter names. Must not end with a dot, which is automatically added. * @return a {@link Scheduler} that is instrumented with dedicated metrics - * @see DocumentedTimedSchedulerMeters + * @see TimedSchedulerMeterDocumentation */ public static Scheduler timedScheduler(Scheduler original, MeterRegistry meterRegistry, String metricsPrefix) { return new TimedScheduler(original, meterRegistry, metricsPrefix, Tags.empty()); @@ -114,14 +114,14 @@ public static Scheduler timedScheduler(Scheduler original, MeterRegistry meterRe * the provided {@link MeterRegistry} and naming meters using the provided {@code metricsPrefix}. * User-provided collection of common tags (ie. {@link Tags}) can also be provided to be added to * all the meters of that timed Scheduler. - * See {@link DocumentedTimedSchedulerMeters} for a documentation of the default set of meters and tags. + * See {@link TimedSchedulerMeterDocumentation} for a documentation of the default set of meters and tags. * * @param original the original {@link Scheduler} to decorate with metrics * @param meterRegistry the {@link MeterRegistry} in which to register the various meters * @param metricsPrefix the prefix to use in meter names. Must not end with a dot, which is automatically added. * @param tags the tags to put on meters * @return a {@link Scheduler} that is instrumented with dedicated metrics - * @see DocumentedTimedSchedulerMeters + * @see TimedSchedulerMeterDocumentation */ public static Scheduler timedScheduler(Scheduler original, MeterRegistry meterRegistry, String metricsPrefix, Iterable tags) { return new TimedScheduler(original, meterRegistry, metricsPrefix, tags); diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java index 1ba4a915b6..246d40d658 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListener.java @@ -29,8 +29,8 @@ import reactor.core.publisher.SignalType; import reactor.util.annotation.Nullable; -import static reactor.core.observability.micrometer.DocumentedMeterListenerMeters.CommonTags.*; -import static reactor.core.observability.micrometer.DocumentedMeterListenerMeters.TerminationTags.*; +import static reactor.core.observability.micrometer.MicrometerMeterListenerDocumentation.CommonTags.*; +import static reactor.core.observability.micrometer.MicrometerMeterListenerDocumentation.TerminationTags.*; /** * A {@link SignalListener} that activates metrics gathering using Micrometer 1.x. @@ -61,12 +61,12 @@ final class MicrometerMeterListener implements SignalListener { requestedCounter = null; } else { - this.onNextIntervalTimer = Timer.builder(DocumentedMeterListenerMeters.ON_NEXT_DELAY.getName(configuration.sequenceName)) + this.onNextIntervalTimer = Timer.builder(MicrometerMeterListenerDocumentation.ON_NEXT_DELAY.getName(configuration.sequenceName)) .tags(configuration.commonTags) .register(configuration.registry); if (!Micrometer.DEFAULT_METER_PREFIX.equals(configuration.sequenceName)) { - this.requestedCounter = DistributionSummary.builder(DocumentedMeterListenerMeters.REQUESTED_AMOUNT.getName(configuration.sequenceName)) + this.requestedCounter = DistributionSummary.builder(MicrometerMeterListenerDocumentation.REQUESTED_AMOUNT.getName(configuration.sequenceName)) .tags(configuration.commonTags) .register(configuration.registry); } @@ -198,7 +198,7 @@ public void handleListenerError(Throwable listenerError) { * cost only in case of cancellation. */ static void recordCancel(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { - Timer timer = Timer.builder(DocumentedMeterListenerMeters.FLOW_DURATION.getName(name)) + Timer timer = Timer.builder(MicrometerMeterListenerDocumentation.FLOW_DURATION.getName(name)) .tags(commonTags.and(TAG_CANCEL)) .description( "Times the duration elapsed between a subscription and the cancellation of the sequence") @@ -214,7 +214,7 @@ static void recordCancel(String name, Tags commonTags, MeterRegistry registry, T * with the added benefit of paying that cost only in case of onNext/onError after termination. */ static void recordMalformed(String name, Tags commonTags, MeterRegistry registry) { - registry.counter(DocumentedMeterListenerMeters.MALFORMED_SOURCE_EVENTS.getName(name), commonTags) + registry.counter(MicrometerMeterListenerDocumentation.MALFORMED_SOURCE_EVENTS.getName(name), commonTags) .increment(); } @@ -226,9 +226,9 @@ static void recordMalformed(String name, Tags commonTags, MeterRegistry registry */ static void recordOnError(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration, Throwable e) { - Timer timer = Timer.builder(DocumentedMeterListenerMeters.FLOW_DURATION.getName(name)) + Timer timer = Timer.builder(MicrometerMeterListenerDocumentation.FLOW_DURATION.getName(name)) .tags(commonTags.and(TAG_ON_ERROR)) - .tag(DocumentedMeterListenerMeters.TerminationTags.EXCEPTION.asString(), + .tag(MicrometerMeterListenerDocumentation.TerminationTags.EXCEPTION.asString(), e.getClass().getName()) .description( "Times the duration elapsed between a subscription and the onError termination of the sequence, with the exception name as a tag.") @@ -244,7 +244,7 @@ static void recordOnError(String name, Tags commonTags, MeterRegistry registry, * that cost only in case of completion (which is not always occurring). */ static void recordOnComplete(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { - Timer timer = Timer.builder(DocumentedMeterListenerMeters.FLOW_DURATION.getName(name)) + Timer timer = Timer.builder(MicrometerMeterListenerDocumentation.FLOW_DURATION.getName(name)) .tags(commonTags.and(TAG_ON_COMPLETE)) .description( "Times the duration elapsed between a subscription and the onComplete termination of a sequence that did emit some elements") @@ -260,7 +260,7 @@ static void recordOnComplete(String name, Tags commonTags, MeterRegistry registr * that cost only in case of completion (which is not always occurring). */ static void recordOnCompleteEmpty(String name, Tags commonTags, MeterRegistry registry, Timer.Sample flowDuration) { - Timer timer = Timer.builder(DocumentedMeterListenerMeters.FLOW_DURATION.getName(name)) + Timer timer = Timer.builder(MicrometerMeterListenerDocumentation.FLOW_DURATION.getName(name)) .tags(commonTags.and(TAG_ON_COMPLETE_EMPTY)) .description( "Times the duration elapsed between a subscription and the onComplete termination of a sequence that didn't emit any element") @@ -276,7 +276,7 @@ static void recordOnCompleteEmpty(String name, Tags commonTags, MeterRegistry re * that cost only in case of subscription. */ static void recordOnSubscribe(String name, Tags commonTags, MeterRegistry registry) { - Counter.builder(DocumentedMeterListenerMeters.SUBSCRIBED.getName(name)) + Counter.builder(MicrometerMeterListenerDocumentation.SUBSCRIBED.getName(name)) .tags(commonTags) .register(registry) .increment(); diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedMeterListenerMeters.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerDocumentation.java similarity index 97% rename from reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedMeterListenerMeters.java rename to reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerDocumentation.java index 4c25295128..b2eaa701b6 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedMeterListenerMeters.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerDocumentation.java @@ -19,7 +19,7 @@ import io.micrometer.common.docs.KeyName; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.docs.DocumentedMeter; +import io.micrometer.core.instrument.docs.MeterDocumentation; /** * Meters and tags used by {@link Micrometer#metrics(MeterRegistry)}. @@ -28,7 +28,7 @@ NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when the docs are generated by Micrometer's tool */ -public enum DocumentedMeterListenerMeters implements DocumentedMeter { +public enum MicrometerMeterListenerDocumentation implements MeterDocumentation { /** * Counts the number of events received from a malformed source (ie an onNext after an onComplete). diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java index b0a9ec5cea..42f16215f7 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java @@ -19,7 +19,7 @@ import io.micrometer.observation.Observation; import reactor.core.observability.SignalListener; -import reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags; +import reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags; import reactor.core.publisher.SignalType; import reactor.util.Logger; import reactor.util.Loggers; @@ -27,11 +27,11 @@ import reactor.util.context.Context; import reactor.util.context.ContextView; -import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.STATUS; -import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.TAG_STATUS_CANCELLED; -import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.TAG_STATUS_COMPLETED; -import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.TAG_STATUS_COMPLETED_EMPTY; -import static reactor.core.observability.micrometer.DocumentedObservationListenerTags.ObservationTags.TAG_STATUS_ERROR; +import static reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags.STATUS; +import static reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags.TAG_STATUS_CANCELLED; +import static reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags.TAG_STATUS_COMPLETED; +import static reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags.TAG_STATUS_COMPLETED_EMPTY; +import static reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags.TAG_STATUS_ERROR; /** * A {@link SignalListener} that makes timings using the {@link io.micrometer.observation.Observation} API from Micrometer 1.10. diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java index 922b6b86a7..0b87e210a9 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerConfiguration.java @@ -38,14 +38,14 @@ */ final class MicrometerObservationListenerConfiguration { - static final KeyValues DEFAULT_KV_FLUX = KeyValues.of(DocumentedObservationListenerTags.ObservationTags.TYPE.asString(), "Flux"); - static final KeyValues DEFAULT_KV_MONO = KeyValues.of(DocumentedObservationListenerTags.ObservationTags.TYPE.asString(), "Mono"); + static final KeyValues DEFAULT_KV_FLUX = KeyValues.of(MicrometerObservationListenerDocumentation.ObservationTags.TYPE.asString(), "Flux"); + static final KeyValues DEFAULT_KV_MONO = KeyValues.of(MicrometerObservationListenerDocumentation.ObservationTags.TYPE.asString(), "Mono"); private static final Logger LOGGER = Loggers.getLogger(MicrometerObservationListenerConfiguration.class); static MicrometerObservationListenerConfiguration fromFlux(Flux source, ObservationRegistry observationRegistry) { KeyValues defaultKeyValues = DEFAULT_KV_FLUX; - final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, DocumentedObservationListenerTags.ANONYMOUS.getName()); + final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, MicrometerObservationListenerDocumentation.ANONYMOUS.getName()); final KeyValues keyValues = resolveKeyValues(source, defaultKeyValues); return new MicrometerObservationListenerConfiguration(name, keyValues, observationRegistry, false); @@ -53,7 +53,7 @@ static MicrometerObservationListenerConfiguration fromFlux(Flux source, Obser static MicrometerObservationListenerConfiguration fromMono(Mono source, ObservationRegistry observationRegistry) { KeyValues defaultKeyValues = DEFAULT_KV_MONO; - final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, DocumentedObservationListenerTags.ANONYMOUS.getName()); + final String name = MicrometerMeterListenerConfiguration.resolveName(source, LOGGER, MicrometerObservationListenerDocumentation.ANONYMOUS.getName()); final KeyValues keyValues = resolveKeyValues(source, defaultKeyValues); return new MicrometerObservationListenerConfiguration(name, keyValues, observationRegistry, true); diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedObservationListenerTags.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerDocumentation.java similarity index 79% rename from reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedObservationListenerTags.java rename to reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerDocumentation.java index d78b68c1cd..af88fb4b2e 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedObservationListenerTags.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerDocumentation.java @@ -18,7 +18,7 @@ import io.micrometer.common.docs.KeyName; import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.docs.DocumentedObservation; +import io.micrometer.observation.docs.ObservationDocumentation; /** * Documentation of {@link Micrometer#observation(ObservationRegistry)} tags and of the anonymous variant @@ -28,7 +28,7 @@ NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when the docs are generated by Micrometer's tool */ -public enum DocumentedObservationListenerTags implements DocumentedObservation { +public enum MicrometerObservationListenerDocumentation implements ObservationDocumentation { /** * Anonymous version of the Micrometer.observation(), when the sequence hasn't been @@ -79,20 +79,20 @@ public String asString() { /** * {@link #STATUS} for when the subscription to the sequence was cancelled. */ - public static final String TAG_STATUS_CANCELLED = DocumentedMeterListenerMeters.TerminationTags.TAG_STATUS_CANCELLED; + public static final String TAG_STATUS_CANCELLED = MicrometerMeterListenerDocumentation.TerminationTags.TAG_STATUS_CANCELLED; /** * {@link #STATUS} for when the sequence completed with values. */ - public static final String TAG_STATUS_COMPLETED = DocumentedMeterListenerMeters.TerminationTags.TAG_STATUS_COMPLETED; + public static final String TAG_STATUS_COMPLETED = MicrometerMeterListenerDocumentation.TerminationTags.TAG_STATUS_COMPLETED; /** * {@link #STATUS} for when the sequence completed without value (no onNext). */ - public static final String TAG_STATUS_COMPLETED_EMPTY = DocumentedMeterListenerMeters.TerminationTags.TAG_STATUS_COMPLETED_EMPTY; + public static final String TAG_STATUS_COMPLETED_EMPTY = MicrometerMeterListenerDocumentation.TerminationTags.TAG_STATUS_COMPLETED_EMPTY; /** * {@link #STATUS} for when the sequence terminated with an error. The {@link io.micrometer.observation.Observation#error(Throwable)} * method is used to capture the exception. */ - public static final String TAG_STATUS_ERROR = DocumentedMeterListenerMeters.TerminationTags.TAG_STATUS_ERROR; + public static final String TAG_STATUS_ERROR = MicrometerMeterListenerDocumentation.TerminationTags.TAG_STATUS_ERROR; } diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java index a81ad2374e..862d4b012b 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java @@ -26,16 +26,16 @@ import io.micrometer.core.instrument.Timer; import reactor.core.Disposable; -import reactor.core.observability.micrometer.DocumentedTimedSchedulerMeters.SubmittedTags; +import reactor.core.observability.micrometer.TimedSchedulerMeterDocumentation.SubmittedTags; import reactor.core.scheduler.Scheduler; -import static reactor.core.observability.micrometer.DocumentedTimedSchedulerMeters.*; +import static reactor.core.observability.micrometer.TimedSchedulerMeterDocumentation.*; /** * An instrumented {@link Scheduler} wrapping an original {@link Scheduler} * and gathering metrics around submitted tasks. *

    - * See {@link DocumentedTimedSchedulerMeters} for the various metrics and tags associated with this class. + * See {@link TimedSchedulerMeterDocumentation} for the various metrics and tags associated with this class. * * @author Simon Baslé */ diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedTimedSchedulerMeters.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedSchedulerMeterDocumentation.java similarity index 97% rename from reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedTimedSchedulerMeters.java rename to reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedSchedulerMeterDocumentation.java index b1d01bd436..50fb42ab1f 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/DocumentedTimedSchedulerMeters.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedSchedulerMeterDocumentation.java @@ -19,7 +19,7 @@ import io.micrometer.common.docs.KeyName; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.docs.DocumentedMeter; +import io.micrometer.core.instrument.docs.MeterDocumentation; import reactor.core.scheduler.Scheduler; @@ -30,7 +30,7 @@ NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when the docs are generated by Micrometer's tool */ -public enum DocumentedTimedSchedulerMeters implements DocumentedMeter { +public enum TimedSchedulerMeterDocumentation implements MeterDocumentation { /** * Counter that increments by one each time a task is submitted (via any of the diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java index 5faa41aefd..8f8778420a 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java @@ -17,7 +17,6 @@ package reactor.core.observability.micrometer; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Clock; @@ -72,7 +71,7 @@ public long monotonicTime() { @Test void whenStartedFluxWithDefaultName() { configuration = new MicrometerObservationListenerConfiguration( - DocumentedObservationListenerTags.ANONYMOUS.getName(), + MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -131,7 +130,7 @@ void whenStartedFluxWithCustomName() { @Test void whenStartedMono() { configuration = new MicrometerObservationListenerConfiguration( - DocumentedObservationListenerTags.ANONYMOUS.getName(), + MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromMono (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -374,7 +373,7 @@ void observationStoppedByError() { @Test void observationGetsParentFromContext() { configuration = new MicrometerObservationListenerConfiguration( - DocumentedObservationListenerTags.ANONYMOUS.getName(), + MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -427,7 +426,7 @@ void observationGetsParentFromContext() { @Test void observationWithEmptyContextHasNoParent() { configuration = new MicrometerObservationListenerConfiguration( - DocumentedObservationListenerTags.ANONYMOUS.getName(), + MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, @@ -468,7 +467,7 @@ void observationWithEmptyContextHasNoParent() { @Test void observationWithEmptyContextHasParentWhenExternalScopeOpened() { configuration = new MicrometerObservationListenerConfiguration( - DocumentedObservationListenerTags.ANONYMOUS.getName(), + MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), registry, diff --git a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java index 29d86e6a93..ee07891d5e 100644 --- a/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java +++ b/reactor-core/src/main/java/reactor/util/context/ReactorContextAccessor.java @@ -37,8 +37,8 @@ public final class ReactorContextAccessor implements ContextAccessor { @Override - public boolean canReadFrom(Class contextType) { - return ContextView.class.isAssignableFrom(contextType); + public Class readableType() { + return ContextView.class; } @Override @@ -57,8 +57,8 @@ public T readValue(ContextView sourceContext, Object key) { } @Override - public boolean canWriteTo(Class contextType) { - return Context.class.isAssignableFrom(contextType); + public Class writeableType() { + return Context.class; } @Override diff --git a/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java b/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java index eafbdfe6fc..596cef966a 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/util/context/ReactorContextAccessorTest.java @@ -38,22 +38,22 @@ class ReactorContextAccessorTest { @Test void canReadFromContext() { - assertThat(accessor.canReadFrom(Context.class)).isTrue(); + assertThat(accessor.readableType().isAssignableFrom(Context.class)).isTrue(); } @Test void canReadFromContextView() { - assertThat(accessor.canReadFrom(ContextView.class)).isTrue(); + assertThat(accessor.readableType().isAssignableFrom(ContextView.class)).isTrue(); } @Test void canWriteToContext() { - assertThat(accessor.canWriteTo(Context.class)).isTrue(); + assertThat(accessor.writeableType().isAssignableFrom(Context.class)).isTrue(); } @Test void cannotWriteToContextView() { - assertThat(accessor.canWriteTo(ContextView.class)).isFalse(); + assertThat(accessor.writeableType().isAssignableFrom(ContextView.class)).isFalse(); } @Test From 5379c6f6cba27ac98989ed305a9eb9614642ae0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Oct 2022 15:08:40 +0200 Subject: [PATCH 060/312] Fix meters documentation generation following renames (#3217) This commit fixes the generation of meters documentation now that the scanned classes have been renamed. It also becomes less brittle to duplicate files when copying the pure generated asciidoc files, using a regexp to rename these. --- docs/asciidoc/metrics-details.adoc | 6 +++--- gradle/asciidoc.gradle | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/asciidoc/metrics-details.adoc b/docs/asciidoc/metrics-details.adoc index e72be84d62..ab23a5bbd9 100644 --- a/docs/asciidoc/metrics-details.adoc +++ b/docs/asciidoc/metrics-details.adoc @@ -11,7 +11,7 @@ IMPORTANT: Please note that metrics below use a dynamic `%s` prefix. When applied on a `Flux` or `Mono` that uses the `name(String n)` operator, this is replaced with `n`. Otherwise, this is replaced by the default value of `"reactor"`. -include::{root-target}meterListener.adoc[leveloffset=4] +include::{root-target}meterListener_metrics.adoc[leveloffset=4] #### `Micrometer.timedScheduler()` Below is the list of meters used by the TimedScheduler feature, as exposed via @@ -19,7 +19,7 @@ Below is the list of meters used by the TimedScheduler feature, as exposed via IMPORTANT: Please note that metrics below use a dynamic `%s` prefix. This is replaced with the provided `metricsPrefix` in practice. -include::{root-target}timedScheduler.adoc[leveloffset=4] +include::{root-target}timedScheduler_metrics.adoc[leveloffset=4] #### `Micrometer.observation()` Below is the list of meters used by the observation tap listener feature, as exposed via @@ -27,4 +27,4 @@ Below is the list of meters used by the observation tap listener feature, as exp This is the ANONYMOUS observation, but you can create a similar Observation with a custom name by using the `name(String)` operator. -include::{root-target}observation.adoc[leveloffset=4] \ No newline at end of file +include::{root-target}observation_metrics.adoc[leveloffset=4] \ No newline at end of file diff --git a/gradle/asciidoc.gradle b/gradle/asciidoc.gradle index b2b183880e..35257493b0 100644 --- a/gradle/asciidoc.gradle +++ b/gradle/asciidoc.gradle @@ -104,19 +104,19 @@ configure(rootProject) { task generateMeterListenerDocs(type: JavaExec) { mainClass.set("io.micrometer.docs.metrics.DocsFromSources") classpath configurations.adoc - args project.rootDir.getAbsolutePath(), ".*DocumentedMeter.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/meterListener").toAbsolutePath().toString() + args project.rootDir.getAbsolutePath(), ".*MicrometerMeterListenerDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/meterListener").toAbsolutePath().toString() } task generateTimedSchedulerDocs(type: JavaExec) { mainClass.set("io.micrometer.docs.metrics.DocsFromSources") classpath configurations.adoc - args project.rootDir.getAbsolutePath(), ".*DocumentedTimedScheduler.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/timedScheduler").toAbsolutePath().toString() + args project.rootDir.getAbsolutePath(), ".*TimedSchedulerMeterDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/timedScheduler").toAbsolutePath().toString() } task generateObservationDocs(type: JavaExec) { mainClass.set("io.micrometer.docs.metrics.DocsFromSources") classpath configurations.adoc - args project.rootDir.getAbsolutePath(), ".*DocumentedObservation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/observation").toAbsolutePath().toString() + args project.rootDir.getAbsolutePath(), ".*MicrometerObservationListenerDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/observation").toAbsolutePath().toString() } task polishGeneratedMetricsDocs(type: Copy) { @@ -125,15 +125,15 @@ configure(rootProject) { mustRunAfter "generateObservationDocs" from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/meterListener/") { include "_*.adoc" - rename '_metrics.adoc', 'meterListener.adoc' + rename '_(.*).adoc', 'meterListener_$1.adoc' } from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/timedScheduler/") { include "_*.adoc" - rename '_metrics.adoc', 'timedScheduler.adoc' + rename '_(.*).adoc', 'timedScheduler_$1.adoc' } from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/observation/") { include "_*.adoc" - rename '_metrics.adoc', 'observation.adoc' + rename '_(.*).adoc', 'observation_$1.adoc' } into project.rootProject.buildDir.toString() + "/documentedMetrics" filter { String line -> From ee7aa8a041072c57f800dcf8c7e1affa3c7c9fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Oct 2022 17:04:39 +0200 Subject: [PATCH 061/312] Add ContextPropagation runtime util + captureContext operator (#3145) This commit introduces a `ContextPropagation` runtime-detection utility class as well as a `contextCapture()` operator. If context-propagation isn't on the classpath, this operator is NO-OP. If context-propagation is on the classpath however, the operator will use it to capture relevant ThreadLocals during the subscription phase (at the point a contextWrite would be effected) and store it in the ContextView visible from upstream of the operator. Co-authored-by: Rossen Stoyanchev --- .../asciidoc/advanced-contextPropagation.adoc | 40 +++++ docs/asciidoc/advancedFeatures.adoc | 12 +- .../core/publisher/ContextPropagation.java | 120 ++++++++++++++ .../java/reactor/core/publisher/Flux.java | 21 +++ .../java/reactor/core/publisher/Mono.java | 21 +++ .../ContextPropagationNotThereSmokeTest.java | 55 +++++++ .../publisher/ContextPropagationTest.java | 152 ++++++++++++++++++ 7 files changed, 412 insertions(+), 9 deletions(-) create mode 100644 docs/asciidoc/advanced-contextPropagation.adoc create mode 100644 reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java create mode 100644 reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java diff --git a/docs/asciidoc/advanced-contextPropagation.adoc b/docs/asciidoc/advanced-contextPropagation.adoc new file mode 100644 index 0000000000..0c96e05175 --- /dev/null +++ b/docs/asciidoc/advanced-contextPropagation.adoc @@ -0,0 +1,40 @@ +[[context.propagation]] += Context-Propagation Support + +Since 3.5.0, Reactor-Core embeds support for the `io.micrometer:context-propagation` SPI. +This library is intended as a means to easily adapt between various implementations of the concept of a Context, of which +`ContextView`/`Context` is an example, and between `ThreadLocal` variables as well. + +`ReactorContextAccessor` allows the Context-Propagation library to understand Reactor `Context` and `Contextview`. +It implements the SPI and is loaded via `java.util.ServiceLoader`. +No user action is required, other than having a dependency on both reactor-core and `io.micrometer:context-propagation`. The `ReactorContextAccessor` class is public but shouldn't generally be accessed by user code. + +On top of that, Reactor-Core 3.5.0 also introduces the `contextCapture` operator that transparently deals with `ContextSnapshot`s if the library is available at runtime, for users' convenience. + +== `contextCapture` Operator +This operator can be used when one needs to capture `ThreadLocal` value(s) at subscription time and reflect these values in the Reactor `Context` for the benefit of upstream operators. +It relies on the `context-propagation` library and notably the registered `ThreadLocalAccessor`(s) to discover relevant ThreadLocal values. + +This is a convenient alternative to `contextWrite` which uses the `context-propagation` API to obtain a `ContextSnapshot` and then uses that snapshot to populate the Reactor `Context`. + +As a result, if there were any ThreadLocal values during subscription phase, for which there is a registered `ThreadLocalAccessor`, their values would now be stored in the Reactor `Context` and visible +at runtime in upstream operators. + +==== +[source,java] +---- +//assuming TL is known to Context-Propagation as key TLKEY. +static final ThreadLocal TL = new ThreadLocal<>(); + +//in the main thread, TL is set to "HELLO" +TL.set("HELLO"); + +Mono.deferContextual(ctx -> + Mono.delay(Duration.ofSeconds(1)) + //we're now in another thread, TL is not set + .map(v -> "delayed ctx[" + TLKEY + "]=" + ctx.getOrDefault(TLKEY, "not found") + ", TL=" + TL.get()) +) +.contextCapture() +.block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" +---- +==== \ No newline at end of file diff --git a/docs/asciidoc/advancedFeatures.adoc b/docs/asciidoc/advancedFeatures.adoc index 8fcbf74fba..2e44f624cd 100644 --- a/docs/asciidoc/advancedFeatures.adoc +++ b/docs/asciidoc/advancedFeatures.adoc @@ -11,6 +11,7 @@ This chapter covers advanced features and concepts of Reactor, including the fol * <> * <> * <> +* <> * <> * <> @@ -841,15 +842,6 @@ TIP: In order to read from the `Context` without misleading users into thinking while data is running through the pipeline, only the `ContextView` is exposed by the operators above. In case one needs to use one of the remaining APIs that still require a `Context`, one can use `Context.of(contextView)` for conversion. -[[context.propagation]] -=== Micrometer Context-Propagation Support -Since 3.5.0, Reactor-Core embeds basic support for the `io.micrometer:context-propagation` SPI. -This library is intended as a mean to easily adapt between various implementations of the concept of a Context, of which -`ContextView`/`Context` is an example, and between `ThreadLocal` variables as well. - -`ReactorContextAccessor` is one implementation of this SPI that is loaded via `ServiceLoader`. No user action is required, -other than depending on reactor-core and `context-propagation` (the class is public but shouldn't generally be accessed by user code). - === Simple `Context` Examples The examples in this section are meant as ways to better understand some of the caveats of @@ -1088,6 +1080,8 @@ public void contextForLibraryReactivePut() { ---- ==== +include::advanced-contextPropagation.adoc[leveloffset=1] + [[cleanup]] == Dealing with Objects that Need Cleanup diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java new file mode 100644 index 0000000000..be6a5b2ba1 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.function.Function; +import java.util.function.Predicate; + +import io.micrometer.context.ContextRegistry; +import io.micrometer.context.ContextSnapshot; + +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +/** + * Utility private class to detect if the context-propagation library is on the classpath and to offer + * ContextSnapshot support to {@link Flux} and {@link Mono}. + * + * @author Simon Baslé + */ +final class ContextPropagation { + + static final boolean isContextPropagationAvailable; + + static final Predicate PREDICATE_TRUE = v -> true; + static final Function NO_OP = c -> c; + static final Function WITH_GLOBAL_REGISTRY_NO_PREDICATE; + + static { + boolean contextPropagation; + try { + Class.forName("io.micrometer.context.ContextRegistry", false, ContextPropagation.class.getClassLoader()); + contextPropagation = true; + } + catch (Throwable t) { + contextPropagation = false; + } + isContextPropagationAvailable = contextPropagation; + if (contextPropagation) { + WITH_GLOBAL_REGISTRY_NO_PREDICATE = new ContextCaptureFunction(PREDICATE_TRUE, ContextRegistry.getInstance()); + } + else { + WITH_GLOBAL_REGISTRY_NO_PREDICATE = NO_OP; + } + } + + /** + * Is Micrometer {@code context-propagation} API on the classpath? + * + * @return true if context-propagation is available at runtime, false otherwise + */ + static boolean isContextPropagationAvailable() { + return isContextPropagationAvailable; + } + + /** + * Create a support function that takes a snapshot of thread locals and merges them with the + * provided {@link Context}, resulting in a new {@link Context} which includes entries + * captured from threadLocals by the Context-Propagation API. + * + * @return the {@link Context} augmented with captured entries + */ + public static Function contextCapture() { + if (!isContextPropagationAvailable) { + return NO_OP; + } + return WITH_GLOBAL_REGISTRY_NO_PREDICATE; + } + + /** + * Create a support function that takes a snapshot of thread locals and merges them with the + * provided {@link Context}, resulting in a new {@link Context} which includes entries + * captured from threadLocals by the Context-Propagation API. + *

    + * The provided {@link Predicate} is used on keys associated to said thread locals + * by the Context-Propagation API to filter which entries should be captured in the + * first place. + * + * @param captureKeyPredicate a {@link Predicate} used on keys to determine if each entry + * should be injected into the new {@link Context} + * @return a {@link Function} augmenting {@link Context} with captured entries + */ + public static Function contextCapture(Predicate captureKeyPredicate) { + if (!isContextPropagationAvailable) { + return NO_OP; + } + return new ContextCaptureFunction(captureKeyPredicate, null); + } + + //the Function indirection allows tests to directly assert code in this class rather than static methods + static final class ContextCaptureFunction implements Function { + + final Predicate capturePredicate; + final ContextRegistry registry; + + ContextCaptureFunction(Predicate capturePredicate, @Nullable ContextRegistry registry) { + this.capturePredicate = capturePredicate; + this.registry = registry != null ? registry : ContextRegistry.getInstance(); + } + + @Override + public Context apply(Context target) { + return ContextSnapshot.captureAllUsing(capturePredicate, this.registry).updateContext(target); + } + } + +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 0c860aad40..24dede4e78 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -4029,6 +4029,27 @@ public final Flux concatWith(Publisher other) { return concat(this, other); } + /** + * If context-propagation library + * is on the classpath, this is a convenience shortcut to capture thread local values during the + * subscription phase and put them in the {@link Context} that is visible upstream of this operator. + *

    + * As a result this operator should generally be used as close as possible to the end of + * the chain / subscription point. + *

    + * If context-propagation is not available at runtime, this operator simply returns the current {@link Flux} + * instance. + * + * @return a new {@link Flux} where context-propagation API has been used to capture entries and + * inject them into the {@link Context} + */ + public final Flux contextCapture() { + if (!ContextPropagation.isContextPropagationAvailable()) { + return this; + } + return onAssembly(new FluxContextWrite<>(this, ContextPropagation.contextCapture())); + } + /** * Enrich the {@link Context} visible from downstream for the benefit of upstream * operators, by making all values from the provided {@link ContextView} visible on top diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index d10a0727ce..4133a0489a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -2227,6 +2227,27 @@ public final Flux concatWith(Publisher other) { return Flux.concat(this, other); } + /** + * If context-propagation library + * is on the classpath, this is a convenience shortcut to capture thread local values during the + * subscription phase and put them in the {@link Context} that is visible upstream of this operator. + *

    + * As a result this operator should generally be used as close as possible to the end of + * the chain / subscription point. + *

    + * If context-propagation is not available at runtime, this operator simply returns the current {@link Mono} + * instance. + * + * @return a new {@link Flux} where context-propagation API has been used to capture entries and + * inject them into the {@link Context} + */ + public final Mono contextCapture() { + if (!ContextPropagation.isContextPropagationAvailable()) { + return this; + } + return onAssembly(new MonoContextWrite<>(this, ContextPropagation.contextCapture())); + } + /** * Enrich the {@link Context} visible from downstream for the benefit of upstream * operators, by making all values from the provided {@link ContextView} visible on top diff --git a/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java b/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java new file mode 100644 index 0000000000..5b2f310ad3 --- /dev/null +++ b/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * For tests that actually assert things when context-propagation is available, see + * {@code withMicrometer} test set. + * @author Simon Baslé + */ +class ContextPropagationNotThereSmokeTest { + + @Test + void contextPropagationIsNotAvailable() { + assertThat(ContextPropagation.isContextPropagationAvailable()).isFalse(); + } + + @Test + void contextCaptureIsNoOp() { + assertThat(ContextPropagation.contextCapture()).as("without predicate").isSameAs(ContextPropagation.NO_OP); + assertThat(ContextPropagation.contextCapture(v -> true)).as("with predicate").isSameAs(ContextPropagation.NO_OP); + } + + @Test + void contextCaptureFluxApiIsNoOp() { + Flux source = Flux.empty(); + assertThat(source.contextCapture()).isSameAs(source); + } + + @Test + void contextCaptureMonoApiIsNoOp() { + Mono source = Mono.empty(); + assertThat(source.contextCapture()).isSameAs(source); + } + +} diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java new file mode 100644 index 0000000000..0263d842b7 --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import io.micrometer.context.ContextRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import reactor.util.context.Context; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Simon Baslé + */ +class ContextPropagationTest { + + private static final String KEY1 = "key1"; + private static final String KEY2 = "key2"; + + private static final AtomicReference REF1 = new AtomicReference<>(); + private static final AtomicReference REF2 = new AtomicReference<>(); + + //NOTE: no way to currently remove accessors from the ContextRegistry, so we recreate one on each test + private ContextRegistry registry; + + @BeforeEach + void setup() { + registry = new ContextRegistry().loadContextAccessors(); + + REF1.set("ref1_init"); + REF2.set("ref2_init"); + + registry.registerThreadLocalAccessor( + KEY1, REF1::get, REF1::set, () -> REF1.set(null)); + + registry.registerThreadLocalAccessor( + KEY2, REF2::get, REF2::set, () -> REF2.set(null)); + } + + @Test + void isContextPropagationAvailable() { + assertThat(ContextPropagation.isContextPropagationAvailable()).isTrue(); + } + + + @Test + void contextCaptureWithNoPredicateReturnsTheConstantFunction() { + assertThat(ContextPropagation.contextCapture()) + .as("no predicate nor registry") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + .hasFieldOrPropertyWithValue("registry", ContextRegistry.getInstance()); + } + + @Test + void contextCaptureWithPredicateReturnsNewFunctionWithGlobalRegistry() { + Function test = ContextPropagation.contextCapture(ContextPropagation.PREDICATE_TRUE); + + assertThat(test) + .as("predicate, no registry") + .isNotNull() + .isNotSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + .isNotSameAs(ContextPropagation.NO_OP) + // as long as a predicate is supplied, the method creates new instances of the Function + .isNotSameAs(ContextPropagation.contextCapture(ContextPropagation.PREDICATE_TRUE)) + .isInstanceOfSatisfying(ContextPropagation.ContextCaptureFunction.class, f -> + assertThat(f.registry).as("function default registry").isSameAs(ContextRegistry.getInstance())); + } + + @Test + void fluxApiUsesContextPropagationConstantFunction() { + Flux source = Flux.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(FluxContextWrite.class, fcw -> + assertThat(fcw.doOnContext) + .as("flux's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Test + void monoApiUsesContextPropagationConstantFunction() { + Mono source = Mono.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(MonoContextWrite.class, fcw -> + assertThat(fcw.doOnContext) + .as("mono's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Nested + class ContextCaptureFunctionTest { + + @Test + void contextCaptureFunctionWithoutFiltering() { + ContextPropagation.ContextCaptureFunction test = new ContextPropagation.ContextCaptureFunction( + ContextPropagation.PREDICATE_TRUE, registry); + + Context ctx = test.apply(Context.empty()); + Map asMap = new HashMap<>(); + ctx.forEach(asMap::put); //easier to assert + + assertThat(asMap) + .containsEntry(KEY1, "ref1_init") + .containsEntry(KEY2, "ref2_init") + .hasSize(2); + } + + @Test + void captureWithFiltering() { + ContextPropagation.ContextCaptureFunction test = new ContextPropagation.ContextCaptureFunction( + k -> k.toString().equals(KEY2), registry); + + Context ctx = test.apply(Context.empty()); + Map asMap = new HashMap<>(); + ctx.forEach(asMap::put); //easier to assert + + assertThat(asMap) + .containsEntry(KEY2, "ref2_init") + .hasSize(1); + } + + @Test + void captureFunctionWithNullRegistryUsesGlobalRegistry() { + ContextPropagation.ContextCaptureFunction test = new ContextPropagation.ContextCaptureFunction(v -> true, null); + + assertThat(test.registry).as("default registry").isSameAs(ContextRegistry.getInstance()); + } + } + +} From 6fc9496005a822e440bfc1d65b6e2a94bf7a3fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 10 Oct 2022 10:56:53 +0200 Subject: [PATCH 062/312] Micrometer Observation test: ensure assert fails the test (#3220) This commit ensures that in MicrometerObservationIntegrationTest, the last assertion that checks the delay thread for leftover Span can actually fail the test if an unexpected Span is discovered. This is done by capturing the currentSpan() in the thread, but asserting it in the main test body (otherwise the AssertionError would just get logged). Also adds a few comments as to why that particular assertion used to fail for a while due to Brave defaults, and a javadoc comment on how to run the test. Fixes #3214. --- .../MicrometerObservationIntegrationTest.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java index aea4150f2a..ecd6edd642 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import io.micrometer.tracing.Span; @@ -34,6 +35,12 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * To run this integration test on an actual Zipkin instance, one can use Docker: + *

    + * {@code docker run -d -p 9411:9411 openzipkin/zipkin} + *

    + * Then open http://localhost:9411/zipkin/ in your browser. + * * @author Simon Baslé */ @Tag("slow") @@ -109,18 +116,30 @@ public SampleTestRunnerConsumer yourCode() throws Exception { .isEqualTo(beforeStart); //original span was restored //finally, assert that the delay thread was not polluted either + /* + Impl note: This assertion is a bit redundant since we don't use Scope anyway so there shouldn't + be any possibility of polluting ThreadLocals. It used to fail for Brave because Brave defaults to + using InheritableThreadLocals. Now that SampleTestRunner configures Brave to use simple ThreadLocal, + it works both for OTel and Brave. + The atomic ref ensures that the Tracer is used and found nothing. It also allows the test to fail + by ensuring only the Span capture is done in separate thread (the assertion has to be done in main + testing thread). + */ + String notCaptured = "tracer.currentSpan() not invoked"; + AtomicReference delaySpanRef = new AtomicReference<>(notCaptured); CountDownLatch latch = new CountDownLatch(1); delayScheduler.schedule(() -> { try { - assertThat(bb.getTracer().currentSpan()) - .as("no leftover span in delay thread") - .isNull(); + delaySpanRef.set(bb.getTracer().currentSpan()); } finally { latch.countDown(); } }); latch.await(10, TimeUnit.SECONDS); + assertThat(delaySpanRef.get()) + .as("no leftover span in delay thread") + .isNull(); }; } } From de4383891f0f60a342a2219df36a77aba10a5c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 10 Oct 2022 12:01:24 +0200 Subject: [PATCH 063/312] Polish Micrometer Docs generation (#3225) - switch to new transverse docs generator artifact - polish sanitizing of generated docs, check included files exist - Polish javadoc of meters now that code and links work --- gradle/asciidoc.gradle | 17 ++++++++----- gradle/libs.versions.toml | 3 +-- .../MicrometerMeterListenerDocumentation.java | 24 +++++++++---------- ...meterObservationListenerDocumentation.java | 10 +++----- .../TimedSchedulerMeterDocumentation.java | 14 +++++------ 5 files changed, 32 insertions(+), 36 deletions(-) diff --git a/gradle/asciidoc.gradle b/gradle/asciidoc.gradle index 35257493b0..7e76f88615 100644 --- a/gradle/asciidoc.gradle +++ b/gradle/asciidoc.gradle @@ -90,8 +90,7 @@ configure(rootProject) { } dependencies { - adoc platform(libs.micrometer.docsGenerator.bom) - adoc libs.micrometer.docsGenerator.metrics + adoc libs.micrometer.docsGenerator } task generateObservabilityDocs(dependsOn: [ @@ -102,19 +101,19 @@ configure(rootProject) { } task generateMeterListenerDocs(type: JavaExec) { - mainClass.set("io.micrometer.docs.metrics.DocsFromSources") + mainClass.set("io.micrometer.docs.DocsGeneratorCommand") classpath configurations.adoc args project.rootDir.getAbsolutePath(), ".*MicrometerMeterListenerDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/meterListener").toAbsolutePath().toString() } task generateTimedSchedulerDocs(type: JavaExec) { - mainClass.set("io.micrometer.docs.metrics.DocsFromSources") + mainClass.set("io.micrometer.docs.DocsGeneratorCommand") classpath configurations.adoc args project.rootDir.getAbsolutePath(), ".*TimedSchedulerMeterDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/timedScheduler").toAbsolutePath().toString() } task generateObservationDocs(type: JavaExec) { - mainClass.set("io.micrometer.docs.metrics.DocsFromSources") + mainClass.set("io.micrometer.docs.DocsGeneratorCommand") classpath configurations.adoc args project.rootDir.getAbsolutePath(), ".*MicrometerObservationListenerDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/observation").toAbsolutePath().toString() } @@ -139,11 +138,17 @@ configure(rootProject) { filter { String line -> line.startsWith('[[observability-metrics]]') || line.startsWith('=== Observability - Metrics') || - line.startsWith('Below you can find a list of all samples ') || + line.startsWith('Below you can find a list of all ') || line.startsWith("Fully qualified name of the enclosing class ") ? null : line } filter { String line -> line.startsWith("====") ? line.replaceFirst("====", "=") : line } + doLast { + //since these are the files that get explicitly included in asciidoc, smoke test they exist + assert file(project.rootProject.buildDir.toString() + "/documentedMetrics/meterListener_metrics.adoc").exists() + assert file(project.rootProject.buildDir.toString() + "/documentedMetrics/timedScheduler_metrics.adoc").exists() + assert file(project.rootProject.buildDir.toString() + "/documentedMetrics/observation_metrics.adoc").exists() + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b08aabed8b..bdb34a13c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,8 +36,7 @@ micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micro micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" # was -M5 -micrometer-docsGenerator-bom = { module = "io.micrometer:micrometer-docs-generator-bom", version = "1.0.0-SNAPSHOT"} # was -M7 -micrometer-docsGenerator-metrics = { module = "io.micrometer:micrometer-docs-generator-metrics" } +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.0-SNAPSHOT"} # was -M7 micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" # was -M8 micrometer-test = { module = "io.micrometer:micrometer-test" } diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerDocumentation.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerDocumentation.java index b2eaa701b6..8ba70cb403 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerDocumentation.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerMeterListenerDocumentation.java @@ -24,10 +24,6 @@ /** * Meters and tags used by {@link Micrometer#metrics(MeterRegistry)}. */ -/* -NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when -the docs are generated by Micrometer's tool -*/ public enum MicrometerMeterListenerDocumentation implements MeterDocumentation { /** @@ -56,7 +52,7 @@ public Meter.Type getType() { ON_NEXT_DELAY { @Override public String getBaseUnit() { - return ""; //FIXME nanoseconds? milliseconds + return "nanoseconds"; } @Override @@ -76,7 +72,8 @@ public Meter.Type getType() { }, /** - * Counts the amount requested to a named sequence (eg. Flux.name(String)) by all subscribers, until at least one requests an unbounded amount. + * Counts the amount requested to a named sequence (eg. {@code Flux.name(String)}) + * by all subscribers, until at least one requests an unbounded amount. */ REQUESTED_AMOUNT { @Override @@ -117,7 +114,8 @@ public Meter.Type getType() { /** * Times the duration elapsed between a subscription and the termination or cancellation of the sequence. - * A TerminationTags#STATUS tag is added to specify what event caused the timer to end (completed, completedEmpty, error, cancelled). + * A TerminationTags#STATUS tag is added to specify what event caused the timer to end + * ({@code "completed"}, {@code "completedEmpty"}, {@code "error"} or {@code "cancelled"}). */ FLOW_DURATION { @Override @@ -145,7 +143,7 @@ public Meter.Type getType() { public enum CommonTags implements KeyName { /** - * The type of the sequence (Flux or Mono). + * The type of the sequence ({@code "Flux"} or {@code "Mono"}). */ TYPE { @Override @@ -174,10 +172,10 @@ public enum TerminationTags implements KeyName { /** * The termination status: *
      - *
    • TAG_STATUS_COMPLETED for a sequence that terminates with an onComplete, with onNext(s)
    • - *
    • TAG_STATUS_COMPLETED_EMPTY for a sequence that terminates without any onNext before the onComplete
    • - *
    • TAG_STATUS_ERROR for a sequence that terminates with an onError
    • - *
    • TAG_STATUS_CANCELLED for a sequence that has cancelled its subscription
    • + *
    • {@code "completed"} for a sequence that terminates with an onComplete, with onNext(s)
    • + *
    • {@code "completedEmpty"} for a sequence that terminates without any onNext before the onComplete
    • + *
    • {@code "error"} for a sequence that terminates with an onError
    • + *
    • {@code "cancelled"} for a sequence that has cancelled its subscription
    • *
    */ STATUS { @@ -188,7 +186,7 @@ public String asString() { }, /** - * Tag used by FLOW_DURATION when STATUS is TAG_STATUS_ERROR, to store the + * Tag used by FLOW_DURATION when STATUS is {@code "error"}, to store the * exception that occurred. */ EXCEPTION { diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerDocumentation.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerDocumentation.java index af88fb4b2e..e3da46ba5c 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerDocumentation.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerDocumentation.java @@ -24,10 +24,6 @@ * Documentation of {@link Micrometer#observation(ObservationRegistry)} tags and of the anonymous variant * of the observation (no {@link reactor.core.publisher.Flux#name(String)}). */ -/* -NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when -the docs are generated by Micrometer's tool -*/ public enum MicrometerObservationListenerDocumentation implements ObservationDocumentation { /** @@ -57,8 +53,8 @@ public KeyName[] getLowCardinalityKeyNames() { */ public static enum ObservationTags implements KeyName { /** - * The status of the sequence, which indicates how it terminated (completed, completedEmpty, - * error or cancelled). + * The status of the sequence, which indicates how it terminated ({@code "completed"}, + * {@code "completedEmpty"}, {@code "error"} or {@code "cancelled"}). */ STATUS { @Override @@ -67,7 +63,7 @@ public String asString() { } }, /** - * The type of the sequence, i.e. Flux or Mono. + * The type of the sequence, i.e. {@code "Flux"} or {@code "Mono"}. */ TYPE { @Override diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedSchedulerMeterDocumentation.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedSchedulerMeterDocumentation.java index 50fb42ab1f..7bc05b04b8 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedSchedulerMeterDocumentation.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedSchedulerMeterDocumentation.java @@ -16,6 +16,8 @@ package reactor.core.observability.micrometer; +import java.util.concurrent.TimeUnit; + import io.micrometer.common.docs.KeyName; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; @@ -26,10 +28,6 @@ /** * Meters and tags used by {@link Micrometer#timedScheduler(Scheduler, MeterRegistry, String)}. */ -/* -NOTE that javadocs avoid using links / code taglets as these are replaced with blanks when -the docs are generated by Micrometer's tool -*/ public enum TimedSchedulerMeterDocumentation implements MeterDocumentation { /** @@ -118,10 +116,10 @@ public enum SubmittedTags implements KeyName { /** * The type of submission: *
      - *
    • #SUBMISSION_DIRECT for Scheduler#schedule(Runnable)
    • - *
    • #SUBMISSION_DELAYED for Scheduler#schedule(Runnable, long, TimeUnit)
    • - *
    • #SUBMISSION_PERIODIC_INITIAL for Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit) after the initial delay
    • - *
    • #SUBMISSION_PERIODIC_ITERATION for Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit) further periodic iterations
    • + *
    • {@code "direct"} for {@link Scheduler#schedule(Runnable)}
    • + *
    • {@code "delayed"} for {@link Scheduler#schedule(Runnable, long, TimeUnit)}
    • + *
    • {@code "periodic_initial"} for {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} after the initial delay
    • + *
    • {@code "periodic_iteration"} for {@link Scheduler#schedulePeriodically(Runnable, long, long, TimeUnit)} further periodic iterations
    • *
    */ SUBMISSION { From 4e284fc1a182dc80df0847323d331eaa8a5c8028 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 11 Oct 2022 09:19:33 +0300 Subject: [PATCH 064/312] Update Micrometer dependency to RC1 (#3228) --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bdb34a13c1..f8412cddf1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.17" jmh = "1.35" junit = "5.9.1" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.0-SNAPSHOT" # was -M6 +micrometer = "1.10.0-RC1" reactiveStreams = "1.0.4" [libraries] @@ -35,10 +35,10 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" # was -M5 -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.0-SNAPSHOT"} # was -M7 +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-RC1" +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.0-RC1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-SNAPSHOT" # was -M8 +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-RC1" micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.8.0" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From c78c70c5ad6c635f6b003bd48c1767c34a11ce67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 11 Oct 2022 10:33:15 +0200 Subject: [PATCH 065/312] Rework the refguide section on metrics to focus on new module (#3205) This commit reworks the reference guide section on metrics which now focuses on `reactor-core-micrometer` module: - replacement APIs for publisher and scheduler metrics - `Observation` API which is entirely new Since meters and tags documentation is now generated, relevant cross links have been added. Fixes #3108. --- docs/asciidoc/metrics-details.adoc | 3 + docs/asciidoc/metrics.adoc | 128 +++++++++++++++++------------ 2 files changed, 79 insertions(+), 52 deletions(-) diff --git a/docs/asciidoc/metrics-details.adoc b/docs/asciidoc/metrics-details.adoc index ab23a5bbd9..529c4a6ef6 100644 --- a/docs/asciidoc/metrics-details.adoc +++ b/docs/asciidoc/metrics-details.adoc @@ -3,6 +3,7 @@ ### Meters and tags for Reactor-Core-Micrometer module +[[micrometer-details-metrics]] #### `Micrometer.metrics()` Below is the list of meters used by the metrics tap listener feature, as exposed via `Micrometer.metrics(MeterRegistry meterRegistry)`. @@ -13,6 +14,7 @@ Otherwise, this is replaced by the default value of `"reactor"`. include::{root-target}meterListener_metrics.adoc[leveloffset=4] +[[micrometer-details-timedScheduler]] #### `Micrometer.timedScheduler()` Below is the list of meters used by the TimedScheduler feature, as exposed via `Micrometer.timedScheduler(Scheduler original, MeterRegistry meterRegistry, String metricsPrefix)`. @@ -21,6 +23,7 @@ IMPORTANT: Please note that metrics below use a dynamic `%s` prefix. This is rep include::{root-target}timedScheduler_metrics.adoc[leveloffset=4] +[[micrometer-details-observation]] #### `Micrometer.observation()` Below is the list of meters used by the observation tap listener feature, as exposed via `Micrometer.observation(ObservationRegistry registry)`. diff --git a/docs/asciidoc/metrics.adoc b/docs/asciidoc/metrics.adoc index 09d6b14d47..4bb292560a 100644 --- a/docs/asciidoc/metrics.adoc +++ b/docs/asciidoc/metrics.adoc @@ -4,41 +4,54 @@ Project Reactor is a library designed for performance and better utilization of resources. But to truly understand the performance of a system, it is best to be able to monitor its various components. -This is why Reactor provides a built-in integration with https://micrometer.io[Micrometer]. +This is why Reactor provides a built-in integration with https://micrometer.io[Micrometer] via the `reactor-core-micrometer` module. +Introduced in the `2022.0 BOM` release, the module provides an explicit dependency to Micrometer, which allows it to offer fine-tuned APIs for metrics and observations. -TIP: If Micrometer is not on the classpath, metrics will be a no-op. +NOTE: Up to Reactor-Core `3.5.0`, metrics were implemented as operators that would be no-op if Micrometer wasn't on the classpath. + +The `reactor-core-micrometer` APIs require the user to provide a form of _registry_ explicitly instead of relying on a hardcoded global registry. +When applying instrumentation to classes that have a NATIVE notion of naming or tags, these APIs will attempt to discover such elements in the reactive chain. +Otherwise, the API will expect that a _prefix_ for naming meters is provided alongside the registry. == Scheduler metrics Every async operation in Reactor is done via the Scheduler abstraction described in <>. This is why it is important to monitor your schedulers, watch out for key metrics that start to look suspicious and react accordingly. -To enable scheduler metrics, you will need to use the following method: +The `reactor-core-micrometer` module offers a "timed" `Scheduler` wrapper that perform measurements around tasks submitted through it, which can be used as follows: ==== [source,java] ---- -Schedulers.enableMetrics(); +Scheduler originalScheduler = Schedulers.newParallel("test", 4); + +Scheduler schedulerWithMetrics = Micrometer.timedScheduler( + originalScheduler, // <1> + applicationDefinedMeterRegistry, // <2> + "testingMetrics", // <3> + Tags.of(Tag.of("additionalTag", "yes")) // <4> +); ---- ==== +<1> the `Scheduler` to wrap +<2> the `MeterRegistry` in which to publish metrics +<3> the prefix to use in naming meters. This would for example lead to a `testingMetrics.scheduler.tasks.completed` meter being created. +<4> optional tags to add to all the meters created for that wrapping `Scheduler` -WARNING: The instrumentation is performed when a scheduler is created. It is recommended to call this method as early as possible. - -TIP: If you're using Spring Boot, it is a good idea to place the invocation before `SpringApplication.run(Application.class, args)` call. - -Once scheduler metrics are enabled and provided it is on the classpath, Reactor will use Micrometer's support for instrumenting the executors that back most schedulers. - -Please refer to https://micrometer.io/docs/ref/jvm[Micrometer's documentation] for the exposed metrics. +IMPORTANT: When wrapping a common `Scheduler` (eg. `Schedulers.single()`) or a `Scheduler` that is used in multiple places, only the `Runnable` tasks that are +submitted through the wrapper instance returned by `Micrometer#timedScheduler` are going to be instrumented. -Since one scheduler may have multiple executors, every executor metric has a `reactor_scheduler_id` tag. +See <> for produced meters and associated default tags. -TIP: Grafana + Prometheus users can use https://raw.githubusercontent.com/reactor/reactor-monitoring-demo/master/dashboards/schedulers.json[a pre-built dashboard] which includes panels for threads, completed tasks, task queues and other handy metrics. +// FIXME reactor-monitoring-demo won't be in sync with 3.5.0 anymore +//TIP: Grafana + Prometheus users can use https://raw.githubusercontent.com/reactor/reactor-monitoring-demo/master/dashboards/schedulers.json[a pre-built dashboard] which includes panels for threads, completed tasks, task queues and other handy metrics. == Publisher metrics Sometimes it is useful to be able to record metrics at some stage in your reactive pipeline. -One way to do it would be to manually push the values to your metrics backend of choice. -Another option would be to use Reactor's built-in metrics integration for `Flux`/`Mono` and interpret them. +One way to do it would be to manually push the values to your metrics backend of choice from a custom `SignalListener` +provided to the `tap` operator. +An out-of-the-box implementation is actually provided by the `reactor-core-micrometer` module, via `Micrometer#metrics` APIs. Consider the following pipeline: ==== [source,java] @@ -51,70 +64,81 @@ listenToEvents() ---- ==== -To enable the metrics for this source `Flux` (returned from `listenToEvents()`), we need to turn on the metrics collecting: +To enable the metrics for this source `Flux` (returned from `listenToEvents()`), we need to turn on the metrics collection: ==== [source,java] ---- listenToEvents() - .name("events") <1> - .metrics() <2> + .name("events") // <1> + .tap(Micrometer.metrics( // <2> + applicationDefinedMeterRegistry // <3> + )) .doOnNext(event -> log.info("Received {}", event)) .delayUntil(this::processEvent) .retry() .subscribe(); ---- -<1> Every metric at this stage will be identified as "events" (optional). -<2> `Flux#metrics` operator enables the reporting of metrics, using the name provided in when calling `Flux#name` operator. In case `Flux#name` operator has not been used, the default name will be `reactor`. +<1> Every metric at this stage of the reactive pipeline will use "events" as a naming prefix (optional, defaults to `reactor` prefix). +<2> We use the `tap` operator combined with a `SignalListener` implementation provided in `reactor-core-micrometer` for metrics collection. +<3> As with other APIs in that module, the `MeterRegistry` into which to publish metrics needs to be explicitly provided. ==== -Just adding these two operators will expose a whole bunch of useful metrics! - -[width="100%",options="header"] -|======= -| metric name | type | description - -| [name].subscribed | Counter | Counts how many Reactor sequences have been subscribed to - -| [name].malformed.source | Counter | Counts the number of events received from a malformed source (ie an onNext after an onComplete) +The detail of the exposed metrics is available in <>. -| [name].requested | DistributionSummary | Counts the amount requested to a named Flux by all subscribers, until at least one requests an unbounded amount - -| [name].onNext.delay | Timer | Measures delays between onNext signals (or between onSubscribe and first onNext) - -| [name].flow.duration | Timer | Times the duration elapsed between a subscription and the termination or cancellation of the sequence. A status tag is added to specify what event caused the timer to end (`completed`, `completedEmpty`, `error`, `cancelled`). -|======= - -Want to know how many times your event processing has restarted due to some error? Read `[name].subscribed`, because `retry()` operator will re-subscribe to the source publisher on error. - -Interested in "events per second" metric? Measure the rate of `[name].onNext.delay` 's count. - -Want to be alerted when the listener throws an error? `[name].flow.duration` with `status=error` tag is your friend. -Similarly, `status=completed` and `status=completedEmpty` will allow you to distinguish sequences that completed with elements from sequences that completed empty. - -Please note that when giving a name to a sequence, this sequence could not be aggregated with others anymore. As a compromise if you want to identify your sequence but still make it possible to aggregate with other views, you can use a <> for the name by calling `(tag("flow", "events"))` for example. +//TODO update and reintroduce tips for using the metrics +//Want to know how many times your event processing has restarted due to some error? Read `[name].subscribed`, because `retry()` operator will re-subscribe to the source publisher on error. +// +//Interested in "events per second" metric? Measure the rate of `[name].onNext.delay` 's count. +// +//Want to be alerted when the listener throws an error? `[name].flow.duration` with `status=error` tag is your friend. +//Similarly, `status=completed` and `status=completedEmpty` will allow you to distinguish sequences that completed with elements from sequences that completed empty. +// +//Please note that when giving a name to a sequence, this sequence could not be aggregated with others anymore. As a compromise if you want to identify your sequence but still make it possible to aggregate with other views, you can use a <> for the name by calling `(tag("flow", "events"))` for example. === Tags -Every metric will have a `type` tag in common, which value will be either `Flux` or `Mono` depending on the publisher's nature. - -Users are allowed to add custom tags to their reactive chains: +In addition to the common tags described in <>, users can add custom tags to their reactive chains via the `tag` operator: ==== [source,java] ---- listenToEvents() - .name("events") <1> - .tag("source", "kafka") <2> - .metrics() <3> + .name("events") // <1> + .tag("source", "kafka") // <2> + .tap(Micrometer.metrics(applicationDefinedRegistry)) // <3> .doOnNext(event -> log.info("Received {}", event)) .delayUntil(this::processEvent) .retry() .subscribe(); ---- -<1> Every metric at this stage will be identified as "events". +<1> Every metric at this stage will be identified with the "events" prefix. <2> Set a custom tag "source" to value "kafka". -<3> All reported metrics will have `source=kafka` tag assigned in addition to the common tag described above. +<3> All reported metrics will have `source=kafka` tag assigned in addition to the common tags. ==== Please note that depending on the monitoring system you're using, using a name can be considered mandatory when using tags, since it would otherwise result in a different set of tags between two default-named sequences. Some systems like Prometheus might also require to have the exact same set of tags for each metric with the same name. + +=== Observation +In addition to full metrics, the `reactor-core-micrometer` module offers an alternative based on Micrometer's `Observation`. +Depending on the configuration and runtime classpath, an `Observation` could translate to timers, spans, logging statements or any combination. + +A reactive chain can be observed via the `tap` operator and `Micrometer.observation` utility, as follows: +==== +[source,java] +---- +listenToEvents() + .name("events") // <1> + .tap(Micrometer.observation( // <2> + applicationDefinedRegistry)) // <3> + .doOnNext(event -> log.info("Received {}", event)) + .delayUntil(this::processEvent) + .retry() + .subscribe(); +---- +<1> The `Observation` for this pipeline will be identified with the "events" prefix. +<2> We use the `tap` operator with the `observation` utility. +<3> A registry must be provided into which to publish the observation results. Note this is an `ObservationRegistry`. +==== + +The detail of the observation and its tags is provided in <>. \ No newline at end of file From 72b78d6ed054a83dc5308fe676f99be31cc8b865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 11 Oct 2022 14:39:39 +0200 Subject: [PATCH 066/312] [release] Prepare and release 3.5.0-RC1 --- README.md | 6 +++--- gradle.properties | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 22c1889c11..937efdb290 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.0-M6" - testCompile "io.projectreactor:reactor-test:3.5.0-M6" + compile "io.projectreactor:reactor-core:3.5.0-RC1" + testCompile "io.projectreactor:reactor-test:3.5.0-RC1" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.5.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.5.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.0-M6" + // implementation "io.projectreactor:reactor-tools:3.5.0-RC1" } ``` diff --git a/gradle.properties b/gradle.properties index 229ef99c70..e6faa15cff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-SNAPSHOT -bomVersion=2022.0.0-M6 -metricsMicrometerVersion=1.0.0-SNAPSHOT +version=3.5.0-RC1 +bomVersion=2022.0.0-RC1 +metricsMicrometerVersion=1.0.0-RC1 From 4dbf793afd15151288bce7a754d2fee7c98c2006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 11 Oct 2022 15:33:34 +0200 Subject: [PATCH 067/312] [release] Next development version 3.5.0-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e6faa15cff..bbbbcb86bd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-RC1 +version=3.5.0-SNAPSHOT bomVersion=2022.0.0-RC1 -metricsMicrometerVersion=1.0.0-RC1 +metricsMicrometerVersion=1.0.0-SNAPSHOT From 593c68a7adfe0cb790e00ce35da12413ed892b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 21 Oct 2022 17:08:15 +0200 Subject: [PATCH 068/312] Scheduler.isDisposed() only true for disposed instances (#3243) Some schedulers for optimization purposes started in a state which was considered as disposed (Scheduler#isDisposed()). This change affects only BoundedElasticScheduler as it was the last one which behaved this way. This is a change of behavior, however one which was not documented. --- .../java/reactor/core/scheduler/BoundedElasticScheduler.java | 4 +++- .../java/reactor/core/scheduler/AbstractSchedulerTest.java | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticScheduler.java b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticScheduler.java index 78e08b9e21..09ffef0fb3 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticScheduler.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticScheduler.java @@ -144,7 +144,9 @@ BoundedScheduledExecutorService createBoundedExecutorService() { @Override public boolean isDisposed() { - return state.currentResource == SHUTDOWN; + // we only consider disposed as actually shutdown + SchedulerState current = this.state; + return current != INIT && current.currentResource == SHUTDOWN; } @Override diff --git a/reactor-core/src/test/java/reactor/core/scheduler/AbstractSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/AbstractSchedulerTest.java index 84bed0686c..de5a8f959b 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/AbstractSchedulerTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/AbstractSchedulerTest.java @@ -110,7 +110,6 @@ protected boolean isTerminated(Scheduler s) { } @Test - @Disabled("Should be enabled in 3.5.0") void nonInitializedIsNotDisposed() { Scheduler s = freshScheduler(); assertThat(s.isDisposed()).isFalse(); From 6fabb8c04d1e5f23f961c6e112ad4c18d3f8edbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 21 Oct 2022 17:56:49 +0200 Subject: [PATCH 069/312] Handle/Tap implicit threadlocal restoration if ContextCapture (#3180) This commit adds implicit behavior to `handle` and `tap` operators when used in conjunction with `contextCapture()` down the chain. By way of a marker key added by `contextCapture`, both operators detect context-propagation snapshot capture was performed and they use context-propagation snapshot restoration of thread locals around their respective user-provided code: - `BiConsumer#accept` for `handle` - each method of the `SignalListener` interface for `tap` Relates to #3149. --- .../asciidoc/advanced-contextPropagation.adoc | 33 +- .../core/publisher/ContextPropagation.java | 202 ++++++++- .../java/reactor/core/publisher/Flux.java | 25 +- .../reactor/core/publisher/FluxHandle.java | 6 +- .../core/publisher/FluxHandleFuseable.java | 6 +- .../java/reactor/core/publisher/FluxTap.java | 4 + .../core/publisher/FluxTapFuseable.java | 3 + .../java/reactor/core/publisher/Mono.java | 23 +- .../reactor/core/publisher/MonoHandle.java | 5 +- .../core/publisher/MonoHandleFuseable.java | 5 +- .../java/reactor/core/publisher/MonoTap.java | 3 + .../core/publisher/MonoTapFuseable.java | 3 + .../publisher/ContextPropagationTest.java | 429 +++++++++++++++++- 13 files changed, 713 insertions(+), 34 deletions(-) diff --git a/docs/asciidoc/advanced-contextPropagation.adoc b/docs/asciidoc/advanced-contextPropagation.adoc index 0c96e05175..d20377fbae 100644 --- a/docs/asciidoc/advanced-contextPropagation.adoc +++ b/docs/asciidoc/advanced-contextPropagation.adoc @@ -9,7 +9,8 @@ This library is intended as a means to easily adapt between various implementati It implements the SPI and is loaded via `java.util.ServiceLoader`. No user action is required, other than having a dependency on both reactor-core and `io.micrometer:context-propagation`. The `ReactorContextAccessor` class is public but shouldn't generally be accessed by user code. -On top of that, Reactor-Core 3.5.0 also introduces the `contextCapture` operator that transparently deals with `ContextSnapshot`s if the library is available at runtime, for users' convenience. +On top of that, Reactor-Core 3.5.0 also modifies the behavior of a couple key operators as well as introduces the `contextCapture` operator +to transparently deal with `ContextSnapshot`s if the library is available at runtime. == `contextCapture` Operator This operator can be used when one needs to capture `ThreadLocal` value(s) at subscription time and reflect these values in the Reactor `Context` for the benefit of upstream operators. @@ -37,4 +38,34 @@ Mono.deferContextual(ctx -> .contextCapture() .block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" ---- +==== + +== Operators that transparently restore a snapshot: `handle` and `tap` +When using `contextCapture()` a marker is added to the Reactor `Context` in which the snapshot has been captured. +This is detected by `Flux` and `Mono` variants of `handle` and `tap`, which restore `ThreadLocal`s from that snapshot transparently. + +These operators will ensure restoration is performed around the user-provided code, respectively: + - `handle` will wrap the `BiConsumer` in one which restores `ThreadLocal`s + - `tap` variants will wrap the `SignalListener` into one that has the same kind of wrapping around each method (this includes the `addToContext` method) + +The intent is to have a minimalistic set of operators transparently perform restoration. +As a result we chose operators with rather general and broad applications (one with transformative capabilities, one with side-effect capabilities) + +==== +[source,java] +---- +//assuming TL is known to Context-Propagation. +static final ThreadLocal TL = new ThreadLocal<>(); + +//in the main thread, TL is set to "HELLO" +TL.set("HELLO"); + +Mono.delay(Duration.ofSeconds(1)) + //we're now in another thread, TL is not set yet + .doOnNext(v -> System.out.println(TL.get())) + //inside the handler however, TL _is_ restored + .handle((v, sink) -> sink.next("handled delayed TL=" + TL.get())) + .contextCapture() + .block(); // prints "null" and returns "handled delayed TL=HELLO" +---- ==== \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index be6a5b2ba1..a24319f603 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -16,14 +16,18 @@ package reactor.core.publisher; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import io.micrometer.context.ContextRegistry; import io.micrometer.context.ContextSnapshot; +import reactor.core.CoreSubscriber; +import reactor.core.observability.SignalListener; import reactor.util.annotation.Nullable; import reactor.util.context.Context; +import reactor.util.context.ContextView; /** * Utility private class to detect if the context-propagation library is on the classpath and to offer @@ -35,6 +39,8 @@ final class ContextPropagation { static final boolean isContextPropagationAvailable; + static final String CAPTURED_CONTEXT_MARKER = "reactor.core.contextSnapshotCaptured"; + static final Predicate PREDICATE_TRUE = v -> true; static final Function NO_OP = c -> c; static final Function WITH_GLOBAL_REGISTRY_NO_PREDICATE; @@ -70,10 +76,18 @@ static boolean isContextPropagationAvailable() { * Create a support function that takes a snapshot of thread locals and merges them with the * provided {@link Context}, resulting in a new {@link Context} which includes entries * captured from threadLocals by the Context-Propagation API. + *

    + * Additionally, a marker key is added to the returned {@link Context} which can + * be detected upstream by a small subset of operators in order to restore thread locals + * from the context transparently. + *

    + * This variant uses the implicit global {@code ContextRegistry} and captures from all + * available {@code ThreadLocalAccessors}. It is the same variant backing {@link Flux#contextCapture()} + * and {@link Mono#contextCapture()}. * * @return the {@link Context} augmented with captured entries */ - public static Function contextCapture() { + static Function contextCapture() { if (!isContextPropagationAvailable) { return NO_OP; } @@ -88,18 +102,40 @@ public static Function contextCapture() { * The provided {@link Predicate} is used on keys associated to said thread locals * by the Context-Propagation API to filter which entries should be captured in the * first place. + *

    + * Additionally, a marker key is added to the returned {@link Context} which can + * be detected upstream by a small subset of operators in order to restore thread locals + * from the context transparently. + *

    + * This variant uses the implicit global {@code ContextRegistry} and captures only from + * available {@code ThreadLocalAccessors} that match the {@link Predicate}. * * @param captureKeyPredicate a {@link Predicate} used on keys to determine if each entry * should be injected into the new {@link Context} * @return a {@link Function} augmenting {@link Context} with captured entries */ - public static Function contextCapture(Predicate captureKeyPredicate) { + static Function contextCapture(Predicate captureKeyPredicate) { if (!isContextPropagationAvailable) { return NO_OP; } return new ContextCaptureFunction(captureKeyPredicate, null); } + static BiConsumer> contextRestoreForHandle(BiConsumer> handler, CoreSubscriber actual) { + if (!ContextPropagation.isContextPropagationAvailable() || !actual.currentContext().hasKey(ContextPropagation.CAPTURED_CONTEXT_MARKER)) { + return handler; + } + return new ContextRestoreHandleConsumer<>(handler, ContextRegistry.getInstance(), actual.currentContext()); + } + + static SignalListener contextRestoringSignalListener(final SignalListener original, + CoreSubscriber actual) { + if (!ContextPropagation.isContextPropagationAvailable() || !actual.currentContext().hasKey(ContextPropagation.CAPTURED_CONTEXT_MARKER)) { + return original; + } + return new ContextRestoreSignalListener(original, actual.currentContext(), ContextRegistry.getInstance()); + } + //the Function indirection allows tests to directly assert code in this class rather than static methods static final class ContextCaptureFunction implements Function { @@ -113,8 +149,168 @@ static final class ContextCaptureFunction implements Function @Override public Context apply(Context target) { - return ContextSnapshot.captureAllUsing(capturePredicate, this.registry).updateContext(target); + return ContextSnapshot.captureAllUsing(capturePredicate, this.registry) + .updateContext(target) + .put(CAPTURED_CONTEXT_MARKER, ""); } } + //the SignalListener implementation can be tested independently with a test-specific ContextRegistry + static final class ContextRestoreSignalListener implements SignalListener { + + final SignalListener original; + final ContextView context; + final ContextRegistry registry; + + public ContextRestoreSignalListener(SignalListener original, ContextView context, @Nullable ContextRegistry registry) { + this.original = original; + this.context = context; + this.registry = registry == null ? ContextRegistry.getInstance() : registry; + } + + ContextSnapshot.Scope restoreThreadLocals() { + //TODO for now ContextSnapshot static methods don't allow restoring _all_ TLs without an intermediate ContextSnapshot + return ContextSnapshot + .captureFrom(this.context, k -> true, this.registry) + .setThreadLocals(); + } + + @Override + public void doFirst() throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doFirst(); + } + } + + @Override + public void doFinally(SignalType terminationType) throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doFinally(terminationType); + } + } + + @Override + public void doOnSubscription() throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnSubscription(); + } + } + + @Override + public void doOnFusion(int negotiatedFusion) throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnFusion(negotiatedFusion); + } + } + + @Override + public void doOnRequest(long requested) throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnRequest(requested); + } + } + + @Override + public void doOnCancel() throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnCancel(); + } + } + + @Override + public void doOnNext(T value) throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnNext(value); + } + } + + @Override + public void doOnComplete() throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnComplete(); + } + } + + @Override + public void doOnError(Throwable error) throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnError(error); + } + } + + @Override + public void doAfterComplete() throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doAfterComplete(); + } + } + + @Override + public void doAfterError(Throwable error) throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doAfterError(error); + } + } + + @Override + public void doOnMalformedOnNext(T value) throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnMalformedOnNext(value); + } + } + + @Override + public void doOnMalformedOnError(Throwable error) throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnMalformedOnError(error); + } + } + + @Override + public void doOnMalformedOnComplete() throws Throwable { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.doOnMalformedOnComplete(); + } + } + + @Override + public void handleListenerError(Throwable listenerError) { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + original.handleListenerError(listenerError); + } + } + + @Override + public Context addToContext(Context originalContext) { + try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { + return original.addToContext(originalContext); + } + } + } + + //the BiConsumer implementation can be tested independently with a test-specific ContextRegistry + static final class ContextRestoreHandleConsumer implements BiConsumer> { + + private final BiConsumer> originalHandler; + private final ContextRegistry registry; + private final ContextView reactorContext; + + ContextRestoreHandleConsumer(BiConsumer> originalHandler, ContextRegistry registry, + ContextView reactorContext) { + this.originalHandler = originalHandler; + this.registry = registry; + this.reactorContext = reactorContext; + } + + @Override + public void accept(T t, SynchronousSink sink) { + //TODO for now ContextSnapshot static methods don't allow restoring _all_ TLs without an intermediate ContextSnapshot + final ContextSnapshot snapshot = ContextSnapshot.captureFrom(this.reactorContext, k -> true, this.registry); + try (ContextSnapshot.Scope ignored = snapshot.setThreadLocals(k -> { + return true; + })) { + originalHandler.accept(t, sink); + } + } + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 24dede4e78..7a6e940a8d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -4032,16 +4032,21 @@ public final Flux concatWith(Publisher other) { /** * If context-propagation library * is on the classpath, this is a convenience shortcut to capture thread local values during the - * subscription phase and put them in the {@link Context} that is visible upstream of this operator. + * subscription phase and put them in the {@link Context} that is visible upstream of this operator, + * alongside a marker key indicating that context capture occurred. *

    * As a result this operator should generally be used as close as possible to the end of * the chain / subscription point. + * If the marker key is encountered upstream, a small subset of operators will automatically restore the + * context snapshot ({@link #handle(BiConsumer) handle}, {@link #tap(SignalListenerFactory) tap}). *

    * If context-propagation is not available at runtime, this operator simply returns the current {@link Flux} * instance. * * @return a new {@link Flux} where context-propagation API has been used to capture entries and * inject them into the {@link Context} + * @see #handle(BiConsumer) + * @see #tap(SignalListenerFactory) */ public final Flux contextCapture() { if (!ContextPropagation.isContextPropagationAvailable()) { @@ -5795,11 +5800,13 @@ public final Flux groupJoin( *

    Error Mode Support: This operator supports {@link #onErrorContinue(BiConsumer) resuming on errors} (including when * fusion is enabled) when the {@link BiConsumer} throws an exception or if an error is signaled explicitly via * {@link SynchronousSink#error(Throwable)}. + *

    + * When used in conjunction with {@link #contextCapture()} down the chain, thread locals + * are restored from the Reactor {@link ContextView} within the handler {@link BiConsumer} using the + * context-propagation library. * * @param handler the handling {@link BiConsumer} - * * @param the transformed type - * * @return a transformed {@link Flux} */ public final Flux handle(BiConsumer> handler) { @@ -9031,6 +9038,10 @@ public final Flux takeWhile(Predicate continuePredicate) { *

    * This simplified variant assumes the state is purely initialized within the {@link Supplier}, * as it is called for each incoming {@link Subscriber} without additional context. + *

    + * When used in conjunction with {@link #contextCapture()} down the chain, thread locals + * are restored from the downstream {@link ContextView} around all invocations of {@link SignalListener} methods + * using the context-propagation library. * * @param simpleListenerGenerator the {@link Supplier} to create a new {@link SignalListener} on each subscription * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} @@ -9063,6 +9074,10 @@ public SignalListener createListener(Publisher ignored1, Context *

    * This simplified variant allows the {@link SignalListener} to be constructed for each subscription * with access to the incoming {@link Subscriber}'s {@link ContextView}. + *

    + * When used in conjunction with {@link #contextCapture()} down the chain, thread locals + * are restored from the same {@link ContextView} around all invocations of {@link SignalListener} methods + * using the context-propagation library. * * @param listenerGenerator the {@link Function} to create a new {@link SignalListener} on each subscription * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} @@ -9096,6 +9111,10 @@ public SignalListener createListener(Publisher ignored1, Context * exception. Note that {@link SignalListener#doFinally(SignalType)}, {@link SignalListener#doAfterComplete()} and * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} * the exception. + *

    + * When used in conjunction with {@link #contextCapture()} down the chain, thread locals + * are restored from the downstream {@link ContextView} around all invocations of {@link SignalListener} methods + * using the context-propagation library. * * @param listenerFactory the {@link SignalListenerFactory} to create a new {@link SignalListener} on each subscription * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java b/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java index 5a5ef4d607..9890d394a0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java @@ -19,6 +19,7 @@ import java.util.Objects; import java.util.function.BiConsumer; +import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; @@ -44,12 +45,13 @@ final class FluxHandle extends InternalFluxOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { + BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual); if (actual instanceof Fuseable.ConditionalSubscriber) { @SuppressWarnings("unchecked") Fuseable.ConditionalSubscriber cs = (Fuseable.ConditionalSubscriber) actual; - return new HandleConditionalSubscriber<>(cs, handler); + return new HandleConditionalSubscriber<>(cs, handler2); } - return new HandleSubscriber<>(actual, handler); + return new HandleSubscriber<>(actual, handler2); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java index 93e884ee87..08bd061d25 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java @@ -19,6 +19,7 @@ import java.util.Objects; import java.util.function.BiConsumer; +import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; @@ -57,12 +58,13 @@ final class FluxHandleFuseable extends InternalFluxOperator implemen @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { + BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual); if (actual instanceof ConditionalSubscriber) { @SuppressWarnings("unchecked") ConditionalSubscriber cs = (ConditionalSubscriber) actual; - return new HandleFuseableConditionalSubscriber<>(cs, handler); + return new HandleFuseableConditionalSubscriber<>(cs, handler2); } - return new HandleFuseableSubscriber<>(actual, handler); + return new HandleFuseableSubscriber<>(actual, handler2); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java index 2cd1715eb3..b6811980af 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java @@ -16,6 +16,7 @@ package reactor.core.publisher; +import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -56,6 +57,9 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act Operators.error(actual, generatorError); return null; } + // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods + // (only if ContextPropagation.isContextPropagationAvailable() is true) + signalListener = ContextPropagation.contextRestoringSignalListener(signalListener, actual); try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java index 7cca5abda8..d0e3411c1c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java @@ -56,6 +56,9 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act Operators.error(actual, generatorError); return null; } + // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods + // (only if ContextPropagation.isContextPropagationAvailable() is true) + signalListener = ContextPropagation.contextRestoringSignalListener(signalListener, actual); try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 4133a0489a..d566e0002c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -2230,16 +2230,21 @@ public final Flux concatWith(Publisher other) { /** * If context-propagation library * is on the classpath, this is a convenience shortcut to capture thread local values during the - * subscription phase and put them in the {@link Context} that is visible upstream of this operator. + * subscription phase and put them in the {@link Context} that is visible upstream of this operator, + * alongside a marker key indicating that context capture occurred. *

    * As a result this operator should generally be used as close as possible to the end of * the chain / subscription point. + * If the marker key is encountered upstream, a small subset of operators will automatically restore the + * context snapshot ({@link #handle(BiConsumer) handle}, {@link #tap(SignalListenerFactory) tap}). *

    * If context-propagation is not available at runtime, this operator simply returns the current {@link Mono} * instance. * * @return a new {@link Flux} where context-propagation API has been used to capture entries and * inject them into the {@link Context} + * @see #handle(BiConsumer) + * @see #tap(SignalListenerFactory) */ public final Mono contextCapture() { if (!ContextPropagation.isContextPropagationAvailable()) { @@ -3141,6 +3146,10 @@ public final Mono hasElement() { * output sink for each onNext. At most one {@link SynchronousSink#next(Object)} * call must be performed and/or 0 or 1 {@link SynchronousSink#error(Throwable)} or * {@link SynchronousSink#complete()}. + *

    + * When used in conjunction with {@link #contextCapture()} down the chain, thread locals + * are restored from the Reactor {@link ContextView} within the handler {@link BiConsumer} using the + * context-propagation library. * * @param handler the handling {@link BiConsumer} * @param the transformed type @@ -4568,6 +4577,10 @@ public final Mono takeUntilOther(Publisher other) { *

    * This simplified variant assumes the state is purely initialized within the {@link Supplier}, * as it is called for each incoming {@link Subscriber} without additional context. + *

    + * When used in conjunction with {@link #contextCapture()} down the chain, thread locals + * are restored from the downstream {@link ContextView} around all invocations of {@link SignalListener} methods + * using the context-propagation library. * * @param simpleListenerGenerator the {@link Supplier} to create a new {@link SignalListener} on each subscription * @return a new {@link Mono} with side effects defined by generated {@link SignalListener} @@ -4600,6 +4613,10 @@ public SignalListener createListener(Publisher ignored1, Context *

    * This simplified variant allows the {@link SignalListener} to be constructed for each subscription * with access to the incoming {@link Subscriber}'s {@link ContextView}. + *

    + * When used in conjunction with {@link #contextCapture()} down the chain, thread locals + * are restored from the same {@link ContextView} around all invocations of {@link SignalListener} methods + * using the context-propagation library. * * @param listenerGenerator the {@link Function} to create a new {@link SignalListener} on each subscription * @return a new {@link Mono} with side effects defined by generated {@link SignalListener} @@ -4633,6 +4650,10 @@ public SignalListener createListener(Publisher ignored1, Context * exception. Note that {@link SignalListener#doFinally(SignalType)}, {@link SignalListener#doAfterComplete()} and * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} * the exception. + *

    + * When used in conjunction with {@link #contextCapture()} down the chain, thread locals + * are restored from the downstream {@link ContextView} around all invocations of {@link SignalListener} methods + * using the context-propagation library. * * @param listenerFactory the {@link SignalListenerFactory} to create a new {@link SignalListener} on each subscription * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java b/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java index 0aea009b23..b6a33c6236 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,8 @@ final class MonoHandle extends InternalMonoOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - return new FluxHandle.HandleSubscriber<>(actual, handler); + BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual); + return new FluxHandle.HandleSubscriber<>(actual, handler2); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java index 344cc4cba7..05c77e4a30 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,8 @@ final class MonoHandleFuseable extends InternalMonoOperator @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - return new FluxHandleFuseable.HandleFuseableSubscriber<>(actual, handler); + BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual); + return new FluxHandleFuseable.HandleFuseableSubscriber<>(actual, handler2); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java index 6b4bb6f8ea..5931eb3904 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java @@ -53,6 +53,9 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act Operators.error(actual, generatorError); return null; } + // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods + // (only if ContextPropagation.isContextPropagationAvailable() is true) + signalListener = ContextPropagation.contextRestoringSignalListener(signalListener, actual); try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java index 08617f2777..209b7a8856 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java @@ -52,6 +52,9 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act Operators.error(actual, generatorError); return null; } + // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods + // (only if ContextPropagation.isContextPropagationAvailable() is true) + signalListener = ContextPropagation.contextRestoringSignalListener(signalListener, actual); try { signalListener.doFirst(); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index 0263d842b7..7efbb7b5e1 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -16,17 +16,38 @@ package reactor.core.publisher; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import java.util.function.Function; import io.micrometer.context.ContextRegistry; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; +import org.reactivestreams.Publisher; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; +import reactor.core.publisher.FluxHandle.HandleConditionalSubscriber; +import reactor.core.publisher.FluxHandle.HandleSubscriber; +import reactor.core.publisher.FluxHandleFuseable.HandleFuseableConditionalSubscriber; +import reactor.core.publisher.FluxHandleFuseable.HandleFuseableSubscriber; +import reactor.test.ParameterizedTestWithName; +import reactor.test.subscriber.TestSubscriber; +import reactor.test.subscriber.TestSubscriberBuilder; import reactor.util.context.Context; +import reactor.util.context.ContextView; import static org.assertj.core.api.Assertions.assertThat; @@ -35,27 +56,29 @@ */ class ContextPropagationTest { - private static final String KEY1 = "key1"; - private static final String KEY2 = "key2"; + private static final String KEY1 = "ContextPropagationTest.key1"; + private static final String KEY2 = "ContextPropagationTest.key2"; - private static final AtomicReference REF1 = new AtomicReference<>(); - private static final AtomicReference REF2 = new AtomicReference<>(); + private static final ThreadLocal REF1 = ThreadLocal.withInitial(() -> "ref1_init"); + private static final ThreadLocal REF2 = ThreadLocal.withInitial(() -> "ref2_init"); //NOTE: no way to currently remove accessors from the ContextRegistry, so we recreate one on each test private ContextRegistry registry; @BeforeEach - void setup() { + void initializeThreadLocals() { registry = new ContextRegistry().loadContextAccessors(); - REF1.set("ref1_init"); - REF2.set("ref2_init"); - - registry.registerThreadLocalAccessor( - KEY1, REF1::get, REF1::set, () -> REF1.set(null)); + registry.registerThreadLocalAccessor(KEY1, REF1); + registry.registerThreadLocalAccessor(KEY2, REF2); + } - registry.registerThreadLocalAccessor( - KEY2, REF2::get, REF2::set, () -> REF2.set(null)); + //the cleanup of "thread locals" could be especially important if one starts relying on + //the global registry in tests: it would ensure no TL pollution. + @AfterEach + void cleanupThreadLocals() { + REF1.remove(); + REF2.remove(); } @Test @@ -117,14 +140,18 @@ void contextCaptureFunctionWithoutFiltering() { ContextPropagation.ContextCaptureFunction test = new ContextPropagation.ContextCaptureFunction( ContextPropagation.PREDICATE_TRUE, registry); + REF1.set("expected1"); + REF2.set("expected2"); + Context ctx = test.apply(Context.empty()); Map asMap = new HashMap<>(); ctx.forEach(asMap::put); //easier to assert assertThat(asMap) - .containsEntry(KEY1, "ref1_init") - .containsEntry(KEY2, "ref2_init") - .hasSize(2); + .containsEntry(KEY1, "expected1") + .containsEntry(KEY2, "expected2") + .containsEntry(ContextPropagation.CAPTURED_CONTEXT_MARKER, "") + .hasSize(3); } @Test @@ -132,13 +159,17 @@ void captureWithFiltering() { ContextPropagation.ContextCaptureFunction test = new ContextPropagation.ContextCaptureFunction( k -> k.toString().equals(KEY2), registry); + REF1.set("not_expected"); + REF2.set("expected"); + Context ctx = test.apply(Context.empty()); Map asMap = new HashMap<>(); ctx.forEach(asMap::put); //easier to assert assertThat(asMap) - .containsEntry(KEY2, "ref2_init") - .hasSize(1); + .containsEntry(KEY2, "expected") + .containsEntry(ContextPropagation.CAPTURED_CONTEXT_MARKER, "") + .hasSize(2); } @Test @@ -149,4 +180,366 @@ void captureFunctionWithNullRegistryUsesGlobalRegistry() { } } + static private enum Cases { + NORMAL_NO_MARKER(false, false, false), + NORMAL_WITH_MARKER(false, false, true), + CONDITIONAL_NO_MARKER(false, true, false), + CONDITIONAL_WITH_MARKER(false, true, true), + FUSED_NO_MARKER(true, false, false), + FUSED_WITH_MARKER(true, false, true), + FUSED_CONDITIONAL_NO_MARKER(true, true, false), + FUSED_CONDITIONAL_WITH_MARKER(true, true, true); + + final boolean fusion; + final boolean conditional; + final boolean marker; + + Cases(boolean fusion, boolean conditional, boolean marker) { + this.fusion = fusion; + this.conditional = conditional; + this.marker = marker; + } + } + + @Nested + class ContextRestoreForTap { + + @EnumSource(Cases.class) + @ParameterizedTestWithName + void properWrappingForFluxTap(Cases characteristics) { + SignalListener originalListener = Mockito.mock(SignalListener.class); + SignalListenerFactory originalFactory = new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + return originalListener; + } + }; + + Publisher tap; + TestSubscriberBuilder builder = TestSubscriber.builder(); + if (characteristics.fusion) { + tap = new FluxTapFuseable<>(Flux.empty(), originalFactory); + builder = builder.requireFusion(Fuseable.ANY); + } + else { + tap = new FluxTap<>(Flux.empty(), originalFactory); + builder = builder.requireNotFuseable(); + } + + if (characteristics.marker) { + builder = builder.contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true); + } + + TestSubscriber testSubscriber; + if (characteristics.conditional) { + testSubscriber = builder.buildConditional(v -> true); + } + else { + testSubscriber = builder.build(); + } + + tap.subscribe(testSubscriber); + Scannable parent = testSubscriber.parents().findFirst().get(); + + if (!characteristics.fusion) { + assertThat(parent).isInstanceOfSatisfying(FluxTap.TapSubscriber.class, + tapSubscriber -> { + if (characteristics.marker) { + assertThat(tapSubscriber.listener).as("listener wrapped") + .isNotSameAs(originalListener) + .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + } + else { + assertThat(tapSubscriber.listener) + .as("listener not wrapped") + .isSameAs(originalListener); + } + }); + } + else { + assertThat(parent).isInstanceOfSatisfying(FluxTapFuseable.TapFuseableSubscriber.class, + tapSubscriber -> { + if (characteristics.marker) { + assertThat(tapSubscriber.listener) + .as("listener wrapped") + .isNotSameAs(originalListener) + .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + } + else { + assertThat(tapSubscriber.listener) + .as("listener not wrapped") + .isSameAs(originalListener); + } + }); + } + } + + @EnumSource(Cases.class) + @ParameterizedTestWithName + void properWrappingForMonoTap(Cases characteristics) { + SignalListener originalListener = Mockito.mock(SignalListener.class); + SignalListenerFactory originalFactory = new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + return originalListener; + } + }; + + Mono tap; + TestSubscriberBuilder builder = TestSubscriber.builder(); + if (characteristics.fusion) { + tap = new MonoTapFuseable<>(Mono.empty(), originalFactory); + builder = builder.requireFusion(Fuseable.ANY); + } + else { + tap = new MonoTap<>(Mono.empty(), originalFactory); + builder = builder.requireNotFuseable(); + } + + if (characteristics.marker) { + builder = builder.contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true); + } + + TestSubscriber testSubscriber; + if (characteristics.conditional) { + testSubscriber = builder.buildConditional(v -> true); + } + else { + testSubscriber = builder.build(); + } + + tap.subscribe(testSubscriber); + Scannable parent = testSubscriber.parents().findFirst().get(); + + if (!characteristics.fusion) { + assertThat(parent).isInstanceOfSatisfying(FluxTap.TapSubscriber.class, + tapSubscriber -> { + if (characteristics.marker) { + assertThat(tapSubscriber.listener) + .as("listener wrapped") + .isNotSameAs(originalListener) + .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + + } + else { + assertThat(tapSubscriber.listener).as("listener not wrapped").isSameAs(originalListener); + } + }); + } + else { + assertThat(parent).isInstanceOfSatisfying(FluxTapFuseable.TapFuseableSubscriber.class, + tapSubscriber -> { + if (characteristics.marker) { + assertThat(tapSubscriber.listener) + .as("listener wrapped") + .isNotSameAs(originalListener) + .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + } + else { + assertThat(tapSubscriber.listener).as("listener not wrapped").isSameAs(originalListener); + } + }); + } + } + + @Test + void threadLocalRestoredInSignalListener() throws InterruptedException { + REF1.set(null); + Context context = Context.of(KEY1, "expected"); + List list = new ArrayList<>(); + + SignalListener tlReadingListener = Mockito.mock(SignalListener.class, invocation -> { + list.add(invocation.getMethod().getName() + ": " + REF1.get()); + return null; + }); + + ContextPropagation.ContextRestoreSignalListener listener = new ContextPropagation.ContextRestoreSignalListener<>(tlReadingListener, context, + registry); + + Thread t = new Thread(() -> { + try { + listener.doFirst(); + listener.doOnSubscription(); + + listener.doOnFusion(1); + listener.doOnRequest(1L); + listener.doOnCancel(); + + listener.doOnNext(1); + listener.doOnComplete(); + listener.doOnError(new IllegalStateException("boom")); + + listener.doAfterComplete(); + listener.doAfterError(new IllegalStateException("boom")); + listener.doFinally(SignalType.ON_COMPLETE); + + listener.doOnMalformedOnNext(1); + listener.doOnMalformedOnComplete(); + listener.doOnMalformedOnError(new IllegalStateException("boom")); + + listener.addToContext(Context.empty()); + listener.handleListenerError(new IllegalStateException("boom")); + } + catch (Throwable error) { + error.printStackTrace(); + } + }); + t.start(); + t.join(); + + assertThat(list).as("extracted TLs") + .containsExactly( + "doFirst: expected", + "doOnSubscription: expected", + "doOnFusion: expected", + "doOnRequest: expected", + "doOnCancel: expected", + "doOnNext: expected", + "doOnComplete: expected", + "doOnError: expected", + "doAfterComplete: expected", + "doAfterError: expected", + "doFinally: expected", + "doOnMalformedOnNext: expected", + "doOnMalformedOnComplete: expected", + "doOnMalformedOnError: expected", + "addToContext: expected", + "handleListenerError: expected" + ); + } + + } + + @Nested + class ContextRestoreForHandle { + + @ValueSource(booleans = {true, false}) + @ParameterizedTestWithName + void publicMethodChecksForMarkerBeforeWrapping(boolean withMarker) { + BiConsumer> originalHandler = (v, sink) -> { }; + final Context context; + if (withMarker) { + context = Context.of(KEY1, "expected", ContextPropagation.CAPTURED_CONTEXT_MARKER, true); + } + else { + context = Context.of(KEY1, "expected"); + } + CoreSubscriber mockSubscriber = Mockito.mock(CoreSubscriber.class); + Mockito.when(mockSubscriber.currentContext()).thenReturn(context); + + BiConsumer> decoratedHandler = ContextPropagation.contextRestoreForHandle(originalHandler, mockSubscriber); + + if (!withMarker) { + assertThat(decoratedHandler).as("no marker: same handler").isSameAs(originalHandler); + } + else { + assertThat(decoratedHandler).as("marker: decorated handler").isNotSameAs(originalHandler); + } + } + + @ValueSource(booleans = {true, false}) + @ParameterizedTestWithName + void classContextRestoreHandleConsumerRestoresWithOrWithoutMarker(boolean withMarker) { + BiConsumer> originalHandler = (v, sink) -> { + if (v.equals("bar")) { + sink.next(v + "=" + REF1.get()); + } + }; + + final String expected = "bar=expected"; + final Context context; + if (withMarker) { + context = Context.of(KEY1, "expected", ContextPropagation.CAPTURED_CONTEXT_MARKER, true); + } + else { + context = Context.of(KEY1, "expected"); + } + + BiConsumer> decoratedHandler = new ContextPropagation.ContextRestoreHandleConsumer<>(originalHandler, registry, context); + + SynchronousSink mockSink = Mockito.mock(SynchronousSink.class); + decoratedHandler.accept("bar", mockSink); + Mockito.verify(mockSink, Mockito.times(1)).next(expected); + } + + @SuppressWarnings("rawtypes") + @Test + void fluxHandleVariantsCallTheWrapper() { + BiConsumer> originalHandler = (v, sink) -> {}; + + FluxHandle publisher = new FluxHandle<>(Flux.empty(), originalHandler); + FluxHandleFuseable publisherFuseable = new FluxHandleFuseable<>(Flux.empty(), originalHandler); + + CoreSubscriber actual = TestSubscriber.builder().contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true).build(); + Fuseable.ConditionalSubscriber actualCondi = TestSubscriber.builder().contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true).buildConditional(v -> true); + + HandleSubscriber sub = (HandleSubscriber) publisher.subscribeOrReturn(actual); + HandleConditionalSubscriber subCondi = (HandleConditionalSubscriber) publisher.subscribeOrReturn(actualCondi); + HandleFuseableSubscriber subFused = (HandleFuseableSubscriber) publisherFuseable.subscribeOrReturn(actual); + HandleFuseableConditionalSubscriber subFusedCondi = (HandleFuseableConditionalSubscriber) publisherFuseable.subscribeOrReturn(actualCondi); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(publisher.handler).as("publisher.handler").isSameAs(originalHandler); + softly.assertThat(publisherFuseable.handler).as("publisherFuseable.handler").isSameAs(originalHandler); + + softly.assertThat(sub.handler) + .as("sub.handler") + .isNotSameAs(originalHandler) + .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + softly.assertThat(subCondi.handler) + .as("subCondi.handler") + .isNotSameAs(originalHandler) + .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + softly.assertThat(subFused.handler) + .as("subFused.handler") + .isNotSameAs(originalHandler) + .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + softly.assertThat(subFusedCondi.handler) + .as("subFusedCondi.handler") + .isNotSameAs(originalHandler) + .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + }); + } + + @SuppressWarnings("rawtypes") + @Test + void monoHandleVariantsCallTheWrapper() { + BiConsumer> originalHandler = (v, sink) -> {}; + + MonoHandle publisher = new MonoHandle<>(Mono.empty(), originalHandler); + MonoHandleFuseable publisherFuseable = new MonoHandleFuseable<>(Mono.empty(), originalHandler); + + CoreSubscriber actual = TestSubscriber.builder().contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true).build(); + + HandleSubscriber sub = (HandleSubscriber) publisher.subscribeOrReturn(actual); + HandleFuseableSubscriber subFused = (HandleFuseableSubscriber) publisherFuseable.subscribeOrReturn(actual); + //note: unlike FluxHandle, MonoHandle doesn't have support for ConditionalSubscriber + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(publisher.handler).as("publisher.handler").isSameAs(originalHandler); + softly.assertThat(publisherFuseable.handler).as("publisherFuseable.handler").isSameAs(originalHandler); + + softly.assertThat(sub.handler) + .as("sub.handler") + .isNotSameAs(originalHandler) + .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + softly.assertThat(subFused.handler) + .as("subFused.handler") + .isNotSameAs(originalHandler) + .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + }); + } + } } From 07062f83c20cdc40344f9db07553ce5768ad186e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 24 Oct 2022 16:29:04 +0200 Subject: [PATCH 070/312] Fix the way context-propagation is detected (#3246) This commit changes the way the context-propagation is detected, preventing an issue where the boolean would become true while actually using the registry would throw. Reactor-Netty CI would fail with a NoClassDefFoundError due to this. --- .../main/java/reactor/core/publisher/ContextPropagation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index a24319f603..39f2e6e3dc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -48,7 +48,7 @@ final class ContextPropagation { static { boolean contextPropagation; try { - Class.forName("io.micrometer.context.ContextRegistry", false, ContextPropagation.class.getClassLoader()); + Class.forName("io.micrometer.context.ContextRegistry"); contextPropagation = true; } catch (Throwable t) { From 227819a63ef77690fba17bcb3383705449aad159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 26 Oct 2022 10:48:35 +0200 Subject: [PATCH 071/312] Further improve detection of ContextPropagation (see #3246) (#3249) In #3246 the ContextPropagation initialization was altered to avoid a `NoClassDefFoundError` discovered in Reactor-Netty. While the fix does appear to prevent that exception, the true root cause was not correctly identified. It appears that the underlying issue is not the way classloading of `ContextRegistry` is performed, but rather the fact that it can load accessors via the Service Loader mecanism. Given that there is a singleton instance which does load accessors and that Service Loader implies file reading, the true root cause is... BlockHound! In the failing Reactor-Netty test, BlockHound was active. Note that now the first use of a `handle` operator references `ContextPropagation` and thus triggers lazy initialization of the class. It turns out that this first use was occurring in a non-blocking thread in the test, tripping BlockHound _inside the static initializer of ContextPropagation_. This commit replaces the `Class.forName` with an attempt to get a reference to `ContextRegistry.getInstance()` since the next step is to use it anyway, to initialize a `Function`. It modifies the logic to better distinguish between a rather expected `ClassNotFoundException` (context-propagation library is not there) and other exceptions like the BlockHound one (unexpected initialization errors), logging the later to ease future investigations. Finally, this commit alters the `ReactorBlockHoundIntegration` to allow blocking calls inside of `ContextPropagation`'s static initializer. Replaces fix in #3246. --- .../core/publisher/ContextPropagation.java | 26 ++++++++++++++----- .../ReactorBlockHoundIntegration.java | 5 ++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 39f2e6e3dc..521d88adbc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -25,6 +25,8 @@ import reactor.core.CoreSubscriber; import reactor.core.observability.SignalListener; +import reactor.util.Logger; +import reactor.util.Loggers; import reactor.util.annotation.Nullable; import reactor.util.context.Context; import reactor.util.context.ContextView; @@ -37,6 +39,8 @@ */ final class ContextPropagation { + static final Logger LOGGER; + static final boolean isContextPropagationAvailable; static final String CAPTURED_CONTEXT_MARKER = "reactor.core.contextSnapshotCaptured"; @@ -46,21 +50,29 @@ final class ContextPropagation { static final Function WITH_GLOBAL_REGISTRY_NO_PREDICATE; static { + LOGGER = Loggers.getLogger(ContextPropagation.class); + + Function contextCaptureFunction; boolean contextPropagation; try { - Class.forName("io.micrometer.context.ContextRegistry"); + ContextRegistry registry = ContextRegistry.getInstance(); + contextCaptureFunction = new ContextCaptureFunction(PREDICATE_TRUE, registry); contextPropagation = true; } + catch (LinkageError t) { + // Context-Propagation library is not available + contextCaptureFunction = NO_OP; + contextPropagation = false; + } catch (Throwable t) { + contextCaptureFunction = NO_OP; contextPropagation = false; + LOGGER.error("Unexpected exception while detecting ContextPropagation feature." + + " The feature is considered disabled due to this:", t); } + isContextPropagationAvailable = contextPropagation; - if (contextPropagation) { - WITH_GLOBAL_REGISTRY_NO_PREDICATE = new ContextCaptureFunction(PREDICATE_TRUE, ContextRegistry.getInstance()); - } - else { - WITH_GLOBAL_REGISTRY_NO_PREDICATE = NO_OP; - } + WITH_GLOBAL_REGISTRY_NO_PREDICATE = contextCaptureFunction; } /** diff --git a/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java b/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java index 7f068445e7..8d2987cb86 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java @@ -45,5 +45,10 @@ public void applyTo(BlockHound.Builder builder) { builder.allowBlockingCallsInside(WorkerTask.class.getName(), "dispose"); builder.allowBlockingCallsInside(ThreadPoolExecutor.class.getName(), "processWorkerExit"); + + // Most allowances are from the schedulers package but this one is from the publisher package. + // For now, let's not add a separate integration, but rather let's define the class name manually + // ContextRegistry reads files as part of the Service Loader aspect. If class is initialized in a non-blocking thread, BlockHound would complain + builder.allowBlockingCallsInside("reactor.core.publisher.ContextPropagation", ""); } } From 632559355a3443194818e82867a3fff534c11afa Mon Sep 17 00:00:00 2001 From: Guillaume Le Floch Date: Wed, 26 Oct 2022 15:15:00 +0200 Subject: [PATCH 072/312] Minor polish on documentation (#3247) This just fix a missing spaces and add a missing `s`. --- docs/asciidoc/advancedFeatures.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/asciidoc/advancedFeatures.adoc b/docs/asciidoc/advancedFeatures.adoc index 2e44f624cd..88743410ed 100644 --- a/docs/asciidoc/advancedFeatures.adoc +++ b/docs/asciidoc/advancedFeatures.adoc @@ -766,13 +766,13 @@ libraries that are responsible for the subscriptions. [[context.api]] === The `Context` API -`Context` is an interface reminiscent of `Map`.It stores key-value pairs and lets you +`Context` is an interface reminiscent of `Map`. It stores key-value pairs and lets you fetch a value you stored by its key. It has a simplified version that only exposes read methods, the `ContextView`. More specifically: * Both key and values are of type `Object`, so a `Context` (and `ContextView`) instance can contain any number of highly divergent values from different libraries and sources. -* A `Context` is immutable. It expose write methods like `put` and `putAll` but they produce a new instance. +* A `Context` is immutable. It exposes write methods like `put` and `putAll` but they produce a new instance. * For a read-only API that doesn't even expose such write methods, there's the `ContextView` superinterface since 3.4.0 * You can check whether the key is present with `hasKey(Object key)`. * Use `getOrDefault(Object key, T defaultValue)` to retrieve a value (cast to a `T`) or From 3248c43c9c417e62e0380c8b7eb8f6f5d1dad5ce Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Wed, 26 Oct 2022 15:20:52 +0200 Subject: [PATCH 073/312] Code polishing (#3234) This commit consist of several small code polishing, mostly in Flux: - vestigial SuppressWarnings - vestigial method type parameters (safe to remove according to JLS 13.4.13. Method and Constructor Type Parameters) - turning an URL from http to https --- README.md | 2 +- .../java/reactor/core/publisher/Flux.java | 24 +++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 937efdb290..b5e45ed680 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,6 @@ https://github.com/reactor/head-first-reactive-with-spring-and-reactor/ ------------------------------------- _Powered by [Reactive Streams Commons](https://github.com/reactor/reactive-streams-commons)_ -_Licensed under [Apache Software License 2.0](www.apache.org/licenses/LICENSE-2.0)_ +_Licensed under [Apache Software License 2.0](https://www.apache.org/licenses/LICENSE-2.0)_ _Sponsored by [VMware](https://tanzu.vmware.com/)_ diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 7a6e940a8d..3e59c82a6f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -396,7 +396,7 @@ public static Flux combineLatest(Iterable combinator) { - return onAssembly(new FluxCombineLatest(sources, + return onAssembly(new FluxCombineLatest<>(sources, combinator, Queues.get(prefetch), prefetch)); } @@ -2397,10 +2397,10 @@ public static Flux zip(Iterable> sources, int prefetch, final Function combinator) { - return onAssembly(new FluxZip(sources, - combinator, - Queues.get(prefetch), - prefetch)); + return onAssembly(new FluxZip<>(sources, + combinator, + Queues.get(prefetch), + prefetch)); } /** @@ -3050,7 +3050,7 @@ public final Flux> bufferUntil(Predicate predicate, boolean c * * @return a microbatched {@link Flux} of {@link List} */ - public final Flux> bufferUntilChanged() { + public final Flux> bufferUntilChanged() { return bufferUntilChanged(identityFunction()); } @@ -3718,7 +3718,6 @@ public final Mono> collectSortedList() { * * @return a {@link Mono} of a sorted {@link List} of all values from this {@link Flux} */ - @SuppressWarnings({ "unchecked", "rawtypes" }) public final Mono> collectSortedList(@Nullable Comparator comparator) { return collectList().doOnNext(list -> { // Note: this assumes the list emitted by buffer() is mutable @@ -4021,7 +4020,6 @@ public final Flux concatMapIterable(Function concatWith(Publisher other) { if (this instanceof FluxConcatArray) { - @SuppressWarnings({ "unchecked" }) FluxConcatArray fluxConcatArray = (FluxConcatArray) this; return fluxConcatArray.concatAdditionalSourceLast(other); @@ -5997,7 +5995,6 @@ public final Mono last(T defaultValue) { @SuppressWarnings("unchecked") Callable thiz = (Callable)this; if(thiz instanceof Fuseable.ScalarCallable){ - @SuppressWarnings("unchecked") Fuseable.ScalarCallable c = (Fuseable.ScalarCallable)thiz; T v; try { @@ -9201,7 +9198,6 @@ public final Mono thenEmpty(Publisher other) { */ public final Flux thenMany(Publisher other) { if (this instanceof FluxConcatArray) { - @SuppressWarnings({ "unchecked" }) FluxConcatArray fluxConcatArray = (FluxConcatArray) this; return fluxConcatArray.concatAdditionalIgnoredLast(other); } @@ -9391,7 +9387,7 @@ public final Flux timeout(Publisher firstTimeout, return timeout(firstTimeout, nextTimeoutFactory, "first signal from a Publisher"); } - private final Flux timeout(Publisher firstTimeout, + private Flux timeout(Publisher firstTimeout, Function> nextTimeoutFactory, String timeoutDescription) { return onAssembly(new FluxTimeout<>(this, firstTimeout, nextTimeoutFactory, timeoutDescription)); @@ -10224,7 +10220,7 @@ public final Flux> windowUntil(Predicate boundaryTrigger, boolean cut * * @return a microbatched {@link Flux} of {@link Flux} windows. */ - public final Flux> windowUntilChanged() { + public final Flux> windowUntilChanged() { return windowUntilChanged(identityFunction()); } @@ -10489,7 +10485,6 @@ public final Flux> zipWith(Publisher source2) { public final Flux zipWith(Publisher source2, final BiFunction combinator) { if (this instanceof FluxZip) { - @SuppressWarnings("unchecked") FluxZip o = (FluxZip) this; Flux result = o.zipAdditionalSource(source2, combinator); if (result != null) { @@ -10561,7 +10556,6 @@ public final Flux> zipWith(Publisher source2, i * @return a zipped {@link Flux} * */ - @SuppressWarnings("unchecked") public final Flux> zipWithIterable(Iterable iterable) { return zipWithIterable(iterable, tuple2Function()); } @@ -10663,7 +10657,6 @@ final Flux flatMapSequential(Function Flux doOnSignal(Flux source, @Nullable Consumer onSubscribe, @Nullable Consumer onNext, @@ -10866,6 +10859,7 @@ static Flux wrap(Publisher source) { @SuppressWarnings("rawtypes") static final Supplier SET_SUPPLIER = HashSet::new; static final BooleanSupplier ALWAYS_BOOLEAN_SUPPLIER = () -> true; + @SuppressWarnings("rawtypes") static final BiPredicate OBJECT_EQUAL = Object::equals; @SuppressWarnings("rawtypes") static final Function IDENTITY_FUNCTION = Function.identity(); From 5a8a74eb8fa6ea9013698e547fbd6a81e2265235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 26 Oct 2022 18:00:34 +0200 Subject: [PATCH 074/312] Remove marker need for tap/handle context-propagation (#3251) This commit removes the notion of a `Context` marker inserted by the `contextCapture()` operator as a precondition for `tap` and `handle` to restore ThreadLocals from a `ContextSnapshot` implicitely. Now, the operators only check that A) `context-propagation` library is available at runtime and B) the downstream context is not empty. The javadoc and reference guide have been amended to reflect that fact. Fixes #3250. --- .../asciidoc/advanced-contextPropagation.adoc | 8 +- .../core/publisher/ContextPropagation.java | 17 +--- .../java/reactor/core/publisher/Flux.java | 35 +++++---- .../java/reactor/core/publisher/Mono.java | 35 +++++---- .../ContextPropagationNotThereSmokeTest.java | 2 - .../publisher/ContextPropagationTest.java | 77 ++++++++----------- 6 files changed, 81 insertions(+), 93 deletions(-) diff --git a/docs/asciidoc/advanced-contextPropagation.adoc b/docs/asciidoc/advanced-contextPropagation.adoc index d20377fbae..3a2fc76ebb 100644 --- a/docs/asciidoc/advanced-contextPropagation.adoc +++ b/docs/asciidoc/advanced-contextPropagation.adoc @@ -41,8 +41,12 @@ Mono.deferContextual(ctx -> ==== == Operators that transparently restore a snapshot: `handle` and `tap` -When using `contextCapture()` a marker is added to the Reactor `Context` in which the snapshot has been captured. -This is detected by `Flux` and `Mono` variants of `handle` and `tap`, which restore `ThreadLocal`s from that snapshot transparently. +Both `Flux` and `Mono` variants of `handle` and `tap` will have their behavior slightly modified +if the Context-Propagation library is available at runtime. + +Namely, if their downstream `ContextView` is not empty they will assume a context capture has occurred +(either manually or via the `contextCapture()` operator) and will attempt to restore `ThreadLocal`s from +that snapshot transparently. These operators will ensure restoration is performed around the user-provided code, respectively: - `handle` will wrap the `BiConsumer` in one which restores `ThreadLocal`s diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 521d88adbc..919123edaf 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -43,8 +43,6 @@ final class ContextPropagation { static final boolean isContextPropagationAvailable; - static final String CAPTURED_CONTEXT_MARKER = "reactor.core.contextSnapshotCaptured"; - static final Predicate PREDICATE_TRUE = v -> true; static final Function NO_OP = c -> c; static final Function WITH_GLOBAL_REGISTRY_NO_PREDICATE; @@ -89,10 +87,6 @@ static boolean isContextPropagationAvailable() { * provided {@link Context}, resulting in a new {@link Context} which includes entries * captured from threadLocals by the Context-Propagation API. *

    - * Additionally, a marker key is added to the returned {@link Context} which can - * be detected upstream by a small subset of operators in order to restore thread locals - * from the context transparently. - *

    * This variant uses the implicit global {@code ContextRegistry} and captures from all * available {@code ThreadLocalAccessors}. It is the same variant backing {@link Flux#contextCapture()} * and {@link Mono#contextCapture()}. @@ -115,10 +109,6 @@ static Function contextCapture() { * by the Context-Propagation API to filter which entries should be captured in the * first place. *

    - * Additionally, a marker key is added to the returned {@link Context} which can - * be detected upstream by a small subset of operators in order to restore thread locals - * from the context transparently. - *

    * This variant uses the implicit global {@code ContextRegistry} and captures only from * available {@code ThreadLocalAccessors} that match the {@link Predicate}. * @@ -134,7 +124,7 @@ static Function contextCapture(Predicate captureKeyPre } static BiConsumer> contextRestoreForHandle(BiConsumer> handler, CoreSubscriber actual) { - if (!ContextPropagation.isContextPropagationAvailable() || !actual.currentContext().hasKey(ContextPropagation.CAPTURED_CONTEXT_MARKER)) { + if (!ContextPropagation.isContextPropagationAvailable() || actual.currentContext().isEmpty()) { return handler; } return new ContextRestoreHandleConsumer<>(handler, ContextRegistry.getInstance(), actual.currentContext()); @@ -142,7 +132,7 @@ static BiConsumer> contextRestoreForHandle(BiConsum static SignalListener contextRestoringSignalListener(final SignalListener original, CoreSubscriber actual) { - if (!ContextPropagation.isContextPropagationAvailable() || !actual.currentContext().hasKey(ContextPropagation.CAPTURED_CONTEXT_MARKER)) { + if (!ContextPropagation.isContextPropagationAvailable() || actual.currentContext().isEmpty()) { return original; } return new ContextRestoreSignalListener(original, actual.currentContext(), ContextRegistry.getInstance()); @@ -162,8 +152,7 @@ static final class ContextCaptureFunction implements Function @Override public Context apply(Context target) { return ContextSnapshot.captureAllUsing(capturePredicate, this.registry) - .updateContext(target) - .put(CAPTURED_CONTEXT_MARKER, ""); + .updateContext(target); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 3e59c82a6f..5f7730dadb 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -4030,14 +4030,13 @@ public final Flux concatWith(Publisher other) { /** * If context-propagation library * is on the classpath, this is a convenience shortcut to capture thread local values during the - * subscription phase and put them in the {@link Context} that is visible upstream of this operator, - * alongside a marker key indicating that context capture occurred. + * subscription phase and put them in the {@link Context} that is visible upstream of this operator. *

    * As a result this operator should generally be used as close as possible to the end of * the chain / subscription point. - * If the marker key is encountered upstream, a small subset of operators will automatically restore the - * context snapshot ({@link #handle(BiConsumer) handle}, {@link #tap(SignalListenerFactory) tap}). *

    + * If the {@link ContextView} visible upstream is not empty, a small subset of operators will automatically + * restore the context snapshot ({@link #handle(BiConsumer) handle}, {@link #tap(SignalListenerFactory) tap}). * If context-propagation is not available at runtime, this operator simply returns the current {@link Flux} * instance. * @@ -5799,9 +5798,10 @@ public final Flux groupJoin( * fusion is enabled) when the {@link BiConsumer} throws an exception or if an error is signaled explicitly via * {@link SynchronousSink#error(Throwable)}. *

    - * When used in conjunction with {@link #contextCapture()} down the chain, thread locals - * are restored from the Reactor {@link ContextView} within the handler {@link BiConsumer} using the - * context-propagation library. + * When the context-propagation library + * is available at runtime and the downstream {@link ContextView} is not empty, this operator implicitly uses the + * library to restore thread locals around the handler {@link BiConsumer}. Typically, this would be done in conjunction + * with the use of {@link #contextCapture()} operator down the chain. * * @param handler the handling {@link BiConsumer} * @param the transformed type @@ -9036,9 +9036,10 @@ public final Flux takeWhile(Predicate continuePredicate) { * This simplified variant assumes the state is purely initialized within the {@link Supplier}, * as it is called for each incoming {@link Subscriber} without additional context. *

    - * When used in conjunction with {@link #contextCapture()} down the chain, thread locals - * are restored from the downstream {@link ContextView} around all invocations of {@link SignalListener} methods - * using the context-propagation library. + * When the context-propagation library + * is available at runtime and the downstream {@link ContextView} is not empty, this operator implicitly uses the library + * to restore thread locals around all invocations of {@link SignalListener} methods. Typically, this would be done + * in conjunction with the use of {@link #contextCapture()} operator down the chain. * * @param simpleListenerGenerator the {@link Supplier} to create a new {@link SignalListener} on each subscription * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} @@ -9072,9 +9073,10 @@ public SignalListener createListener(Publisher ignored1, Context * This simplified variant allows the {@link SignalListener} to be constructed for each subscription * with access to the incoming {@link Subscriber}'s {@link ContextView}. *

    - * When used in conjunction with {@link #contextCapture()} down the chain, thread locals - * are restored from the same {@link ContextView} around all invocations of {@link SignalListener} methods - * using the context-propagation library. + * When the context-propagation library + * is available at runtime and the {@link ContextView} is not empty, this operator implicitly uses the library + * to restore thread locals around all invocations of {@link SignalListener} methods. Typically, this would be done + * in conjunction with the use of {@link #contextCapture()} operator down the chain. * * @param listenerGenerator the {@link Function} to create a new {@link SignalListener} on each subscription * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} @@ -9109,9 +9111,10 @@ public SignalListener createListener(Publisher ignored1, Context * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} * the exception. *

    - * When used in conjunction with {@link #contextCapture()} down the chain, thread locals - * are restored from the downstream {@link ContextView} around all invocations of {@link SignalListener} methods - * using the context-propagation library. + * When the context-propagation library + * is available at runtime and the downstream {@link ContextView} is not empty, this operator implicitly uses the library + * to restore thread locals around all invocations of {@link SignalListener} methods. Typically, this would be done + * in conjunction with the use of {@link #contextCapture()} operator down the chain. * * @param listenerFactory the {@link SignalListenerFactory} to create a new {@link SignalListener} on each subscription * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index d566e0002c..6498adf8bc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -2230,14 +2230,13 @@ public final Flux concatWith(Publisher other) { /** * If context-propagation library * is on the classpath, this is a convenience shortcut to capture thread local values during the - * subscription phase and put them in the {@link Context} that is visible upstream of this operator, - * alongside a marker key indicating that context capture occurred. + * subscription phase and put them in the {@link Context} that is visible upstream of this operator. *

    * As a result this operator should generally be used as close as possible to the end of * the chain / subscription point. - * If the marker key is encountered upstream, a small subset of operators will automatically restore the - * context snapshot ({@link #handle(BiConsumer) handle}, {@link #tap(SignalListenerFactory) tap}). *

    + * If the {@link ContextView} visible upstream is not empty, a small subset of operators will automatically + * restore the context snapshot ({@link #handle(BiConsumer) handle}, {@link #tap(SignalListenerFactory) tap}). * If context-propagation is not available at runtime, this operator simply returns the current {@link Mono} * instance. * @@ -3147,9 +3146,10 @@ public final Mono hasElement() { * call must be performed and/or 0 or 1 {@link SynchronousSink#error(Throwable)} or * {@link SynchronousSink#complete()}. *

    - * When used in conjunction with {@link #contextCapture()} down the chain, thread locals - * are restored from the Reactor {@link ContextView} within the handler {@link BiConsumer} using the - * context-propagation library. + * When the context-propagation library + * is available at runtime and the downstream {@link ContextView} is not empty, this operator implicitly uses the + * library to restore thread locals around the handler {@link BiConsumer}. Typically, this would be done in conjunction + * with the use of {@link #contextCapture()} operator down the chain. * * @param handler the handling {@link BiConsumer} * @param the transformed type @@ -4578,9 +4578,10 @@ public final Mono takeUntilOther(Publisher other) { * This simplified variant assumes the state is purely initialized within the {@link Supplier}, * as it is called for each incoming {@link Subscriber} without additional context. *

    - * When used in conjunction with {@link #contextCapture()} down the chain, thread locals - * are restored from the downstream {@link ContextView} around all invocations of {@link SignalListener} methods - * using the context-propagation library. + * When the context-propagation library + * is available at runtime and the downstream {@link ContextView} is not empty, this operator implicitly uses the library + * to restore thread locals around all invocations of {@link SignalListener} methods. Typically, this would be done + * in conjunction with the use of {@link #contextCapture()} operator down the chain. * * @param simpleListenerGenerator the {@link Supplier} to create a new {@link SignalListener} on each subscription * @return a new {@link Mono} with side effects defined by generated {@link SignalListener} @@ -4614,9 +4615,10 @@ public SignalListener createListener(Publisher ignored1, Context * This simplified variant allows the {@link SignalListener} to be constructed for each subscription * with access to the incoming {@link Subscriber}'s {@link ContextView}. *

    - * When used in conjunction with {@link #contextCapture()} down the chain, thread locals - * are restored from the same {@link ContextView} around all invocations of {@link SignalListener} methods - * using the context-propagation library. + * When the context-propagation library + * is available at runtime and the {@link ContextView} is not empty, this operator implicitly uses the library + * to restore thread locals around all invocations of {@link SignalListener} methods. Typically, this would be done + * in conjunction with the use of {@link #contextCapture()} operator down the chain. * * @param listenerGenerator the {@link Function} to create a new {@link SignalListener} on each subscription * @return a new {@link Mono} with side effects defined by generated {@link SignalListener} @@ -4651,9 +4653,10 @@ public SignalListener createListener(Publisher ignored1, Context * {@link SignalListener#doAfterError(Throwable)} instead just {@link Operators#onErrorDropped(Throwable, Context) drop} * the exception. *

    - * When used in conjunction with {@link #contextCapture()} down the chain, thread locals - * are restored from the downstream {@link ContextView} around all invocations of {@link SignalListener} methods - * using the context-propagation library. + * When the context-propagation library + * is available at runtime and the downstream {@link ContextView} is not empty, this operator implicitly uses the library + * to restore thread locals around all invocations of {@link SignalListener} methods. Typically, this would be done + * in conjunction with the use of {@link #contextCapture()} operator down the chain. * * @param listenerFactory the {@link SignalListenerFactory} to create a new {@link SignalListener} on each subscription * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} diff --git a/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java b/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java index 5b2f310ad3..780a7a8ca5 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java @@ -16,8 +16,6 @@ package reactor.core.publisher; -import java.util.function.Predicate; - import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index 7efbb7b5e1..e2464871e1 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -150,8 +150,7 @@ void contextCaptureFunctionWithoutFiltering() { assertThat(asMap) .containsEntry(KEY1, "expected1") .containsEntry(KEY2, "expected2") - .containsEntry(ContextPropagation.CAPTURED_CONTEXT_MARKER, "") - .hasSize(3); + .hasSize(2); } @Test @@ -168,8 +167,7 @@ void captureWithFiltering() { assertThat(asMap) .containsEntry(KEY2, "expected") - .containsEntry(ContextPropagation.CAPTURED_CONTEXT_MARKER, "") - .hasSize(2); + .hasSize(1); } @Test @@ -181,23 +179,23 @@ void captureFunctionWithNullRegistryUsesGlobalRegistry() { } static private enum Cases { - NORMAL_NO_MARKER(false, false, false), - NORMAL_WITH_MARKER(false, false, true), - CONDITIONAL_NO_MARKER(false, true, false), - CONDITIONAL_WITH_MARKER(false, true, true), - FUSED_NO_MARKER(true, false, false), - FUSED_WITH_MARKER(true, false, true), - FUSED_CONDITIONAL_NO_MARKER(true, true, false), - FUSED_CONDITIONAL_WITH_MARKER(true, true, true); + NORMAL_NO_CONTEXT(false, false, false), + NORMAL_WITH_CONTEXT(false, false, true), + CONDITIONAL_NO_CONTEXT(false, true, false), + CONDITIONAL_WITH_CONTEXT(false, true, true), + FUSED_NO_CONTEXT(true, false, false), + FUSED_WITH_CONTEXT(true, false, true), + FUSED_CONDITIONAL_NO_CONTEXT(true, true, false), + FUSED_CONDITIONAL_WITH_CONTEXT(true, true, true); final boolean fusion; final boolean conditional; - final boolean marker; + final boolean withContext; - Cases(boolean fusion, boolean conditional, boolean marker) { + Cases(boolean fusion, boolean conditional, boolean withContext) { this.fusion = fusion; this.conditional = conditional; - this.marker = marker; + this.withContext = withContext; } } @@ -232,8 +230,8 @@ public SignalListener createListener(Publisher source, builder = builder.requireNotFuseable(); } - if (characteristics.marker) { - builder = builder.contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true); + if (characteristics.withContext) { + builder = builder.contextPut("properWrappingForFluxTap", true); } TestSubscriber testSubscriber; @@ -250,7 +248,7 @@ public SignalListener createListener(Publisher source, if (!characteristics.fusion) { assertThat(parent).isInstanceOfSatisfying(FluxTap.TapSubscriber.class, tapSubscriber -> { - if (characteristics.marker) { + if (characteristics.withContext) { assertThat(tapSubscriber.listener).as("listener wrapped") .isNotSameAs(originalListener) .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); @@ -265,7 +263,7 @@ public SignalListener createListener(Publisher source, else { assertThat(parent).isInstanceOfSatisfying(FluxTapFuseable.TapFuseableSubscriber.class, tapSubscriber -> { - if (characteristics.marker) { + if (characteristics.withContext) { assertThat(tapSubscriber.listener) .as("listener wrapped") .isNotSameAs(originalListener) @@ -308,8 +306,8 @@ public SignalListener createListener(Publisher source, builder = builder.requireNotFuseable(); } - if (characteristics.marker) { - builder = builder.contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true); + if (characteristics.withContext) { + builder = builder.contextPut("properWrappingForMonoTap", true); } TestSubscriber testSubscriber; @@ -326,7 +324,7 @@ public SignalListener createListener(Publisher source, if (!characteristics.fusion) { assertThat(parent).isInstanceOfSatisfying(FluxTap.TapSubscriber.class, tapSubscriber -> { - if (characteristics.marker) { + if (characteristics.withContext) { assertThat(tapSubscriber.listener) .as("listener wrapped") .isNotSameAs(originalListener) @@ -341,7 +339,7 @@ public SignalListener createListener(Publisher source, else { assertThat(parent).isInstanceOfSatisfying(FluxTapFuseable.TapFuseableSubscriber.class, tapSubscriber -> { - if (characteristics.marker) { + if (characteristics.withContext) { assertThat(tapSubscriber.listener) .as("listener wrapped") .isNotSameAs(originalListener) @@ -427,31 +425,30 @@ class ContextRestoreForHandle { @ValueSource(booleans = {true, false}) @ParameterizedTestWithName - void publicMethodChecksForMarkerBeforeWrapping(boolean withMarker) { + void publicMethodChecksForContextNotEmptyBeforeWrapping(boolean withContext) { BiConsumer> originalHandler = (v, sink) -> { }; final Context context; - if (withMarker) { - context = Context.of(KEY1, "expected", ContextPropagation.CAPTURED_CONTEXT_MARKER, true); + if (withContext) { + context = Context.of(KEY1, "expected"); } else { - context = Context.of(KEY1, "expected"); + context = Context.empty(); } CoreSubscriber mockSubscriber = Mockito.mock(CoreSubscriber.class); Mockito.when(mockSubscriber.currentContext()).thenReturn(context); BiConsumer> decoratedHandler = ContextPropagation.contextRestoreForHandle(originalHandler, mockSubscriber); - if (!withMarker) { - assertThat(decoratedHandler).as("no marker: same handler").isSameAs(originalHandler); + if (withContext) { + assertThat(decoratedHandler).as("context not empty: decorated handler").isNotSameAs(originalHandler); } else { - assertThat(decoratedHandler).as("marker: decorated handler").isNotSameAs(originalHandler); + assertThat(decoratedHandler).as("empty context: same handler").isSameAs(originalHandler); } } - @ValueSource(booleans = {true, false}) - @ParameterizedTestWithName - void classContextRestoreHandleConsumerRestoresWithOrWithoutMarker(boolean withMarker) { + @Test + void classContextRestoreHandleConsumerRestoresThreadLocal() { BiConsumer> originalHandler = (v, sink) -> { if (v.equals("bar")) { sink.next(v + "=" + REF1.get()); @@ -459,13 +456,7 @@ void classContextRestoreHandleConsumerRestoresWithOrWithoutMarker(boolean withMa }; final String expected = "bar=expected"; - final Context context; - if (withMarker) { - context = Context.of(KEY1, "expected", ContextPropagation.CAPTURED_CONTEXT_MARKER, true); - } - else { - context = Context.of(KEY1, "expected"); - } + final Context context = Context.of(KEY1, "expected"); BiConsumer> decoratedHandler = new ContextPropagation.ContextRestoreHandleConsumer<>(originalHandler, registry, context); @@ -482,8 +473,8 @@ void fluxHandleVariantsCallTheWrapper() { FluxHandle publisher = new FluxHandle<>(Flux.empty(), originalHandler); FluxHandleFuseable publisherFuseable = new FluxHandleFuseable<>(Flux.empty(), originalHandler); - CoreSubscriber actual = TestSubscriber.builder().contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true).build(); - Fuseable.ConditionalSubscriber actualCondi = TestSubscriber.builder().contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true).buildConditional(v -> true); + CoreSubscriber actual = TestSubscriber.builder().contextPut("fluxHandleVariantsCallTheWrapper", true).build(); + Fuseable.ConditionalSubscriber actualCondi = TestSubscriber.builder().contextPut("fluxHandleVariantsCallTheWrapper", true).buildConditional(v -> true); HandleSubscriber sub = (HandleSubscriber) publisher.subscribeOrReturn(actual); HandleConditionalSubscriber subCondi = (HandleConditionalSubscriber) publisher.subscribeOrReturn(actualCondi); @@ -521,7 +512,7 @@ void monoHandleVariantsCallTheWrapper() { MonoHandle publisher = new MonoHandle<>(Mono.empty(), originalHandler); MonoHandleFuseable publisherFuseable = new MonoHandleFuseable<>(Mono.empty(), originalHandler); - CoreSubscriber actual = TestSubscriber.builder().contextPut(ContextPropagation.CAPTURED_CONTEXT_MARKER, true).build(); + CoreSubscriber actual = TestSubscriber.builder().contextPut("monoHandleVariantsCallTheWrapper", true).build(); HandleSubscriber sub = (HandleSubscriber) publisher.subscribeOrReturn(actual); HandleFuseableSubscriber subFused = (HandleFuseableSubscriber) publisherFuseable.subscribeOrReturn(actual); From a62e031596c09f7385ebd93dedf2008b6cd2009d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 2 Nov 2022 14:45:01 +0100 Subject: [PATCH 075/312] Context propagation: use new APIs from latest snapshots (#3256) This commit upgrades context-propagation to post-RC1 snapshots. This allows for several polishes and improvements: ContextSnapshot.setAllThreadLocalsFrom can be used as a way of restoring thread locasl while avoiding intermediary representation. With the introduction of removal methods in ContextRegistry, it becomes more practical to use the ContextRegistry.getInstance global instance. Following that, two of the three intermediate classes are no longer needed and can be replaced with lambdas: ContextCaptureFunction and ContextRestoreHandleConsumer. Instead of calling the ContextPropagation methods with a subscriber (which necessitates some mocking in tests), we now take a Supplier of Context. A CoreSubscriber::currentContext method reference can do the trick in production code. --- gradle/libs.versions.toml | 2 +- .../core/publisher/ContextPropagation.java | 82 ++++++------------- .../reactor/core/publisher/FluxHandle.java | 4 +- .../core/publisher/FluxHandleFuseable.java | 4 +- .../java/reactor/core/publisher/FluxTap.java | 3 +- .../core/publisher/FluxTapFuseable.java | 4 +- .../reactor/core/publisher/MonoHandle.java | 2 +- .../core/publisher/MonoHandleFuseable.java | 2 +- .../java/reactor/core/publisher/MonoTap.java | 6 +- .../core/publisher/MonoTapFuseable.java | 4 +- .../publisher/ContextPropagationTest.java | 77 ++++++++--------- 11 files changed, 72 insertions(+), 118 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 60ba3cdbad..92ba5a4b0f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-RC1" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.0-RC1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-RC1" diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 919123edaf..8d60d19df1 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -19,11 +19,11 @@ import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import io.micrometer.context.ContextRegistry; import io.micrometer.context.ContextSnapshot; -import reactor.core.CoreSubscriber; import reactor.core.observability.SignalListener; import reactor.util.Logger; import reactor.util.Loggers; @@ -53,8 +53,9 @@ final class ContextPropagation { Function contextCaptureFunction; boolean contextPropagation; try { - ContextRegistry registry = ContextRegistry.getInstance(); - contextCaptureFunction = new ContextCaptureFunction(PREDICATE_TRUE, registry); + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + contextCaptureFunction = target -> ContextSnapshot.captureAllUsing(PREDICATE_TRUE, globalRegistry) + .updateContext(target); contextPropagation = true; } catch (LinkageError t) { @@ -120,40 +121,34 @@ static Function contextCapture(Predicate captureKeyPre if (!isContextPropagationAvailable) { return NO_OP; } - return new ContextCaptureFunction(captureKeyPredicate, null); + return target -> ContextSnapshot.captureAllUsing(captureKeyPredicate, ContextRegistry.getInstance()) + .updateContext(target); } - static BiConsumer> contextRestoreForHandle(BiConsumer> handler, CoreSubscriber actual) { - if (!ContextPropagation.isContextPropagationAvailable() || actual.currentContext().isEmpty()) { + static BiConsumer> contextRestoreForHandle(BiConsumer> handler, Supplier contextSupplier) { + if (!ContextPropagation.isContextPropagationAvailable()) { return handler; } - return new ContextRestoreHandleConsumer<>(handler, ContextRegistry.getInstance(), actual.currentContext()); - } - - static SignalListener contextRestoringSignalListener(final SignalListener original, - CoreSubscriber actual) { - if (!ContextPropagation.isContextPropagationAvailable() || actual.currentContext().isEmpty()) { - return original; + final Context ctx = contextSupplier.get(); + if (ctx.isEmpty()) { + return handler; } - return new ContextRestoreSignalListener(original, actual.currentContext(), ContextRegistry.getInstance()); + return (v, sink) -> { + try (ContextSnapshot.Scope ignored = ContextSnapshot.setAllThreadLocalsFrom(ctx)) { + handler.accept(v, sink); + } + }; } - //the Function indirection allows tests to directly assert code in this class rather than static methods - static final class ContextCaptureFunction implements Function { - - final Predicate capturePredicate; - final ContextRegistry registry; - - ContextCaptureFunction(Predicate capturePredicate, @Nullable ContextRegistry registry) { - this.capturePredicate = capturePredicate; - this.registry = registry != null ? registry : ContextRegistry.getInstance(); + static SignalListener contextRestoreForTap(final SignalListener original, Supplier contextSupplier) { + if (!ContextPropagation.isContextPropagationAvailable()) { + return original; } - - @Override - public Context apply(Context target) { - return ContextSnapshot.captureAllUsing(capturePredicate, this.registry) - .updateContext(target); + final Context ctx = contextSupplier.get(); + if (ctx.isEmpty()) { + return original; } + return new ContextRestoreSignalListener(original, ctx, null); } //the SignalListener implementation can be tested independently with a test-specific ContextRegistry @@ -170,10 +165,7 @@ public ContextRestoreSignalListener(SignalListener original, ContextView cont } ContextSnapshot.Scope restoreThreadLocals() { - //TODO for now ContextSnapshot static methods don't allow restoring _all_ TLs without an intermediate ContextSnapshot - return ContextSnapshot - .captureFrom(this.context, k -> true, this.registry) - .setThreadLocals(); + return ContextSnapshot.setAllThreadLocalsFrom(this.context, this.registry); } @Override @@ -288,30 +280,4 @@ public Context addToContext(Context originalContext) { } } } - - //the BiConsumer implementation can be tested independently with a test-specific ContextRegistry - static final class ContextRestoreHandleConsumer implements BiConsumer> { - - private final BiConsumer> originalHandler; - private final ContextRegistry registry; - private final ContextView reactorContext; - - ContextRestoreHandleConsumer(BiConsumer> originalHandler, ContextRegistry registry, - ContextView reactorContext) { - this.originalHandler = originalHandler; - this.registry = registry; - this.reactorContext = reactorContext; - } - - @Override - public void accept(T t, SynchronousSink sink) { - //TODO for now ContextSnapshot static methods don't allow restoring _all_ TLs without an intermediate ContextSnapshot - final ContextSnapshot snapshot = ContextSnapshot.captureFrom(this.reactorContext, k -> true, this.registry); - try (ContextSnapshot.Scope ignored = snapshot.setThreadLocals(k -> { - return true; - })) { - originalHandler.accept(t, sink); - } - } - } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java b/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java index 9890d394a0..21cf1cd460 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java @@ -19,8 +19,8 @@ import java.util.Objects; import java.util.function.BiConsumer; -import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; + import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.util.annotation.Nullable; @@ -45,7 +45,7 @@ final class FluxHandle extends InternalFluxOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual); + BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext); if (actual instanceof Fuseable.ConditionalSubscriber) { @SuppressWarnings("unchecked") Fuseable.ConditionalSubscriber cs = (Fuseable.ConditionalSubscriber) actual; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java index 08bd061d25..a7204f84dc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java @@ -19,8 +19,8 @@ import java.util.Objects; import java.util.function.BiConsumer; -import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; + import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Fuseable; @@ -58,7 +58,7 @@ final class FluxHandleFuseable extends InternalFluxOperator implemen @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual); + BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext); if (actual instanceof ConditionalSubscriber) { @SuppressWarnings("unchecked") ConditionalSubscriber cs = (ConditionalSubscriber) actual; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java index b6811980af..044dbc91fe 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java @@ -16,7 +16,6 @@ package reactor.core.publisher; -import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -59,7 +58,7 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act } // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods // (only if ContextPropagation.isContextPropagationAvailable() is true) - signalListener = ContextPropagation.contextRestoringSignalListener(signalListener, actual); + signalListener = ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext); try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java index d0e3411c1c..ae30d0d02e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java @@ -21,9 +21,9 @@ import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Fuseable; -import reactor.util.annotation.Nullable; import reactor.core.observability.SignalListener; import reactor.core.observability.SignalListenerFactory; +import reactor.util.annotation.Nullable; /** * A {@link reactor.core.Fuseable} generic per-Subscription side effect {@link Flux} that notifies a @@ -58,7 +58,7 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act } // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods // (only if ContextPropagation.isContextPropagationAvailable() is true) - signalListener = ContextPropagation.contextRestoringSignalListener(signalListener, actual); + signalListener = ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext); try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java b/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java index b6a33c6236..1dafb86b70 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java @@ -41,7 +41,7 @@ final class MonoHandle extends InternalMonoOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual); + BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext); return new FluxHandle.HandleSubscriber<>(actual, handler2); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java index 05c77e4a30..807df6eb6d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java @@ -43,7 +43,7 @@ final class MonoHandleFuseable extends InternalMonoOperator @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual); + BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext); return new FluxHandleFuseable.HandleFuseableSubscriber<>(actual, handler2); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java index 5931eb3904..58edad7417 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java @@ -18,10 +18,10 @@ import reactor.core.CoreSubscriber; import reactor.core.Fuseable; -import reactor.core.publisher.FluxTap.TapSubscriber; -import reactor.util.annotation.Nullable; import reactor.core.observability.SignalListener; import reactor.core.observability.SignalListenerFactory; +import reactor.core.publisher.FluxTap.TapSubscriber; +import reactor.util.annotation.Nullable; /** * A generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. @@ -55,7 +55,7 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act } // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods // (only if ContextPropagation.isContextPropagationAvailable() is true) - signalListener = ContextPropagation.contextRestoringSignalListener(signalListener, actual); + signalListener = ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext); try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java index 209b7a8856..be1efb4ae1 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java @@ -18,9 +18,9 @@ import reactor.core.CoreSubscriber; import reactor.core.Fuseable; -import reactor.util.annotation.Nullable; import reactor.core.observability.SignalListener; import reactor.core.observability.SignalListenerFactory; +import reactor.util.annotation.Nullable; /** * A {@link Fuseable} generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. @@ -54,7 +54,7 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act } // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods // (only if ContextPropagation.isContextPropagationAvailable() is true) - signalListener = ContextPropagation.contextRestoringSignalListener(signalListener, actual); + signalListener = ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext); try { signalListener.doFirst(); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index e2464871e1..4b39fda7d7 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -25,8 +25,9 @@ import io.micrometer.context.ContextRegistry; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.provider.EnumSource; @@ -62,15 +63,12 @@ class ContextPropagationTest { private static final ThreadLocal REF1 = ThreadLocal.withInitial(() -> "ref1_init"); private static final ThreadLocal REF2 = ThreadLocal.withInitial(() -> "ref2_init"); - //NOTE: no way to currently remove accessors from the ContextRegistry, so we recreate one on each test - private ContextRegistry registry; + @BeforeAll + static void initializeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); - @BeforeEach - void initializeThreadLocals() { - registry = new ContextRegistry().loadContextAccessors(); - - registry.registerThreadLocalAccessor(KEY1, REF1); - registry.registerThreadLocalAccessor(KEY2, REF2); + globalRegistry.registerThreadLocalAccessor(KEY1, REF1); + globalRegistry.registerThreadLocalAccessor(KEY2, REF2); } //the cleanup of "thread locals" could be especially important if one starts relying on @@ -81,18 +79,25 @@ void cleanupThreadLocals() { REF2.remove(); } + @AfterAll + static void removeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + + globalRegistry.removeThreadLocalAccessor(KEY1); + globalRegistry.removeThreadLocalAccessor(KEY2); + + } + @Test void isContextPropagationAvailable() { assertThat(ContextPropagation.isContextPropagationAvailable()).isTrue(); } - @Test void contextCaptureWithNoPredicateReturnsTheConstantFunction() { assertThat(ContextPropagation.contextCapture()) .as("no predicate nor registry") - .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) - .hasFieldOrPropertyWithValue("registry", ContextRegistry.getInstance()); + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE); } @Test @@ -105,9 +110,7 @@ void contextCaptureWithPredicateReturnsNewFunctionWithGlobalRegistry() { .isNotSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) .isNotSameAs(ContextPropagation.NO_OP) // as long as a predicate is supplied, the method creates new instances of the Function - .isNotSameAs(ContextPropagation.contextCapture(ContextPropagation.PREDICATE_TRUE)) - .isInstanceOfSatisfying(ContextPropagation.ContextCaptureFunction.class, f -> - assertThat(f.registry).as("function default registry").isSameAs(ContextRegistry.getInstance())); + .isNotSameAs(ContextPropagation.contextCapture(ContextPropagation.PREDICATE_TRUE)); } @Test @@ -137,8 +140,7 @@ class ContextCaptureFunctionTest { @Test void contextCaptureFunctionWithoutFiltering() { - ContextPropagation.ContextCaptureFunction test = new ContextPropagation.ContextCaptureFunction( - ContextPropagation.PREDICATE_TRUE, registry); + Function test = ContextPropagation.contextCapture(); REF1.set("expected1"); REF2.set("expected2"); @@ -155,8 +157,7 @@ void contextCaptureFunctionWithoutFiltering() { @Test void captureWithFiltering() { - ContextPropagation.ContextCaptureFunction test = new ContextPropagation.ContextCaptureFunction( - k -> k.toString().equals(KEY2), registry); + Function test = ContextPropagation.contextCapture(k -> k.toString().equals(KEY2)); REF1.set("not_expected"); REF2.set("expected"); @@ -169,13 +170,6 @@ void captureWithFiltering() { .containsEntry(KEY2, "expected") .hasSize(1); } - - @Test - void captureFunctionWithNullRegistryUsesGlobalRegistry() { - ContextPropagation.ContextCaptureFunction test = new ContextPropagation.ContextCaptureFunction(v -> true, null); - - assertThat(test.registry).as("default registry").isSameAs(ContextRegistry.getInstance()); - } } static private enum Cases { @@ -363,8 +357,9 @@ void threadLocalRestoredInSignalListener() throws InterruptedException { return null; }); - ContextPropagation.ContextRestoreSignalListener listener = new ContextPropagation.ContextRestoreSignalListener<>(tlReadingListener, context, - registry); + ContextPropagation.ContextRestoreSignalListener listener = + new ContextPropagation.ContextRestoreSignalListener<>(tlReadingListener, context, + null); Thread t = new Thread(() -> { try { @@ -434,10 +429,9 @@ void publicMethodChecksForContextNotEmptyBeforeWrapping(boolean withContext) { else { context = Context.empty(); } - CoreSubscriber mockSubscriber = Mockito.mock(CoreSubscriber.class); - Mockito.when(mockSubscriber.currentContext()).thenReturn(context); - BiConsumer> decoratedHandler = ContextPropagation.contextRestoreForHandle(originalHandler, mockSubscriber); + BiConsumer> decoratedHandler = ContextPropagation.contextRestoreForHandle(originalHandler, + () -> context); if (withContext) { assertThat(decoratedHandler).as("context not empty: decorated handler").isNotSameAs(originalHandler); @@ -458,7 +452,8 @@ void classContextRestoreHandleConsumerRestoresThreadLocal() { final String expected = "bar=expected"; final Context context = Context.of(KEY1, "expected"); - BiConsumer> decoratedHandler = new ContextPropagation.ContextRestoreHandleConsumer<>(originalHandler, registry, context); + BiConsumer> decoratedHandler = + ContextPropagation.contextRestoreForHandle(originalHandler, () -> context); SynchronousSink mockSink = Mockito.mock(SynchronousSink.class); decoratedHandler.accept("bar", mockSink); @@ -487,20 +482,16 @@ void fluxHandleVariantsCallTheWrapper() { softly.assertThat(sub.handler) .as("sub.handler") - .isNotSameAs(originalHandler) - .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + .isNotSameAs(originalHandler); softly.assertThat(subCondi.handler) .as("subCondi.handler") - .isNotSameAs(originalHandler) - .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + .isNotSameAs(originalHandler); softly.assertThat(subFused.handler) .as("subFused.handler") - .isNotSameAs(originalHandler) - .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + .isNotSameAs(originalHandler); softly.assertThat(subFusedCondi.handler) .as("subFusedCondi.handler") - .isNotSameAs(originalHandler) - .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + .isNotSameAs(originalHandler); }); } @@ -524,12 +515,10 @@ void monoHandleVariantsCallTheWrapper() { softly.assertThat(sub.handler) .as("sub.handler") - .isNotSameAs(originalHandler) - .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + .isNotSameAs(originalHandler); softly.assertThat(subFused.handler) .as("subFused.handler") - .isNotSameAs(originalHandler) - .isInstanceOf(ContextPropagation.ContextRestoreHandleConsumer.class); + .isNotSameAs(originalHandler); }); } } From b290bd8336232718505427cc0a91dced2522ad8c Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 3 Nov 2022 12:03:38 +0200 Subject: [PATCH 076/312] cleans context for concat map for retry companion publisher (#3262) --- .../reactor/util/retry/RetryBackoffSpec.java | 129 +++++++++--------- .../java/reactor/util/retry/RetrySpec.java | 44 +++--- .../core/publisher/FluxRetryWhenTest.java | 27 ++++ 3 files changed, 119 insertions(+), 81 deletions(-) diff --git a/reactor-core/src/main/java/reactor/util/retry/RetryBackoffSpec.java b/reactor-core/src/main/java/reactor/util/retry/RetryBackoffSpec.java index 3283bd7819..540501b5cf 100644 --- a/reactor-core/src/main/java/reactor/util/retry/RetryBackoffSpec.java +++ b/reactor-core/src/main/java/reactor/util/retry/RetryBackoffSpec.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.util.annotation.Nullable; +import reactor.util.context.Context; import reactor.util.context.ContextView; /** @@ -539,69 +540,73 @@ protected void validateArguments() { @Override public Flux generateCompanion(Flux t) { validateArguments(); - return t.concatMap(retryWhenState -> { - //capture the state immediately - RetrySignal copy = retryWhenState.copy(); - Throwable currentFailure = copy.failure(); - long iteration = isTransientErrors ? copy.totalRetriesInARow() : copy.totalRetries(); - - if (currentFailure == null) { - return Mono.error(new IllegalStateException("Retry.RetrySignal#failure() not expected to be null")); - } - - if (!errorFilter.test(currentFailure)) { - return Mono.error(currentFailure); - } - - if (iteration >= maxAttempts) { - return Mono.error(retryExhaustedGenerator.apply(this, copy)); - } - - Duration nextBackoff; - try { - nextBackoff = minBackoff.multipliedBy((long) Math.pow(2, iteration)); - if (nextBackoff.compareTo(maxBackoff) > 0) { + return Flux.deferContextual(cv -> + t.contextWrite(cv) + .concatMap(retryWhenState -> { + //capture the state immediately + RetrySignal copy = retryWhenState.copy(); + Throwable currentFailure = copy.failure(); + long iteration = isTransientErrors ? copy.totalRetriesInARow() : copy.totalRetries(); + + if (currentFailure == null) { + return Mono.error(new IllegalStateException("Retry.RetrySignal#failure() not expected to be null")); + } + + if (!errorFilter.test(currentFailure)) { + return Mono.error(currentFailure); + } + + if (iteration >= maxAttempts) { + return Mono.error(retryExhaustedGenerator.apply(this, copy)); + } + + Duration nextBackoff; + try { + nextBackoff = minBackoff.multipliedBy((long) Math.pow(2, iteration)); + if (nextBackoff.compareTo(maxBackoff) > 0) { + nextBackoff = maxBackoff; + } + } + catch (ArithmeticException overflow) { nextBackoff = maxBackoff; } - } - catch (ArithmeticException overflow) { - nextBackoff = maxBackoff; - } - - //short-circuit delay == 0 case - if (nextBackoff.isZero()) { - return RetrySpec.applyHooks(copy, Mono.just(iteration), + + //short-circuit delay == 0 case + if (nextBackoff.isZero()) { + return RetrySpec.applyHooks(copy, Mono.just(iteration), + syncPreRetry, syncPostRetry, asyncPreRetry, asyncPostRetry); + } + + ThreadLocalRandom random = ThreadLocalRandom.current(); + + long jitterOffset; + try { + jitterOffset = nextBackoff.multipliedBy((long) (100 * jitterFactor)) + .dividedBy(100) + .toMillis(); + } + catch (ArithmeticException ae) { + jitterOffset = Math.round(Long.MAX_VALUE * jitterFactor); + } + long lowBound = Math.max(minBackoff.minus(nextBackoff) + .toMillis(), -jitterOffset); + long highBound = Math.min(maxBackoff.minus(nextBackoff) + .toMillis(), jitterOffset); + + long jitter; + if (highBound == lowBound) { + if (highBound == 0) jitter = 0; + else jitter = random.nextLong(highBound); + } + else { + jitter = random.nextLong(lowBound, highBound); + } + Duration effectiveBackoff = nextBackoff.plusMillis(jitter); + return RetrySpec.applyHooks(copy, Mono.delay(effectiveBackoff, + backoffSchedulerSupplier.get()), syncPreRetry, syncPostRetry, asyncPreRetry, asyncPostRetry); - } - - ThreadLocalRandom random = ThreadLocalRandom.current(); - - long jitterOffset; - try { - jitterOffset = nextBackoff.multipliedBy((long) (100 * jitterFactor)) - .dividedBy(100) - .toMillis(); - } - catch (ArithmeticException ae) { - jitterOffset = Math.round(Long.MAX_VALUE * jitterFactor); - } - long lowBound = Math.max(minBackoff.minus(nextBackoff) - .toMillis(), -jitterOffset); - long highBound = Math.min(maxBackoff.minus(nextBackoff) - .toMillis(), jitterOffset); - - long jitter; - if (highBound == lowBound) { - if (highBound == 0) jitter = 0; - else jitter = random.nextLong(highBound); - } - else { - jitter = random.nextLong(lowBound, highBound); - } - Duration effectiveBackoff = nextBackoff.plusMillis(jitter); - return RetrySpec.applyHooks(copy, Mono.delay(effectiveBackoff, - backoffSchedulerSupplier.get()), - syncPreRetry, syncPostRetry, asyncPreRetry, asyncPostRetry); - }); + }) + .contextWrite(c -> Context.empty()) + ); } } diff --git a/reactor-core/src/main/java/reactor/util/retry/RetrySpec.java b/reactor-core/src/main/java/reactor/util/retry/RetrySpec.java index 55c3ae989f..710ff4de86 100644 --- a/reactor-core/src/main/java/reactor/util/retry/RetrySpec.java +++ b/reactor-core/src/main/java/reactor/util/retry/RetrySpec.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.context.Context; import reactor.util.context.ContextView; /** @@ -353,25 +354,30 @@ public RetrySpec transientErrors(boolean isTransientErrors) { @Override public Flux generateCompanion(Flux flux) { - return flux.concatMap(retryWhenState -> { - //capture the state immediately - RetrySignal copy = retryWhenState.copy(); - Throwable currentFailure = copy.failure(); - long iteration = isTransientErrors ? copy.totalRetriesInARow() : copy.totalRetries(); + return Flux.deferContextual(cv -> + flux + .contextWrite(cv) + .concatMap(retryWhenState -> { + //capture the state immediately + RetrySignal copy = retryWhenState.copy(); + Throwable currentFailure = copy.failure(); + long iteration = isTransientErrors ? copy.totalRetriesInARow() : copy.totalRetries(); - if (currentFailure == null) { - return Mono.error(new IllegalStateException("RetryWhenState#failure() not expected to be null")); - } - else if (!errorFilter.test(currentFailure)) { - return Mono.error(currentFailure); - } - else if (iteration >= maxAttempts) { - return Mono.error(retryExhaustedGenerator.apply(this, copy)); - } - else { - return applyHooks(copy, Mono.just(iteration), doPreRetry, doPostRetry, asyncPreRetry, asyncPostRetry); - } - }); + if (currentFailure == null) { + return Mono.error(new IllegalStateException("RetryWhenState#failure() not expected to be null")); + } + else if (!errorFilter.test(currentFailure)) { + return Mono.error(currentFailure); + } + else if (iteration >= maxAttempts) { + return Mono.error(retryExhaustedGenerator.apply(this, copy)); + } + else { + return applyHooks(copy, Mono.just(iteration), doPreRetry, doPostRetry, asyncPreRetry, asyncPostRetry); + } + }) + .contextWrite(c -> Context.empty()) + ); } //=================== diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxRetryWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxRetryWhenTest.java index bd2257255c..557b0f9dab 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxRetryWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxRetryWhenTest.java @@ -58,6 +58,33 @@ public class FluxRetryWhenTest { Flux rangeError = Flux.concat(Flux.range(1, 2), Flux.error(new RuntimeException("forced failure 0"))); + @Test + //https://github.com/reactor/reactor-core/issues/3253 + public void shouldFailWhenOnErrorContinueEnabled() { + Mono.create(sink -> { + throw new RuntimeException("blah"); + }) + .retryWhen(Retry.indefinitely().filter(t -> false)) + .onErrorContinue((e, o) -> {}) + .as(StepVerifier::create) + .expectError() + .verify(Duration.ofSeconds(10)); + } + + @Test + //https://github.com/reactor/reactor-core/issues/3253 + public void shouldWorkAsExpected() { + Mono.just(1) + .map(v -> { // ensure original context is propagated + throw new RuntimeException("boom"); + }) + .retryWhen(Retry.indefinitely().filter(t -> false)) + .onErrorContinue((e, o) -> {}) + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofSeconds(10)); + } + @Test public void dontRepeat() { AssertSubscriber ts = AssertSubscriber.create(); From 5d4866ae29e978db8f1f5674c1c291ccbca654ee Mon Sep 17 00:00:00 2001 From: Ivan Vyazmitinov Date: Thu, 3 Nov 2022 18:45:08 +0400 Subject: [PATCH 077/312] Document usage of the EmitFailureHandler.busyLooping in refguide (#3271) Adds documentation about #2883. --- docs/asciidoc/processors.adoc | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/asciidoc/processors.adoc b/docs/asciidoc/processors.adoc index 98c72c0665..b332f12d83 100644 --- a/docs/asciidoc/processors.adoc +++ b/docs/asciidoc/processors.adoc @@ -48,16 +48,30 @@ Multiple producer threads may concurrently generate data on the sink by doing th [source,java] ---- //thread1 -replaySink.emitNext(1, FAIL_FAST); +replaySink.emitNext(1, EmitFailureHandler.FAIL_FAST); //thread2, later -replaySink.emitNext(2, FAIL_FAST); +replaySink.emitNext(2, EmitFailureHandler.FAIL_FAST); //thread3, concurrently with thread 2 -EmitResult result = replaySink.tryEmitNext(3); //would return FAIL_NON_SERIALIZED +//would retry emitting for 2 seconds and fail with EmissionException if unsuccessful +replaySink.emitNext(3, EmitFailureHandler.busyLooping(Duration.ofSeconds(2))); + +//thread3, concurrently with thread 2 +//would return FAIL_NON_SERIALIZED +EmitResult result = replaySink.tryEmitNext(4); + + ---- ==== +[NOTE] +==== +When using the `busyLooping`, be aware that returned instances of `EmitFailureHandler` can not be reused, e.g., +it should be one call of `busyLooping` per `emitNext`. +Also, it is recommended to use a timeout above 100ms since smaller values don’t make practical sense. +==== + The `Sinks.Many` can be presented to downstream consumers as a `Flux`, like in the below example: ==== From 3dde1cc6e24d58cfe9497a26e0982966d96106b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 7 Nov 2022 11:44:03 +0100 Subject: [PATCH 078/312] Upgrade Micrometer/Context-Propagation dependencies to 1.10.0 GA (#3274) In the case of `context-propagation`, `micrometer-docs-generator` and `micrometer-tracing-integration-test`, we're in fact dealing with version `1.0.0` instead. --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 92ba5a4b0f..29dc2272ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.18" jmh = "1.35" junit = "5.9.1" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.0-RC1" +micrometer = "1.10.0" reactiveStreams = "1.0.4" [libraries] @@ -35,10 +35,10 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0-SNAPSHOT" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.0-RC1"} +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0" +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.0"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0-RC1" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0" micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.8.1" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 3dd366fd159505d779d0410a2696f3ce22681657 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 8 Nov 2022 08:38:59 +0200 Subject: [PATCH 079/312] [release] Prepare and release 3.5.0 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b5e45ed680..49c4aedbd4 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.0-RC1" - testCompile "io.projectreactor:reactor-test:3.5.0-RC1" + compile "io.projectreactor:reactor-core:3.5.0" + testCompile "io.projectreactor:reactor-test:3.5.0" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.0-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.0-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.1-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.1-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.0-RC1" + // implementation "io.projectreactor:reactor-tools:3.5.0" } ``` diff --git a/gradle.properties b/gradle.properties index bbbbcb86bd..d15307bf8f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0-SNAPSHOT -bomVersion=2022.0.0-RC1 -metricsMicrometerVersion=1.0.0-SNAPSHOT +version=3.5.0 +bomVersion=2022.0.0 +metricsMicrometerVersion=1.0.0 From d5679a345544cda8638b3bf542260c467bc21b60 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 8 Nov 2022 09:20:48 +0200 Subject: [PATCH 080/312] Add jcenter() to the repositories list jcenter() is added to the repositories list in order to be able to resolve org.ysb33r.gradle:grolifant:0.16.1 ``` > Could not resolve org.ysb33r.gradle:grolifant:0.16.1. Required by: project : > org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:3.3.2 > org.asciidoctor:asciidoctor-gradle-jvm:3.3.2 project : > org.asciidoctor.jvm.pdf:org.asciidoctor.jvm.pdf.gradle.plugin:3.3.2 > org.asciidoctor:asciidoctor-gradle-jvm-pdf:3.3.2 project : > org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:3.3.2 > org.asciidoctor:asciidoctor-gradle-jvm:3.3.2 > org.asciidoctor:asciidoctor-gradle-base:3.3.2 > Could not resolve org.ysb33r.gradle:grolifant:0.16.1. > Could not get resource 'https://repo.spring.io/plugins-release/org/ysb33r/gradle/grolifant/0.16.1/grolifant-0.16.1.pom'. > Could not HEAD 'https://repo.spring.io/plugins-release/org/ysb33r/gradle/grolifant/0.16.1/grolifant-0.16.1.pom'. Received status code 401 from server: Unauthorized ``` --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index cae8e649e9..b7ec7bcc7c 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ buildscript { repositories { mavenCentral() maven { url "https://repo.spring.io/plugins-release" } + jcenter() } } From 765fdbc2903e5f55c47e79bf5cf38b3bca791c4f Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 8 Nov 2022 10:42:42 +0200 Subject: [PATCH 081/312] [release] Next development version 3.5.1-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- reactor-core/build.gradle | 31 ------------------------------- reactor-test/build.gradle | 2 +- 4 files changed, 5 insertions(+), 36 deletions(-) diff --git a/gradle.properties b/gradle.properties index d15307bf8f..32ff65ac09 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.0 +version=3.5.1-SNAPSHOT bomVersion=2022.0.0 -metricsMicrometerVersion=1.0.0 +metricsMicrometerVersion=1.0.1-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29dc2272ea..62adaccd92 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.4.24" -baselinePerfCore = "3.4.24" +baseline-core-api = "3.5.0" +baselinePerfCore = "3.5.0" baselinePerfExtra = "3.4.8" # Other shared versions diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 09112c23c2..1a551063b6 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -193,39 +193,8 @@ task japicmp(type: JapicmpTask) { // TODO after a .0 release, bump the gradle.properties baseline // TODO after a .0 release, remove the reactor-core exclusions below if any classExcludes = [ - "reactor.core.publisher.FluxExtensionsKt", - "reactor.core.publisher.MonoExtensionsKt", - "reactor.core.publisher.MonoWhenFunctionsKt", - "reactor.util.function.TupleExtensionsKt", - "reactor.core.publisher.DirectProcessor", - "reactor.core.publisher.DirectInnerContainer", - "reactor.core.publisher.FluxProcessor", - "reactor.core.publisher.EmitterProcessor", - "reactor.core.publisher.MonoProcessor", - "reactor.core.publisher.ReplayProcessor", - "reactor.core.publisher.UnicastProcessor" ] methodExcludes = [ - 'reactor.core.publisher.Mono#doAfterSuccessOrError(java.util.function.BiConsumer)', - 'reactor.core.publisher.Mono#doOnSuccessOrError(java.util.function.BiConsumer)', - 'reactor.core.publisher.Flux#deferWithContext(java.util.function.Function)', - 'reactor.core.publisher.Flux#subscriberContext(reactor.util.context.Context)', - 'reactor.core.publisher.Flux#subscriberContext(java.util.function.Function)', - 'reactor.core.publisher.Mono#deferWithContext(java.util.function.Function)', - 'reactor.core.publisher.Mono#subscriberContext()', - 'reactor.core.publisher.Mono#subscriberContext(reactor.util.context.Context)', - 'reactor.core.publisher.Mono#subscriberContext(java.util.function.Function)', - 'reactor.core.publisher.Signal#getContext()', - 'reactor.util.context.Context#putAll(reactor.util.context.Context)', - 'reactor.core.scheduler.Schedulers#elastic()', - 'reactor.core.scheduler.Schedulers#newElastic(java.lang.String)', - 'reactor.core.scheduler.Schedulers#newElastic(java.lang.String, int)', - 'reactor.core.scheduler.Schedulers#newElastic(java.lang.String, int, boolean)', - 'reactor.core.scheduler.Schedulers#newElastic(int, java.util.concurrent.ThreadFactory)', - 'reactor.core.scheduler.Schedulers$Factory#newElastic(int, java.util.concurrent.ThreadFactory)', - 'reactor.core.Scannable#tagsDeduplicated()', - "reactor.core.publisher.Mono#toProcessor()", - "reactor.core.scheduler.Scheduler#disposeGracefully()" ] } diff --git a/reactor-test/build.gradle b/reactor-test/build.gradle index 343e8e772b..205e1bfd37 100644 --- a/reactor-test/build.gradle +++ b/reactor-test/build.gradle @@ -111,7 +111,7 @@ task japicmp(type: JapicmpTask) { compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ] // TODO after a .0 release, remove the reactor-test exclusions below if any - classExcludes = [ "reactor.test.StepVerifierExtensionsKt" ] + classExcludes = [ ] methodExcludes = [ ] } From ab8edf97ab9103769daac472e5506c3dc1eab096 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 8 Nov 2022 12:05:11 +0200 Subject: [PATCH 082/312] Temporary revert to baselinePerfCore = "3.4.24" --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62adaccd92..41b1e9f8b8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Baselines, should be updated on every release baseline-core-api = "3.5.0" -baselinePerfCore = "3.5.0" +baselinePerfCore = "3.4.24" baselinePerfExtra = "3.4.8" # Other shared versions From 4537dae3f6b78f1805a0d43e25ebc1d1bfd895f9 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Mon, 14 Nov 2022 11:03:49 +0200 Subject: [PATCH 083/312] Revert "Add jcenter() to the repositories list" (#3279) This reverts commit d5679a3. --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index b7ec7bcc7c..cae8e649e9 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,6 @@ buildscript { repositories { mavenCentral() maven { url "https://repo.spring.io/plugins-release" } - jcenter() } } From bd495217244925db700a7ebfb3ca511052e7cb35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 14 Nov 2022 10:05:29 +0100 Subject: [PATCH 084/312] Perf baseline 3.5.0, adapt Shakespeare bench to 2022.0.0 release (#3278) The `benchmarks` project is ready to move to the 3.5.x baseline, now that all 2022.0.0 artifacts are released. That said, the Shakespeare benchmark still uses deprecated ElasticScheduler, which is removed in 3.5.0. This commit switches the benchmark to use BoundedElasticScheduler in addition to upgrading the perf dependencies to 3.5.0. Fixes #3277. --- .../reactor/core/scheduler/OldBoundedElasticScheduler.java | 1 + .../core/scrabble/ShakespearePlaysScrabbleParallelOpt.java | 2 +- gradle/libs.versions.toml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/benchmarks/src/main/java/reactor/core/scheduler/OldBoundedElasticScheduler.java b/benchmarks/src/main/java/reactor/core/scheduler/OldBoundedElasticScheduler.java index 7c789c7154..11a71d5056 100644 --- a/benchmarks/src/main/java/reactor/core/scheduler/OldBoundedElasticScheduler.java +++ b/benchmarks/src/main/java/reactor/core/scheduler/OldBoundedElasticScheduler.java @@ -148,6 +148,7 @@ public boolean isDisposed() { return BOUNDED_SERVICES.get(this) == SHUTDOWN; } + @SuppressWarnings("deprecation") @Override public void start() { for (;;) { diff --git a/benchmarks/src/main/java/reactor/core/scrabble/ShakespearePlaysScrabbleParallelOpt.java b/benchmarks/src/main/java/reactor/core/scrabble/ShakespearePlaysScrabbleParallelOpt.java index ffb592ba81..df8dbaddf2 100644 --- a/benchmarks/src/main/java/reactor/core/scrabble/ShakespearePlaysScrabbleParallelOpt.java +++ b/benchmarks/src/main/java/reactor/core/scrabble/ShakespearePlaysScrabbleParallelOpt.java @@ -50,7 +50,7 @@ public static void main(String[] args) throws Exception { @Setup public void localSetup() { - scheduler = Schedulers.newElastic("RcParallel"); + scheduler = Schedulers.newBoundedElastic(Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE, Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, "RcParallel"); } @TearDown diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41b1e9f8b8..be7a3c7627 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] # Baselines, should be updated on every release baseline-core-api = "3.5.0" -baselinePerfCore = "3.4.24" -baselinePerfExtra = "3.4.8" +baselinePerfCore = "3.5.0" +baselinePerfExtra = "3.5.0" # Other shared versions asciidoctor = "3.3.2" From e0de5a1918fd1168589ea69f6bb5daecbb79fdfe Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 13 Dec 2022 19:49:19 +0100 Subject: [PATCH 085/312] [release] Prepare and release 3.5.1 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 49c4aedbd4..53bfe14565 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.0" - testCompile "io.projectreactor:reactor-test:3.5.0" + compile "io.projectreactor:reactor-core:3.5.1" + testCompile "io.projectreactor:reactor-test:3.5.1" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.1-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.1-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.2-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.2-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.0" + // implementation "io.projectreactor:reactor-tools:3.5.1" } ``` diff --git a/gradle.properties b/gradle.properties index 32ff65ac09..034b4d4fae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.1-SNAPSHOT -bomVersion=2022.0.0 -metricsMicrometerVersion=1.0.1-SNAPSHOT +version=3.5.1 +bomVersion=2022.0.1 +metricsMicrometerVersion=1.0.1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index be7a3c7627..c4a66228f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.18" jmh = "1.35" junit = "5.9.1" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.0" +micrometer = "1.10.2" reactiveStreams = "1.0.4" [libraries] From 13f88e4d05febe7026ee1a0b57bb41ac1a7983c9 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 13 Dec 2022 20:38:01 +0100 Subject: [PATCH 086/312] [release] Next development version 3.5.2-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 034b4d4fae..33e118ff20 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.1 +version=3.5.2-SNAPSHOT bomVersion=2022.0.1 -metricsMicrometerVersion=1.0.1 +metricsMicrometerVersion=1.0.2-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4a66228f7..20e31f93ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.0" -baselinePerfCore = "3.5.0" +baseline-core-api = "3.5.1" +baselinePerfCore = "3.5.1" baselinePerfExtra = "3.5.0" # Other shared versions From 9d47a04b3accfed745f8a5e7acaaf3b1b66d67e8 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Wed, 4 Jan 2023 17:09:56 +0200 Subject: [PATCH 087/312] Add reflection hints for native-image support (#3325) Fixes #3321 --- .../reactor-core/reflect-config.json | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 reactor-core/src/main/resources/META-INF/native-image/io.projectreactor/reactor-core/reflect-config.json diff --git a/reactor-core/src/main/resources/META-INF/native-image/io.projectreactor/reactor-core/reflect-config.json b/reactor-core/src/main/resources/META-INF/native-image/io.projectreactor/reactor-core/reflect-config.json new file mode 100644 index 0000000000..4104625b7f --- /dev/null +++ b/reactor-core/src/main/resources/META-INF/native-image/io.projectreactor/reactor-core/reflect-config.json @@ -0,0 +1,38 @@ +[ + { + "condition": { + "typeReachable": "reactor.core.publisher.Traces" + }, + "name": "reactor.core.publisher.Traces$StackWalkerCallSiteSupplierFactory", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "reactor.core.publisher.Traces" + }, + "name": "reactor.core.publisher.Traces$SharedSecretsCallSiteSupplierFactory", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReachable": "reactor.core.publisher.Traces" + }, + "name": "reactor.core.publisher.Traces$ExceptionCallSiteSupplierFactory", + "methods": [ + { + "name": "", + "parameterTypes": [] + } + ] + } +] From bc4d516bafcaa55ad2dcf015c940825a100eb1ae Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 10 Jan 2023 16:39:10 +0200 Subject: [PATCH 088/312] [release] Prepare and release 3.5.2 Signed-off-by: Oleh Dokuka --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 53bfe14565..4675f1ae9a 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.1" - testCompile "io.projectreactor:reactor-test:3.5.1" + compile "io.projectreactor:reactor-core:3.5.2" + testCompile "io.projectreactor:reactor-test:3.5.2" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.2-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.2-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.3-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.3-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.1" + // implementation "io.projectreactor:reactor-tools:3.5.2" } ``` diff --git a/gradle.properties b/gradle.properties index 33e118ff20..806020dce7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.2-SNAPSHOT -bomVersion=2022.0.1 -metricsMicrometerVersion=1.0.2-SNAPSHOT +version=3.5.2 +bomVersion=2022.0.2 +metricsMicrometerVersion=1.0.2 From 37e9c71b1781d093755b7f29f649f267316f0263 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 10 Jan 2023 17:21:39 +0200 Subject: [PATCH 089/312] [release] Next development version 3.5.3-SNAPSHOT Signed-off-by: Oleh Dokuka --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 806020dce7..ba265b0734 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.2 +version=3.5.3-SNAPSHOT bomVersion=2022.0.2 -metricsMicrometerVersion=1.0.2 +metricsMicrometerVersion=1.0.3-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20e31f93ab..c64d8592e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.1" -baselinePerfCore = "3.5.1" +baseline-core-api = "3.5.2" +baselinePerfCore = "3.5.2" baselinePerfExtra = "3.5.0" # Other shared versions From 912441f7d36adfb84f06595cecd8895b7518d8c9 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Wed, 18 Jan 2023 13:07:08 +0100 Subject: [PATCH 090/312] Add .singleOptional() to Mono (#3317) Introducing a new operator for the `Mono` stack. It converts source `Mono` signals of type `T` to `Optional`. In case of a value, it is wrapped. When the source completes without a value, an empty `Optional` is delivered. Errors are simply propagated downstream. Having a dedicated operator for this case improves performance greatly over a combination of operators: `.map(Optional::ofNullable).defaultIfEmpty(Optional.empty())`. --- .../java/reactor/core/publisher/Mono.java | 33 +- .../core/publisher/MonoSingleOptional.java | 124 ++ .../publisher/MonoSingleOptionalCallable.java | 98 ++ .../doc-files/marbles/singleOptional.svg | 1220 +++++++++++++++++ .../core/publisher/MonoSingleMonoTest.java | 15 +- .../MonoSingleOptionalCallableTest.java | 141 ++ .../publisher/MonoSingleOptionalTest.java | 186 +++ 7 files changed, 1814 insertions(+), 3 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptional.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptionalCallable.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/singleOptional.svg create mode 100644 reactor-core/src/test/java/reactor/core/publisher/MonoSingleOptionalCallableTest.java create mode 100644 reactor-core/src/test/java/reactor/core/publisher/MonoSingleOptionalTest.java diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 77cf41a2d8..b715cef9de 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -4245,6 +4245,37 @@ public final Mono single() { return Mono.onAssembly(new MonoSingleMono<>(this)); } + /** + * Wrap the item produced by this {@link Mono} source into an Optional + * or emit an empty Optional for an empty source. + *

    + * + *

    + * + * @return a {@link Mono} with an Optional containing the item, an empty optional or an error signal + */ + public final Mono> singleOptional() { + if (this instanceof Callable) { + if (this instanceof Fuseable.ScalarCallable) { + @SuppressWarnings("unchecked") + Fuseable.ScalarCallable scalarCallable = (Fuseable.ScalarCallable) this; + + T v; + try { + v = scalarCallable.call(); + } + catch (Exception e) { + return Mono.error(Exceptions.unwrap(e)); + } + return Mono.just(Optional.ofNullable(v)); + } + @SuppressWarnings("unchecked") + Callable thiz = (Callable)this; + return Mono.onAssembly(new MonoSingleOptionalCallable<>(thiz)); + } + return Mono.onAssembly(new MonoSingleOptional<>(this)); + } + /** * Subscribe to this {@link Mono} and request unbounded demand. *

    diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptional.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptional.java new file mode 100644 index 0000000000..dd0d43331a --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptional.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Optional; + +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +/** + * Emits a single item from the source wrapped into an Optional, emits + * an empty Optional instead for empty source. + * + * @param the value type + * @see Reactive-Streams-Commons + */ +final class MonoSingleOptional extends InternalMonoOperator> { + + MonoSingleOptional(Mono source) { + super(source); + } + + @Override + public CoreSubscriber subscribeOrReturn(CoreSubscriber> actual) { + return new MonoSingleOptional.SingleOptionalSubscriber<>(actual); + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + return super.scanUnsafe(key); + } + + static final class SingleOptionalSubscriber extends Operators.MonoInnerProducerBase> implements InnerConsumer { + + Subscription s; + + boolean done; + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.TERMINATED) return done; + if (key == Attr.PARENT) return s; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return super.scanUnsafe(key); + } + + @Override + public Context currentContext() { + return actual().currentContext(); + } + + SingleOptionalSubscriber(CoreSubscriber> actual) { + super(actual); + } + + @Override + public void doOnRequest(long n) { + s.request(Long.MAX_VALUE); + } + + @Override + public void doOnCancel() { + s.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + actual().onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + Operators.onNextDropped(t, actual().currentContext()); + return; + } + done = true; + complete(Optional.of(t)); + } + + @Override + public void onError(Throwable t) { + if (done) { + Operators.onErrorDropped(t, actual().currentContext()); + return; + } + done = true; + actual().onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + complete(Optional.empty()); + } + + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptionalCallable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptionalCallable.java new file mode 100644 index 0000000000..360e53ce45 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptionalCallable.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.util.annotation.Nullable; + +import java.time.Duration; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Callable; + +/** + * Emits a single item from the source wrapped into an Optional, emits + * an empty Optional instead for empty source. + * + * @param the value type + * @see Reactive-Streams-Commons + */ +final class MonoSingleOptionalCallable extends Mono> + implements Callable>, SourceProducer> { + + final Callable callable; + + MonoSingleOptionalCallable(Callable source) { + this.callable = Objects.requireNonNull(source, "source"); + } + + @Override + public void subscribe(CoreSubscriber> actual) { + Operators.MonoInnerProducerBase> + sds = new Operators.MonoInnerProducerBase<>(actual); + + actual.onSubscribe(sds); + + if (sds.isCancelled()) { + return; + } + + try { + T t = callable.call(); + sds.complete(Optional.ofNullable(t)); + } + catch (Throwable e) { + actual.onError(Operators.onOperatorError(e, actual.currentContext())); + } + + } + + @Override + public Optional block() { + //duration is ignored below + return block(Duration.ZERO); + } + + @Override + public Optional block(Duration m) { + final T v; + + try { + v = callable.call(); + } + catch (Throwable e) { + throw Exceptions.propagate(e); + } + + return Optional.ofNullable(v); + } + + @Override + public Optional call() throws Exception { + final T v = callable.call(); + + return Optional.ofNullable(v); + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + return null; + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/singleOptional.svg b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/singleOptional.svg new file mode 100644 index 0000000000..bf5aaa56c4 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/doc-files/marbles/singleOptional.svg @@ -0,0 +1,1220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + singleOptional + + + + + + + + + + + + + + + + + + + + + Optional.of(     ) + + Optional.empty() + diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoSingleMonoTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoSingleMonoTest.java index d443972a6a..f9787c8264 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoSingleMonoTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoSingleMonoTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,12 @@ public void callableValued() { .expectNext("foo") .verifyComplete(); } + + @Test + public void callableError() { + StepVerifier.create(Mono.error(new IllegalStateException("failed")).single()) + .expectErrorMessage("failed"); + } @Test public void normalEmpty() { @@ -59,7 +65,12 @@ public void normalValued() { .expectNext("foo") .verifyComplete(); } - + + @Test + public void normalError() { + StepVerifier.create(Mono.error(new IllegalStateException("failed")).hide().single()) + .expectErrorMessage("failed"); + } // see https://github.com/reactor/reactor-core/issues/2663 @Test void fusionMonoSingleMonoDoesntTriggerFusion() { diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoSingleOptionalCallableTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoSingleOptionalCallableTest.java new file mode 100644 index 0000000000..73c4ac1d40 --- /dev/null +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoSingleOptionalCallableTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2021-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.Callable; + +import org.junit.jupiter.api.Test; + +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.test.StepVerifier; + +class MonoSingleOptionalCallableTest { + + @Test + void testCallableFusedEmptySource() { + Mono> mono = Mono + .fromSupplier(() -> null) + .singleOptional(); + + StepVerifier.create(mono) + .expectNext(Optional.empty()) + .verifyComplete(); + } + + @Test + void testCallableFusedSingleEmptySourceOnBlock() { + Mono> mono = Mono + .fromSupplier(() -> null) + .singleOptional(); + + assertEquals(Optional.empty(), mono.block()); + } + + @Test + void testCallableFusedSingleEmptySourceOnCall() throws Exception { + Mono> mono = Mono + .fromSupplier(() -> null) + .singleOptional(); + + assertThat(mono).isInstanceOf(MonoSingleOptionalCallable.class); + + assertEquals(Optional.empty(), ((Callable) mono).call()); + } + + @Test + void sourceNull() { + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { + new MonoSingleOptionalCallable<>(null); + }); + } + + @Test + void normal() { + StepVerifier.create(new MonoSingleOptionalCallable<>(() -> 1)) + .expectNext(Optional.of(1)) + .verifyComplete(); + } + + @Test + void normalBackpressured() { + StepVerifier.create(new MonoSingleOptionalCallable<>(() -> 1), 0) + .expectSubscription() + .expectNoEvent(Duration.ofMillis(50)) + .thenRequest(1) + .expectNext(Optional.of(1)) + .verifyComplete(); + } + + //scalarCallable empty/error/just are not instantiating MonoSingleOptionalCallable and are covered in MonoSingleTest + //we still cover the case where a callable source throws + + @Test + void failingCallable() { + StepVerifier.create(new MonoSingleOptionalCallable<>(() -> { throw new IllegalStateException("test"); } )) + .verifyErrorMessage("test"); + } + + @Test + void emptyCallable() { + StepVerifier.create(new MonoSingleOptionalCallable<>(() -> null)) + .expectNext(Optional.empty()) + .verifyComplete(); + } + + @Test + void valuedCallable() { + @SuppressWarnings("unchecked") + Callable fluxCallable = (Callable) Mono.fromCallable(() -> 1).flux(); + + + StepVerifier.create(new MonoSingleOptionalCallable<>(fluxCallable)) + .expectNext(Optional.of(1)) + .verifyComplete(); + } + + @Test + void fusionMonoSingleOptionalCallableDoesntTriggerFusion() { + Mono> fusedCase = Mono + .fromCallable(() -> 1) + .singleOptional(); + + assertThat(fusedCase) + .as("fusedCase assembly check") + .isInstanceOf(MonoSingleOptionalCallable.class) + .isNotInstanceOf(Fuseable.class); + + assertThatCode(() -> fusedCase.filter(v -> true).block()) + .as("fusedCase fused") + .doesNotThrowAnyException(); + } + + @Test + void scanOperator(){ + MonoSingleOptionalCallable test = new MonoSingleOptionalCallable<>(() -> "foo"); + + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); + } + +} diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoSingleOptionalTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoSingleOptionalTest.java new file mode 100644 index 0000000000..44b5b93116 --- /dev/null +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoSingleOptionalTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.function.Function; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Subscription; + +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.test.StepVerifier; + +public class MonoSingleOptionalTest { + + @Nested + class ConcreteClassConsistency { + // tests Mono.singleOptional returned classes + + @Test + void monoWithScalarEmpty() { + Mono source = Mono.empty(); + Mono> singleOptional = source.singleOptional(); + + assertThat(source).as("source").isInstanceOf(Fuseable.ScalarCallable.class); + assertThat(singleOptional).as("singleOptional") + .isInstanceOf(MonoJust.class) + .isInstanceOf(Fuseable.ScalarCallable.class); + } + + @Test + void monoWithScalarError() { + Mono source = Mono.error(new IllegalStateException("test")); + Mono> singleOptional = source.singleOptional(); + + assertThat(source).as("source").isInstanceOf(Fuseable.ScalarCallable.class); + assertThat(singleOptional).as("singleOptional") + .isInstanceOf(MonoError.class) + .isInstanceOf(Fuseable.ScalarCallable.class); + } + + @Test + void monoWithScalarValue() { + Mono source = Mono.just(1); + Mono> single = source.singleOptional(); + + assertThat(source).as("source").isInstanceOf(Fuseable.ScalarCallable.class); + assertThat(single).as("singleOptional") + .isInstanceOf(MonoJust.class) + .isInstanceOf(Fuseable.ScalarCallable.class); + } + + @Test + void monoWithCallable() { + Mono source = Mono.fromSupplier(() -> 1); + Mono> single = source.singleOptional(); + + assertThat(source).as("source") + .isInstanceOf(Callable.class) + .isNotInstanceOf(Fuseable.ScalarCallable.class); + assertThat(single).as("singleOptional").isInstanceOf(MonoSingleOptionalCallable.class); + } + + @Test + void monoWithNormal() { + Mono source = Mono.just(1).hide(); + Mono> single = source.singleOptional(); + + assertThat(source).as("source").isNotInstanceOf(Callable.class); // excludes + // ScalarCallable + // too + assertThat(single).as("singleOptional").isInstanceOf(MonoSingleOptional.class); + } + } + + @Test + void source1Null() { + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { + new MonoSingleOptional<>(null); + }); + } + + @Test + public void callableEmpty() { + StepVerifier.create(Mono.empty().singleOptional()) + .expectNext(Optional.empty()) + .verifyComplete(); + } + + @Test + public void callableValued() { + StepVerifier.create(Mono.just("foo").singleOptional()) + .expectNext(Optional.of("foo")) + .verifyComplete(); + } + + @Test + public void callableError() { + StepVerifier.create(Mono.error(new IllegalStateException("failed")).singleOptional()) + .expectErrorMessage("failed"); + } + + @Test + public void normalEmpty() { + StepVerifier.create(Mono.empty().hide().singleOptional()) + .expectNext(Optional.empty()) + .verifyComplete(); + } + + @Test + public void normalValued() { + StepVerifier.create(Mono.just("foo").hide().singleOptional()) + .expectNext(Optional.of("foo")) + .verifyComplete(); + } + + @Test + public void normalError() { + StepVerifier.create(Mono.error(new IllegalStateException("failed")).hide().singleOptional()) + .expectErrorMessage("failed"); + } + + @Test + void fusionMonoSingleFusion() { + Mono> fusedCase = Mono.just(1).map(Function.identity()).singleOptional(); + + assertThat(fusedCase).as("fusedCase assembly check") + .isInstanceOf(MonoSingleOptional.class) + .isNotInstanceOf(Fuseable.class); + + assertThatCode(() -> fusedCase.filter(v -> true).block()).as("fusedCase fused") + .doesNotThrowAnyException(); + } + + @Test + public void scanOperator() { + MonoSingleOptional test = new MonoSingleOptional<>(Mono.just("foo")); + + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); + } + + @Test + public void scanSubscriber() { + CoreSubscriber> actual = new LambdaMonoSubscriber<>(null, e -> {}, null, null); + MonoSingleOptional.SingleOptionalSubscriber test = new MonoSingleOptional.SingleOptionalSubscriber<>( + actual); + Subscription parent = Operators.emptySubscription(); + test.onSubscribe(parent); + + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); + assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); + + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + test.onError(new IllegalStateException("boom")); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + test.cancel(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); + } +} From 89202c4158042f62324aa5b119a6a6eecd64b359 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Fri, 3 Feb 2023 14:26:32 +0200 Subject: [PATCH 091/312] Remove Traces$SharedSecretsCallSiteSupplierFactory from the native-image configuration (#3339) Traces$SharedSecretsCallSiteSupplierFactory is targeting JDK 8, it uses sun.misc.SharedSecrets which is unavailable on Java 9+ Current GraalVM 22.3.1 provides Java 11/17/19-based GraalVM https://www.graalvm.org/release-notes/22_3/ Fixes #3334 --- .../reactor-core/reflect-config.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/reactor-core/src/main/resources/META-INF/native-image/io.projectreactor/reactor-core/reflect-config.json b/reactor-core/src/main/resources/META-INF/native-image/io.projectreactor/reactor-core/reflect-config.json index 4104625b7f..1fe3a5fa00 100644 --- a/reactor-core/src/main/resources/META-INF/native-image/io.projectreactor/reactor-core/reflect-config.json +++ b/reactor-core/src/main/resources/META-INF/native-image/io.projectreactor/reactor-core/reflect-config.json @@ -11,18 +11,6 @@ } ] }, - { - "condition": { - "typeReachable": "reactor.core.publisher.Traces" - }, - "name": "reactor.core.publisher.Traces$SharedSecretsCallSiteSupplierFactory", - "methods": [ - { - "name": "", - "parameterTypes": [] - } - ] - }, { "condition": { "typeReachable": "reactor.core.publisher.Traces" From ecb6cd2209ec56a606850142cdca5c629b72efb1 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Fri, 3 Feb 2023 18:29:21 +0100 Subject: [PATCH 092/312] Avoid BlockHound NoSuchTypeException when context propagation dependency is unavailable (#3337) When the context-propagation jar is unavailable (which can happen since the dependency is optional), then BlockHound reports the following ERROR log: reactor.blockhound.shaded.net.bytebuddy.pool.TypePool$Resolution$NoSuchTypeException: Cannot resolve type description for io.micrometer.context.ContextRegistry This log does not break anything, it is only about logging, but it can be avoided using this patch. In a nutshell, Byte Buddy requires all type information for a transformed type to be available, else the above error log may be displayed. The log may happen occasionally with very rare byte code combinations when the class is being defined (initial classloading). To avoid this problem, the proposed patch is refactoring the ContextPropagation static initializer in order to not use a lambda when capturing the context snapshot. --- .../core/publisher/ContextPropagation.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 8d60d19df1..0f1432fcfd 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,9 +53,9 @@ final class ContextPropagation { Function contextCaptureFunction; boolean contextPropagation; try { + // The following line will throw a LinkageError (NoClassDefFoundError) in case context propagation is not available ContextRegistry globalRegistry = ContextRegistry.getInstance(); - contextCaptureFunction = target -> ContextSnapshot.captureAllUsing(PREDICATE_TRUE, globalRegistry) - .updateContext(target); + contextCaptureFunction = new ContextCaptureNoPredicate(globalRegistry); contextPropagation = true; } catch (LinkageError t) { @@ -280,4 +280,17 @@ public Context addToContext(Context originalContext) { } } } + + static final class ContextCaptureNoPredicate implements Function { + final ContextRegistry globalRegistry; + + ContextCaptureNoPredicate(ContextRegistry globalRegistry) { + this.globalRegistry = globalRegistry; + } + @Override + public Context apply(Context context) { + return ContextSnapshot.captureAllUsing(PREDICATE_TRUE, globalRegistry) + .updateContext(context); + } + } } From 3dbc0701a061f7e20e304e0475a5ddac1745bddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 6 Feb 2023 11:08:04 +0100 Subject: [PATCH 093/312] Upgrade Micrometer dependencies (#3344) - micrometer 1.10.2 -> 1.10.3 - micrometer-docs-generator 1.0.0 -> 1.0.1 - micrometer-tracing-integration-test 1.0.0 - 1.0.1 --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4bb8ac3ba2..285349b067 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.23" jmh = "1.35" junit = "5.9.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.2" +micrometer = "1.10.3" reactiveStreams = "1.0.4" [libraries] @@ -36,9 +36,9 @@ micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micro micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.0"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.0" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.1" micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.11.0" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 51991b81ac6a8f6388462f934b3a86d108712e32 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 13 Feb 2023 19:06:02 +0200 Subject: [PATCH 094/312] ensures exception is caught if inner fused for `FlatMap` (#3351) --- .../reactor/core/publisher/FluxFlatMap.java | 3 +- .../core/publisher/FluxFlatMapTest.java | 40 ++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFlatMap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFlatMap.java index f3959edd33..e14d6eb1fc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFlatMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFlatMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -416,6 +416,7 @@ else if (!delayError || !Exceptions.addThrowable(ERROR, this, e_)) { onError(Operators.onOperatorError(s, e_, t, ctx)); } Operators.onDiscard(t, ctx); + tryEmitScalar(null); return; } tryEmitScalar(v); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxFlatMapTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxFlatMapTest.java index 7176da5845..5997724024 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxFlatMapTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxFlatMapTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,6 @@ import reactor.core.TestLoggerExtension; import reactor.core.publisher.FluxPeekFuseableTest.AssertQueueSubscription; import reactor.core.scheduler.Schedulers; -import reactor.test.util.LoggerUtils; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; import reactor.test.subscriber.AssertSubscriber; @@ -1869,4 +1868,41 @@ public void noWrappingOfCheckedExceptions_hide() { .expectError(NoSuchMethodException.class) .verify(); } + + static class FluxFlatMapDelayError3336Test { + + @Test + void workingFlatMapDelayError() { + Flux.just(0, 1, 2, 3) + .flatMapDelayError(integer -> { + throw new RuntimeException(); // Cancels upstream subscription after consuming one event + }, 1, 1) + .as(StepVerifier::create) + .expectError() + .verify(Duration.ofSeconds(1)); // Completes as expected + } + + @Test + void hangingFlatMapDelayError() { + Flux.just(0, 1, 2, 3) + .flatMapDelayError(integer -> { + return Flux.error(new RuntimeException()); // Does not cancel upstream subscription + }, 1, 1) + .as(StepVerifier::create) + .expectError() + .verify(Duration.ofSeconds(1)); // Triggers timeout + } + + @Test + void deoptimizedFlatMapDelayError() { + Flux.just(0, 1, 2, 3) + .flatMapDelayError(integer -> { + return Flux.error(new RuntimeException()) + .hide(); // Does not cancel upstream subscription + }, 1, 1) + .as(StepVerifier::create) + .expectError() + .verify(Duration.ofSeconds(1)); // Completes after consuming all events + } + } } From 5d3a6b5450ae8ed48c2375dbf18095ea89e9bc38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 14 Feb 2023 10:25:49 +0100 Subject: [PATCH 095/312] Context propagation with automatic ThreadLocals restoration (#3335) Adds a few primitives necessary for propagating reactor `Context` to `ThreadLocal`s using the context-propagation library. It adds the following primitives: * dedicated `Scheduler` task wrapping * dedicated global `Queue` wrapper * dedicated alternative to `FluxContextWrite` and `MonoContextWrite` operators All of these use the context restoration mechanism from context-propagation. --- gradle/libs.versions.toml | 2 +- .../core/publisher/ContextPropagation.java | 244 +++++++++++++++++- .../java/reactor/core/publisher/Flux.java | 13 +- ...FluxContextWriteRestoringThreadLocals.java | 201 +++++++++++++++ .../java/reactor/core/publisher/Hooks.java | 49 +++- .../java/reactor/core/publisher/Mono.java | 10 + ...MonoContextWriteRestoringThreadLocals.java | 195 ++++++++++++++ .../reactor/ReactorTestExecutionListener.java | 4 +- .../publisher/ContextPropagationTest.java | 226 +++++++++++++++- ...ContextWriteRestoringThreadLocalsTest.java | 57 ++++ ...ContextWriteRestoringThreadLocalsTest.java | 34 +++ 11 files changed, 1021 insertions(+), 14 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsTest.java create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocalsTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 285349b067..17227ebb74 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ logback = "ch.qos.logback:logback-classic:1.2.11" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.0" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.2" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.1" diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 0f1432fcfd..83a0aa526d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -16,14 +16,22 @@ package reactor.core.publisher; +import java.util.AbstractQueue; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Queue; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; +import io.micrometer.context.ContextAccessor; import io.micrometer.context.ContextRegistry; import io.micrometer.context.ContextSnapshot; +import io.micrometer.context.ThreadLocalAccessor; import reactor.core.observability.SignalListener; import reactor.util.Logger; import reactor.util.Loggers; @@ -31,6 +39,8 @@ import reactor.util.context.Context; import reactor.util.context.ContextView; +import static reactor.core.Fuseable.QueueSubscription.NOT_SUPPORTED_MESSAGE; + /** * Utility private class to detect if the context-propagation library is on the classpath and to offer * ContextSnapshot support to {@link Flux} and {@link Mono}. @@ -42,11 +52,13 @@ final class ContextPropagation { static final Logger LOGGER; static final boolean isContextPropagationAvailable; + static boolean propagateContextToThreadLocals = false; static final Predicate PREDICATE_TRUE = v -> true; static final Function NO_OP = c -> c; static final Function WITH_GLOBAL_REGISTRY_NO_PREDICATE; + static { LOGGER = Loggers.getLogger(ContextPropagation.class); @@ -83,6 +95,17 @@ static boolean isContextPropagationAvailable() { return isContextPropagationAvailable; } + static boolean shouldPropagateContextToThreadLocals() { + return isContextPropagationAvailable && propagateContextToThreadLocals; + } + + public static Function scopePassingOnScheduleHook() { + return delegate -> { + ContextSnapshot contextSnapshot = ContextSnapshot.captureAll(); + return contextSnapshot.wrap(delegate); + }; + } + /** * Create a support function that takes a snapshot of thread locals and merges them with the * provided {@link Context}, resulting in a new {@link Context} which includes entries @@ -126,7 +149,7 @@ static Function contextCapture(Predicate captureKeyPre } static BiConsumer> contextRestoreForHandle(BiConsumer> handler, Supplier contextSupplier) { - if (!ContextPropagation.isContextPropagationAvailable()) { + if (propagateContextToThreadLocals || !ContextPropagation.isContextPropagationAvailable()) { return handler; } final Context ctx = contextSupplier.get(); @@ -141,7 +164,7 @@ static BiConsumer> contextRestoreForHandle(BiConsum } static SignalListener contextRestoreForTap(final SignalListener original, Supplier contextSupplier) { - if (!ContextPropagation.isContextPropagationAvailable()) { + if (propagateContextToThreadLocals || !ContextPropagation.isContextPropagationAvailable()) { return original; } final Context ctx = contextSupplier.get(); @@ -281,6 +304,151 @@ public Context addToContext(Context originalContext) { } } + static final class ContextQueue extends AbstractQueue { + + final Queue> envelopeQueue; + + boolean cleanOnNull; + boolean hasPrevious = false; + + Thread lastReader; + ContextSnapshot.Scope scope; + + @SuppressWarnings({"unchecked", "rawtypes"}) + ContextQueue(Queue queue) { + this.envelopeQueue = (Queue) queue; + } + + @Override + public int size() { + return envelopeQueue.size(); + } + + @Override + public boolean offer(T o) { + ContextSnapshot contextSnapshot = ContextSnapshot.captureAll(); + return envelopeQueue.offer(new Envelope<>(o, contextSnapshot)); + } + + @Override + public T poll() { + Envelope envelope = envelopeQueue.poll(); + if (envelope == null) { + if (cleanOnNull && scope != null) { + // clear thread-locals if they were just restored + scope.close(); + } + cleanOnNull = true; + lastReader = Thread.currentThread(); + hasPrevious = false; + return null; + } + + + restoreTheContext(envelope); + hasPrevious = true; + return envelope.body; + } + + private void restoreTheContext(Envelope envelope) { + ContextSnapshot contextSnapshot = envelope.contextSnapshot; + // tries to read existing Thread for existing ThreadLocals + ContextSnapshot currentContextSnapshot = ContextSnapshot.captureAll(); + if (!contextSnapshot.equals(currentContextSnapshot)) { + if (!hasPrevious || !Thread.currentThread().equals(this.lastReader)) { + // means context was restored form the envelope, + // thus it has to be cleared + cleanOnNull = true; + lastReader = Thread.currentThread(); + } + scope = contextSnapshot.setThreadLocals(); + } + else if (!hasPrevious || !Thread.currentThread().equals(this.lastReader)) { + // means same context was already available, no need to clean anything + cleanOnNull = false; + lastReader = Thread.currentThread(); + } + } + + @Override + @Nullable + public T peek() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean add(@Nullable T t) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public T remove() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public T element() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean contains(@Nullable Object o) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public T1[] toArray(T1[] a) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean remove(@Nullable Object o) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + } + } + + static class Envelope { + + final T body; + final ContextSnapshot contextSnapshot; + + Envelope(T body, ContextSnapshot contextSnapshot) { + this.body = body; + this.contextSnapshot = contextSnapshot; + } + + } + static final class ContextCaptureNoPredicate implements Function { final ContextRegistry globalRegistry; @@ -293,4 +461,76 @@ public Context apply(Context context) { .updateContext(context); } } + + /* + * Temporary methods not present in context-propagation library that allow + * clearing ThreadLocals not present in Reactor Context. Once context-propagation + * library adds the ability to do this, they can be removed from reactor-core. + */ + + @SuppressWarnings("unchecked") + static ContextSnapshot.Scope setThreadLocals(Object context) { + ContextRegistry registry = ContextRegistry.getInstance(); + ContextAccessor contextAccessor = registry.getContextAccessorForRead(context); + Map previousValues = null; + for (ThreadLocalAccessor threadLocalAccessor : registry.getThreadLocalAccessors()) { + Object key = threadLocalAccessor.key(); + Object value = ((ContextAccessor) contextAccessor).readValue((C) context, key); + previousValues = setThreadLocal(key, value, threadLocalAccessor, previousValues); + } + return ReactorScopeImpl.from(previousValues, registry); + } + + @SuppressWarnings("unchecked") + private static Map setThreadLocal(Object key, @Nullable V value, + ThreadLocalAccessor accessor, @Nullable Map previousValues) { + + previousValues = (previousValues != null ? previousValues : new HashMap<>()); + previousValues.put(key, accessor.getValue()); + if (value != null) { + ((ThreadLocalAccessor) accessor).setValue(value); + } + else { + accessor.reset(); + } + return previousValues; + } + + private static class ReactorScopeImpl implements ContextSnapshot.Scope { + + private final Map previousValues; + + private final ContextRegistry contextRegistry; + + private ReactorScopeImpl(Map previousValues, + ContextRegistry contextRegistry) { + this.previousValues = previousValues; + this.contextRegistry = contextRegistry; + } + + @Override + public void close() { + for (ThreadLocalAccessor accessor : this.contextRegistry.getThreadLocalAccessors()) { + if (this.previousValues.containsKey(accessor.key())) { + Object previousValue = this.previousValues.get(accessor.key()); + resetThreadLocalValue(accessor, previousValue); + } + } + } + + @SuppressWarnings("unchecked") + private void resetThreadLocalValue(ThreadLocalAccessor accessor, @Nullable V previousValue) { + if (previousValue != null) { + ((ThreadLocalAccessor) accessor).restore(previousValue); + } + else { + accessor.reset(); + } + } + + public static ContextSnapshot.Scope from(@Nullable Map previousValues, ContextRegistry registry) { + return (previousValues != null ? new ReactorScopeImpl(previousValues, registry) : () -> { + }); + } + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 43cdf159cf..5194b054e2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,7 @@ import java.util.function.Supplier; import java.util.logging.Level; import java.util.stream.Collector; +import java.util.stream.Collectors; import java.util.stream.Stream; import io.micrometer.core.instrument.MeterRegistry; @@ -4165,6 +4166,11 @@ public final Flux contextCapture() { if (!ContextPropagation.isContextPropagationAvailable()) { return this; } + if (ContextPropagation.propagateContextToThreadLocals) { + return onAssembly(new FluxContextWriteRestoringThreadLocals<>( + this, ContextPropagation.contextCapture() + )); + } return onAssembly(new FluxContextWrite<>(this, ContextPropagation.contextCapture())); } @@ -4211,6 +4217,11 @@ public final Flux contextWrite(ContextView contextToAppend) { * @see Context */ public final Flux contextWrite(Function contextModifier) { + if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + return onAssembly(new FluxContextWriteRestoringThreadLocals<>( + this, contextModifier + )); + } return onAssembly(new FluxContextWrite<>(this, contextModifier)); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java new file mode 100644 index 0000000000..33b5046fee --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.function.Function; + +import io.micrometer.context.ContextSnapshot; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +final class FluxContextWriteRestoringThreadLocals extends FluxOperator implements Fuseable { + + final Function doOnContext; + + FluxContextWriteRestoringThreadLocals(Flux source, + Function doOnContext) { + super(source); + this.doOnContext = Objects.requireNonNull(doOnContext, "doOnContext"); + } + + @SuppressWarnings("try") + @Override + public void subscribe(CoreSubscriber actual) { + Context c = doOnContext.apply(actual.currentContext()); + + try (ContextSnapshot.Scope ignored = ContextPropagation.setThreadLocals(c)) { + source.subscribe(new ContextWriteRestoringThreadLocalsSubscriber<>(actual, c)); + } + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + return super.scanUnsafe(key); + } + + static final class ContextWriteRestoringThreadLocalsSubscriber + implements ConditionalSubscriber, InnerOperator, + QueueSubscription { + + final CoreSubscriber actual; + final ConditionalSubscriber actualConditional; + final Context context; + + Subscription s; + + @SuppressWarnings("unchecked") + ContextWriteRestoringThreadLocalsSubscriber(CoreSubscriber actual, Context context) { + this.actual = actual; + this.context = context; + if (actual instanceof ConditionalSubscriber) { + this.actualConditional = (ConditionalSubscriber) actual; + } + else { + this.actualConditional = null; + } + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) { + return s; + } + if (key == Attr.RUN_STYLE) { + return Attr.RunStyle.SYNC; + } + return InnerOperator.super.scanUnsafe(key); + } + + @Override + public Context currentContext() { + return this.context; + } + + @SuppressWarnings("try") + @Override + public void onSubscribe(Subscription s) { + // This is needed, as the downstream can then switch threads, + // continue the subscription using different primitives and omit this operator + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (Operators.validate(this.s, s)) { + this.s = s; + actual.onSubscribe(this); + } + } + } + + @SuppressWarnings("try") + @Override + public void onNext(T t) { + // We probably ended up here from a request, which set thread locals to + // current context, but we need to clean up and restore thread locals for + // the actual subscriber downstream, as it can expect TLs to match the + // different context. + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onNext(t); + } + } + + @SuppressWarnings("try") + @Override + public boolean tryOnNext(T t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (actualConditional != null) { + return actualConditional.tryOnNext(t); + } + actual.onNext(t); + return true; + } + } + + @SuppressWarnings("try") + @Override + public void onError(Throwable t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onError(t); + } + } + + @SuppressWarnings("try") + @Override + public void onComplete() { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onComplete(); + } + } + + @Override + public CoreSubscriber actual() { + return actual; + } + + @SuppressWarnings("try") + @Override + public void request(long n) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(context)) { + s.request(n); + } + } + + @SuppressWarnings("try") + @Override + public void cancel() { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(context)) { + s.cancel(); + } + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + @Nullable + public T poll() { + throw new UnsupportedOperationException("Operator does not support fusion"); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException("Operator does not support fusion"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Operator does not support fusion"); + } + + @Override + public int size() { + throw new UnsupportedOperationException("Operator does not support fusion"); + } + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java index bbe4229479..e83f0c5915 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,12 @@ import reactor.core.Exceptions; import reactor.core.publisher.FluxOnAssembly.AssemblySnapshot; import reactor.core.publisher.FluxOnAssembly.MethodReturnSnapshot; +import reactor.core.scheduler.Schedulers; import reactor.util.Logger; import reactor.util.Loggers; import reactor.util.annotation.Nullable; import reactor.util.context.Context; +import reactor.util.context.ContextView; /** * A set of overridable lifecycle hooks that can be used for cross-cutting @@ -512,6 +514,51 @@ public static void disableContextLossTracking() { DETECT_CONTEXT_LOSS = false; } + private static final String CONTEXT_IN_THREAD_LOCALS_KEY = "CONTEXT_IN_THREAD_LOCALS"; + + /** + * Globally enables automatic context propagation to {@link ThreadLocal}s. + *

    + * It requires the + * context-propagation library + * to be on the classpath to have an effect. + * Using the implicit global {@code ContextRegistry} it reads entries present in + * the modified {@link Context} using + * {@link Flux#contextWrite(ContextView)} (or {@link Mono#contextWrite(ContextView)}) + * and {@link Flux#contextWrite(Function)} (or {@link Mono#contextWrite(Function)}) + * and restores all {@link ThreadLocal}s associated via same keys for which + * {@code ThreadLocalAccessor}s are registered. + *

    + * The {@link ThreadLocal}s are present in the upstream operators from the + * {@code contextWrite(...)} call and the unmodified (downstream) {@link Context} is + * used when signals are delivered downstream, making the {@code contextWrite(...)} + * a logical boundary for the context propagation mechanism. + */ + public static void enableAutomaticContextPropagation() { + if (ContextPropagation.isContextPropagationAvailable) { + Hooks.addQueueWrapper( + CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.ContextQueue::new + ); + Schedulers.onScheduleHook( + CONTEXT_IN_THREAD_LOCALS_KEY, + ContextPropagation.scopePassingOnScheduleHook() + ); + ContextPropagation.propagateContextToThreadLocals = true; + } + } + + /** + * Globally disables automatic context propagation to {@link ThreadLocal}s. + * @see #enableAutomaticContextPropagation() + */ + public static void disableAutomaticContextPropagation() { + if (ContextPropagation.isContextPropagationAvailable) { + Hooks.removeQueueWrapper(CONTEXT_IN_THREAD_LOCALS_KEY); + Schedulers.resetOnScheduleHook(CONTEXT_IN_THREAD_LOCALS_KEY); + ContextPropagation.propagateContextToThreadLocals = false; + } + } + @Nullable @SuppressWarnings("unchecked") static Function createOrUpdateOpHook(Collection, ? extends Publisher>> hooks) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index b715cef9de..e385004d6a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -2284,6 +2284,11 @@ public final Mono contextCapture() { if (!ContextPropagation.isContextPropagationAvailable()) { return this; } + if (ContextPropagation.propagateContextToThreadLocals) { + return onAssembly(new MonoContextWriteRestoringThreadLocals<>( + this, ContextPropagation.contextCapture() + )); + } return onAssembly(new MonoContextWrite<>(this, ContextPropagation.contextCapture())); } @@ -2330,6 +2335,11 @@ public final Mono contextWrite(ContextView contextToAppend) { * @see Context */ public final Mono contextWrite(Function contextModifier) { + if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + return onAssembly(new MonoContextWriteRestoringThreadLocals<>( + this, contextModifier + )); + } return onAssembly(new MonoContextWrite<>(this, contextModifier)); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java new file mode 100644 index 0000000000..5842865b8b --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.function.Function; + +import io.micrometer.context.ContextSnapshot; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +final class MonoContextWriteRestoringThreadLocals extends MonoOperator { + + final Function doOnContext; + + MonoContextWriteRestoringThreadLocals(Mono source, + Function doOnContext) { + super(source); + this.doOnContext = Objects.requireNonNull(doOnContext, "doOnContext"); + } + + @SuppressWarnings("try") + @Override + public void subscribe(CoreSubscriber actual) { + final Context c = doOnContext.apply(actual.currentContext()); + + try (ContextSnapshot.Scope ignored = ContextPropagation.setThreadLocals(c)) { + source.subscribe(new ContextWriteRestoringThreadLocalsSubscriber<>(actual, c)); + } + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + return super.scanUnsafe(key); + } + + static final class ContextWriteRestoringThreadLocalsSubscriber + implements InnerOperator, Fuseable.QueueSubscription { + + final CoreSubscriber actual; + final Context context; + + Subscription s; + boolean done; + + ContextWriteRestoringThreadLocalsSubscriber(CoreSubscriber actual, Context context) { + this.actual = actual; + this.context = context; + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) { + return s; + } + if (key == Attr.RUN_STYLE) { + return Attr.RunStyle.SYNC; + } + return InnerOperator.super.scanUnsafe(key); + } + + @Override + public Context currentContext() { + return this.context; + } + + @SuppressWarnings("try") + @Override + public void onSubscribe(Subscription s) { + // This is needed, as the downstream can then switch threads, + // continue the subscription using different primitives and omit this operator + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (Operators.validate(this.s, s)) { + this.s = s; + actual.onSubscribe(this); + } + } + } + + @SuppressWarnings("try") + @Override + public void onNext(T t) { + this.done = true; + // We probably ended up here from a request, which set thread locals to + // current context, but we need to clean up and restore thread locals for + // the actual subscriber downstream, as it can expect TLs to match the + // different context. + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onNext(t); + actual.onComplete(); + } + } + + @SuppressWarnings("try") + @Override + public void onError(Throwable t) { + if (this.done) { + Operators.onErrorDropped(t, context); + return; + } + + this.done = true; + + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onError(t); + } + } + + @SuppressWarnings("try") + @Override + public void onComplete() { + if (this.done) { + return; + } + + this.done = true; + + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onComplete(); + } + } + + @Override + public CoreSubscriber actual() { + return actual; + } + + @SuppressWarnings("try") + @Override + public void request(long n) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(context)) { + s.request(n); + } + } + + @SuppressWarnings("try") + @Override + public void cancel() { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(context)) { + s.cancel(); + } + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + @Nullable + public T poll() { + throw new UnsupportedOperationException("Operator does not support fusion"); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException("Operator does not support fusion"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Operator does not support fusion"); + } + + @Override + public int size() { + throw new UnsupportedOperationException("Operator does not support fusion"); + } + } +} diff --git a/reactor-core/src/test/java/reactor/ReactorTestExecutionListener.java b/reactor-core/src/test/java/reactor/ReactorTestExecutionListener.java index 32f7c850fd..734b527c8b 100644 --- a/reactor-core/src/test/java/reactor/ReactorTestExecutionListener.java +++ b/reactor-core/src/test/java/reactor/ReactorTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,8 @@ private static void resetHooksAndSchedulers() { Hooks.removeQueueWrappers(); + Hooks.disableAutomaticContextPropagation(); + Schedulers.resetOnHandleError(); Schedulers.resetFactory(); Schedulers.resetOnScheduleHooks(); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index 4b39fda7d7..a9d8b75e92 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,15 @@ package reactor.core.publisher; +import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; @@ -34,7 +39,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.reactivestreams.Publisher; - import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.core.Scannable; @@ -44,9 +48,11 @@ import reactor.core.publisher.FluxHandle.HandleSubscriber; import reactor.core.publisher.FluxHandleFuseable.HandleFuseableConditionalSubscriber; import reactor.core.publisher.FluxHandleFuseable.HandleFuseableSubscriber; +import reactor.core.scheduler.Schedulers; import reactor.test.ParameterizedTestWithName; import reactor.test.subscriber.TestSubscriber; import reactor.test.subscriber.TestSubscriberBuilder; +import reactor.util.concurrent.Queues; import reactor.util.context.Context; import reactor.util.context.ContextView; @@ -88,6 +94,187 @@ static void removeThreadLocalAccessors() { } + @Test + void threadLocalsPresentAfterSubscribeOn() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doOnNext(i -> tlValue.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentAfterPublishOn() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .publishOn(Schedulers.boundedElastic()) + .doOnNext(i -> tlValue.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInFlatMap() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .flatMap(i -> Mono.just(i) + .doOnNext(j -> tlValue.set(REF1.get()))) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentAfterDelay() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .delayElements(Duration.ofMillis(1)) + .doOnNext(i -> tlValue.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsRestoredAfterPollution() { + // this test validates Queue wrapping takes place + Hooks.enableAutomaticContextPropagation(); + ArrayBlockingQueue modifiedThreadLocals = new ArrayBlockingQueue<>(10); + ArrayBlockingQueue restoredThreadLocals = new ArrayBlockingQueue<>(10); + + Flux.range(0, 10) + .doOnNext(i -> { + REF1.set("i: " + i); + }) + .publishOn(Schedulers.parallel()) + // the validation below shows that modifications to TLs are propagated + // across thread boundaries via queue wrapping, so explicit control + // is required from users to clean up after such modifications + .doOnNext(i -> modifiedThreadLocals.add(REF1.get())) + .contextWrite(Function.identity()) + // the contextWrite above creates a barrier that ensures the downstream + // operator sees TLs from the subscriber context + .doOnNext(i -> restoredThreadLocals.add(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(modifiedThreadLocals).containsExactly( + "i: 0", "i: 1", "i: 2", "i: 3", "i: 4", + "i: 5", "i: 6", "i: 7", "i: 8", "i: 9" + ); + assertThat(restoredThreadLocals).containsExactly( + Collections.nCopies(10, "present").toArray(new String[] {}) + ); + } + + @Test + @SuppressWarnings("unchecked") + void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference requestTlValue = new AtomicReference<>(); + AtomicReference subscribeTlValue = new AtomicReference<>(); + AtomicReference firstNextTlValue = new AtomicReference<>(); + AtomicReference secondNextTlValue = new AtomicReference<>(); + AtomicReference cancelTlValue = new AtomicReference<>(); + + CountDownLatch itemDelivered = new CountDownLatch(1); + CountDownLatch cancelled = new CountDownLatch(1); + + TestSubscriber subscriber = + TestSubscriber.builder().initialRequest(1).build(); + + REF1.set("downstreamContext"); + + Flux.just(1, 2) + .hide() + .doOnRequest(r -> requestTlValue.set(REF1.get())) + .doOnNext(i -> firstNextTlValue.set(REF1.get())) + .doOnSubscribe(s -> subscribeTlValue.set(REF1.get())) + .doOnCancel(() -> { + cancelTlValue.set(REF1.get()); + cancelled.countDown(); + }) + .delayElements(Duration.ofMillis(1)) + .contextWrite(Context.of(KEY1, "upstreamContext")) + // disabling prefetching to observe cancellation + .publishOn(Schedulers.parallel(), 1) + .doOnNext(i -> { + System.out.println(REF1.get()); + secondNextTlValue.set(REF1.get()); + itemDelivered.countDown(); + }) + .subscribeOn(Schedulers.boundedElastic()) + .contextCapture() + .subscribe(subscriber); + + itemDelivered.await(); + + subscriber.cancel(); + + cancelled.await(); + + assertThat(requestTlValue.get()).isEqualTo("upstreamContext"); + assertThat(subscribeTlValue.get()).isEqualTo("upstreamContext"); + assertThat(firstNextTlValue.get()).isEqualTo("upstreamContext"); + assertThat(cancelTlValue.get()).isEqualTo("upstreamContext"); + assertThat(secondNextTlValue.get()).isEqualTo("downstreamContext"); + } + + @Test + void prefetchingShouldMaintainThreadLocals() { + Hooks.enableAutomaticContextPropagation(); + + // We validate streams of items above default prefetch size + // (max concurrency of flatMap == Queues.SMALL_BUFFER_SIZE == 256) + // are able to maintain the context propagation to ThreadLocals + // in the presence of prefetching + int size = Queues.SMALL_BUFFER_SIZE * 10; + + Flux source = Flux.create(s -> { + for (int i = 0; i < size; i++) { + s.next(i); + } + s.complete(); + }); + + assertThat(REF1.get()).isEqualTo("ref1_init"); + + ArrayBlockingQueue innerThreadLocals = new ArrayBlockingQueue<>(size); + ArrayBlockingQueue outerThreadLocals = new ArrayBlockingQueue<>(size); + + source.publishOn(Schedulers.boundedElastic()) + .flatMap(i -> Mono.just(i) + .delayElement(Duration.ofMillis(1)) + .doOnNext(j -> innerThreadLocals.add(REF1.get()))) + .contextWrite(ctx -> ctx.put(KEY1, "present")) + .publishOn(Schedulers.parallel()) + .doOnNext(i -> outerThreadLocals.add(REF1.get())) + .blockLast(); + + assertThat(innerThreadLocals).containsOnly("present").hasSize(size); + assertThat(outerThreadLocals).containsOnly("ref1_init").hasSize(size); + } + @Test void isContextPropagationAvailable() { assertThat(ContextPropagation.isContextPropagationAvailable()).isTrue(); @@ -135,6 +322,29 @@ void monoApiUsesContextPropagationConstantFunction() { ); } + @Test + void fluxApiUsesContextPropagationConstantFunctionWhenAutomaticPropagationEnabled() { + Hooks.enableAutomaticContextPropagation(); + Flux source = Flux.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(FluxContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("flux's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Test + void monoApiUsesContextPropagationConstantFunctionWhenAutomaticPropagationEnabled() { + Hooks.enableAutomaticContextPropagation(); + Mono source = Mono.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(MonoContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("mono's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE)); + } + @Nested class ContextCaptureFunctionTest { @@ -167,8 +377,8 @@ void captureWithFiltering() { ctx.forEach(asMap::put); //easier to assert assertThat(asMap) - .containsEntry(KEY2, "expected") - .hasSize(1); + .containsEntry(KEY2, "expected") + .hasSize(1); } } @@ -514,11 +724,11 @@ void monoHandleVariantsCallTheWrapper() { softly.assertThat(publisherFuseable.handler).as("publisherFuseable.handler").isSameAs(originalHandler); softly.assertThat(sub.handler) - .as("sub.handler") - .isNotSameAs(originalHandler); + .as("sub.handler") + .isNotSameAs(originalHandler); softly.assertThat(subFused.handler) - .as("subFused.handler") - .isNotSameAs(originalHandler); + .as("subFused.handler") + .isNotSameAs(originalHandler); }); } } diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsTest.java new file mode 100644 index 0000000000..3d7b2b4ab1 --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import org.junit.jupiter.api.Test; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Scannable; +import reactor.util.context.Context; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FluxContextWriteRestoringThreadLocalsTest { + + @Test + public void scanOperator(){ + Flux parent = Flux.just(1); + FluxContextWriteRestoringThreadLocals test = + new FluxContextWriteRestoringThreadLocals<>(parent, c -> c); + + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); + } + + @Test + public void scanSubscriber(){ + CoreSubscriber actual = new LambdaSubscriber<>(null, e -> {}, null, null); + FluxContextWriteRestoringThreadLocals + .ContextWriteRestoringThreadLocalsSubscriber test = + new FluxContextWriteRestoringThreadLocals + .ContextWriteRestoringThreadLocalsSubscriber<>( + actual, Context.empty() + ); + + Subscription parent = Operators.emptySubscription(); + test.onSubscribe(parent); + + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(parent); + assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); + } + +} \ No newline at end of file diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocalsTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocalsTest.java new file mode 100644 index 0000000000..3c52b09d22 --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocalsTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + + +import org.junit.jupiter.api.Test; +import reactor.core.Scannable; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MonoContextWriteRestoringThreadLocalsTest { + + @Test + public void scanOperator(){ + MonoContextWriteRestoringThreadLocals test = + new MonoContextWriteRestoringThreadLocals<>(Mono.just(1), c -> c); + + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.SYNC); + } +} \ No newline at end of file From 5311afa0ca80e2b140245263652921c5e30368bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 14 Feb 2023 11:46:11 +0100 Subject: [PATCH 096/312] [release] Prepare and release 3.5.3 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4675f1ae9a..b0c8fa3782 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.2" - testCompile "io.projectreactor:reactor-test:3.5.2" + compile "io.projectreactor:reactor-core:3.5.3" + testCompile "io.projectreactor:reactor-test:3.5.3" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.3-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.3-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.4-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.4-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.2" + // implementation "io.projectreactor:reactor-tools:3.5.3" } ``` diff --git a/gradle.properties b/gradle.properties index ba265b0734..4a099ef90e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.3-SNAPSHOT -bomVersion=2022.0.2 -metricsMicrometerVersion=1.0.3-SNAPSHOT +version=3.5.3 +bomVersion=2022.0.3 +metricsMicrometerVersion=1.0.3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17227ebb74..abe126d263 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ bytebuddy = "1.12.23" jmh = "1.35" junit = "5.9.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.3" +micrometer = "1.10.4" reactiveStreams = "1.0.4" [libraries] From d1c5c1c2a0e26cdec8b93d6088348978f63fc27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 14 Feb 2023 13:57:51 +0100 Subject: [PATCH 097/312] [release] Prepare and release 3.5.3 (follow-up) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index abe126d263..40c20bf95d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,7 +38,7 @@ micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.2" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.1" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.2" micrometer-test = { module = "io.micrometer:micrometer-test" } mockito = "org.mockito:mockito-core:4.11.0" reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 0ba9336793304465a0c6133ed7b5b403a04a2165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 14 Feb 2023 16:06:19 +0100 Subject: [PATCH 098/312] [release] Next development version 3.5.4-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 4a099ef90e..692afa20e7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.3 +version=3.5.4-SNAPSHOT bomVersion=2022.0.3 -metricsMicrometerVersion=1.0.3 +metricsMicrometerVersion=1.0.4-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 40c20bf95d..bd270ec4c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.2" -baselinePerfCore = "3.5.2" +baseline-core-api = "3.5.3" +baselinePerfCore = "3.5.3" baselinePerfExtra = "3.5.0" # Other shared versions From 342a1aa5b9eb4895af2bb5602722c79abc28f651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 28 Feb 2023 15:00:53 +0100 Subject: [PATCH 099/312] ContextQueue wrapper peek method implementation (#3368) Automatic Context Propagation to `ThreadLocals` uses `ContextQueue` to wrap instances returned by `Queues`. The `peek()` method does not throw `UnsupportedOperationException` any more, as it was an error. Still, `iterator()` throws `UnsupportedOperationException`, as well as `AbstractQueue` methods that rely on it. Fixes #3363. --- .../core/publisher/ContextPropagation.java | 63 ++----------------- .../publisher/ContextPropagationTest.java | 41 ++++++++++++ 2 files changed, 47 insertions(+), 57 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 83a0aa526d..d1a982afc8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -39,8 +39,6 @@ import reactor.util.context.Context; import reactor.util.context.ContextView; -import static reactor.core.Fuseable.QueueSubscription.NOT_SUPPORTED_MESSAGE; - /** * Utility private class to detect if the context-propagation library is on the classpath and to offer * ContextSnapshot support to {@link Flux} and {@link Mono}. @@ -306,6 +304,10 @@ public Context addToContext(Context originalContext) { static final class ContextQueue extends AbstractQueue { + static final String NOT_SUPPORTED_MESSAGE = "ContextQueue wrapper is intended " + + "for use with instances returned by Queues class. Iterator based " + + "methods are usually unsupported."; + final Queue> envelopeQueue; boolean cleanOnNull; @@ -373,27 +375,8 @@ else if (!hasPrevious || !Thread.currentThread().equals(this.lastReader)) { @Override @Nullable public T peek() { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public boolean add(@Nullable T t) { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public T remove() { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public T element() { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public boolean contains(@Nullable Object o) { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); + Envelope envelope = envelopeQueue.peek(); + return envelope == null ? null : envelope.body; } @Override @@ -401,40 +384,6 @@ public Iterator iterator() { throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); } - @Override - public Object[] toArray() { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public T1[] toArray(T1[] a) { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public boolean remove(@Nullable Object o) { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public boolean containsAll(Collection c) { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public boolean addAll(Collection c) { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public boolean removeAll(Collection c) { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } - - @Override - public boolean retainAll(Collection c) { - throw new UnsupportedOperationException(NOT_SUPPORTED_MESSAGE); - } } static class Envelope { diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index a9d8b75e92..e6a804b663 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -18,10 +18,12 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; @@ -57,6 +59,7 @@ import reactor.util.context.ContextView; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Simon Baslé @@ -275,6 +278,44 @@ void prefetchingShouldMaintainThreadLocals() { assertThat(outerThreadLocals).containsOnly("ref1_init").hasSize(size); } + @Test + void queueWrapperWorksWithQueues() { + Hooks.enableAutomaticContextPropagation(); + Queue queue = Queues.small() + .get(); + + assertThat(queue.offer("1")).isTrue(); + assertThat(queue.poll()).isSameAs("1"); + assertThat(queue.add("2")).isTrue(); + assertThat(queue.remove()).isSameAs("2"); + assertThat(queue.isEmpty()).isTrue(); + assertThat(queue.addAll(Arrays.asList("3", "4", "5"))).isTrue(); + assertThat(queue.peek()).isSameAs("3"); + assertThat(queue.isEmpty()).isFalse(); + assertThat(queue.element()).isSameAs("3"); + assertThat(queue.size()).isEqualTo(3); + queue.clear(); + assertThat(queue.offer("0")).isTrue(); + assertThat(queue.size()).isEqualTo(1); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(queue::iterator); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.contains("0")); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(queue::toArray); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.toArray(new Object[] {})); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.remove("0")); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.containsAll(Collections.singletonList("0"))); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.retainAll(Collections.singletonList("5"))); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.removeAll(Collections.singletonList("0"))); + } + @Test void isContextPropagationAvailable() { assertThat(ContextPropagation.isContextPropagationAvailable()).isTrue(); From d648d6b76305f28b40242ea9b6bf238df0a379fd Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Mon, 6 Mar 2023 18:04:02 +0200 Subject: [PATCH 100/312] Exclude ContextPropagation#isContextPropagationAvailable from the preprocessing for native-image support (#3374) Reflection is not used for identifying whether this library is available or not, thus this field can be excluded from the preprocessing for native-image support. Related to https://github.com/spring-projects/spring-framework/issues/30058 --- .../core/publisher/ContextPropagation.java | 16 ++++++++++------ .../main/java/reactor/core/publisher/Hooks.java | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index d1a982afc8..7875353166 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -49,7 +49,11 @@ final class ContextPropagation { static final Logger LOGGER; - static final boolean isContextPropagationAvailable; + // Note: If reflection is used for this field, then the name of the field should end with 'Available'. + // The preprocessing for native-image support is Spring Framework, and is a short term solution. + // The field should end with 'Available'. See org.springframework.aot.nativex.feature.PreComputeFieldFeature. + // Ultimately the long term solution should be provided by Reactor Core. + static final boolean isContextPropagationOnClasspath; static boolean propagateContextToThreadLocals = false; static final Predicate PREDICATE_TRUE = v -> true; @@ -80,7 +84,7 @@ final class ContextPropagation { " The feature is considered disabled due to this:", t); } - isContextPropagationAvailable = contextPropagation; + isContextPropagationOnClasspath = contextPropagation; WITH_GLOBAL_REGISTRY_NO_PREDICATE = contextCaptureFunction; } @@ -90,11 +94,11 @@ final class ContextPropagation { * @return true if context-propagation is available at runtime, false otherwise */ static boolean isContextPropagationAvailable() { - return isContextPropagationAvailable; + return isContextPropagationOnClasspath; } static boolean shouldPropagateContextToThreadLocals() { - return isContextPropagationAvailable && propagateContextToThreadLocals; + return isContextPropagationOnClasspath && propagateContextToThreadLocals; } public static Function scopePassingOnScheduleHook() { @@ -116,7 +120,7 @@ public static Function scopePassingOnScheduleHook() { * @return the {@link Context} augmented with captured entries */ static Function contextCapture() { - if (!isContextPropagationAvailable) { + if (!isContextPropagationOnClasspath) { return NO_OP; } return WITH_GLOBAL_REGISTRY_NO_PREDICATE; @@ -139,7 +143,7 @@ static Function contextCapture() { * @return a {@link Function} augmenting {@link Context} with captured entries */ static Function contextCapture(Predicate captureKeyPredicate) { - if (!isContextPropagationAvailable) { + if (!isContextPropagationOnClasspath) { return NO_OP; } return target -> ContextSnapshot.captureAllUsing(captureKeyPredicate, ContextRegistry.getInstance()) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java index e83f0c5915..1c256aea13 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java @@ -535,7 +535,7 @@ public static void disableContextLossTracking() { * a logical boundary for the context propagation mechanism. */ public static void enableAutomaticContextPropagation() { - if (ContextPropagation.isContextPropagationAvailable) { + if (ContextPropagation.isContextPropagationOnClasspath) { Hooks.addQueueWrapper( CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.ContextQueue::new ); @@ -552,7 +552,7 @@ public static void enableAutomaticContextPropagation() { * @see #enableAutomaticContextPropagation() */ public static void disableAutomaticContextPropagation() { - if (ContextPropagation.isContextPropagationAvailable) { + if (ContextPropagation.isContextPropagationOnClasspath) { Hooks.removeQueueWrapper(CONTEXT_IN_THREAD_LOCALS_KEY); Schedulers.resetOnScheduleHook(CONTEXT_IN_THREAD_LOCALS_KEY); ContextPropagation.propagateContextToThreadLocals = false; From dca7de2483977d9cfbd6086258fdfb027642f86d Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Wed, 8 Mar 2023 18:05:18 +0900 Subject: [PATCH 101/312] Polish comment on ContextPropagation.isContextPropagationOnClasspath (#3376) --- .../main/java/reactor/core/publisher/ContextPropagation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 7875353166..4c1f0f0633 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -50,7 +50,7 @@ final class ContextPropagation { static final Logger LOGGER; // Note: If reflection is used for this field, then the name of the field should end with 'Available'. - // The preprocessing for native-image support is Spring Framework, and is a short term solution. + // The preprocessing for native-image support is in Spring Framework, and is a short term solution. // The field should end with 'Available'. See org.springframework.aot.nativex.feature.PreComputeFieldFeature. // Ultimately the long term solution should be provided by Reactor Core. static final boolean isContextPropagationOnClasspath; From ebd7087b90be4b71c4916e77cd6e2181897f59e6 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 14 Mar 2023 13:17:23 +0200 Subject: [PATCH 102/312] [release] Prepare and release 3.5.4 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b0c8fa3782..f7ee9d23a9 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.3" - testCompile "io.projectreactor:reactor-test:3.5.3" + compile "io.projectreactor:reactor-core:3.5.4" + testCompile "io.projectreactor:reactor-test:3.5.4" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.4-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.4-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.5-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.5-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.3" + // implementation "io.projectreactor:reactor-tools:3.5.4" } ``` diff --git a/gradle.properties b/gradle.properties index 692afa20e7..0108f7cf15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.4-SNAPSHOT -bomVersion=2022.0.3 -metricsMicrometerVersion=1.0.4-SNAPSHOT +version=3.5.4 +bomVersion=2022.0.5 +metricsMicrometerVersion=1.0.4 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1555ccbc0..bf11fae7e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.0" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.4" +micrometer = "1.10.5" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,7 +27,7 @@ micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.2" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.2" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.3" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 44f7546b9de0c987c572795475b9055bb4756dd8 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 14 Mar 2023 15:33:08 +0200 Subject: [PATCH 103/312] [release] Next development version 3.5.5-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 0108f7cf15..e47d8d70eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.4 +version=3.5.5-SNAPSHOT bomVersion=2022.0.5 -metricsMicrometerVersion=1.0.4 +metricsMicrometerVersion=1.0.5-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf11fae7e0..1363f0bad1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.3" -baselinePerfCore = "3.5.3" +baseline-core-api = "3.5.4" +baselinePerfCore = "3.5.4" baselinePerfExtra = "3.5.0" # Other shared versions From 7a4378af6deeb321c5c9a1ca192c62d87fc8099f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 22 Mar 2023 09:04:57 +0000 Subject: [PATCH 104/312] Remove Fuseable interface from ContextWriteRestoringThreadLocals (#3409) `FluxContextWriteRestoringThreadLocals` and `MonoContextWriteRestoringThreadLocals` implement `Fuseable`, but their `Subscription` implementations don't support fusion. The `FluxContextWrite` and `MonoContextWrite` operators support fusion, but are not a hard `Thread` barrier, such as the propagating counterparts. Fusion is forbidden in the automatic propagation case as `ThreadLocal`s need to be set for every signal. This simplification is compatible, as the modified classes are not public. --- ...FluxContextWriteRestoringThreadLocals.java | 33 ++----------------- ...MonoContextWriteRestoringThreadLocals.java | 29 +--------------- 2 files changed, 4 insertions(+), 58 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java index 33b5046fee..415c852596 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java @@ -22,11 +22,11 @@ import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; -import reactor.core.Fuseable; +import reactor.core.Fuseable.ConditionalSubscriber; import reactor.util.annotation.Nullable; import reactor.util.context.Context; -final class FluxContextWriteRestoringThreadLocals extends FluxOperator implements Fuseable { +final class FluxContextWriteRestoringThreadLocals extends FluxOperator { final Function doOnContext; @@ -53,8 +53,7 @@ public Object scanUnsafe(Attr key) { } static final class ContextWriteRestoringThreadLocalsSubscriber - implements ConditionalSubscriber, InnerOperator, - QueueSubscription { + implements ConditionalSubscriber, InnerOperator { final CoreSubscriber actual; final ConditionalSubscriber actualConditional; @@ -171,31 +170,5 @@ public void cancel() { s.cancel(); } } - - @Override - public int requestFusion(int requestedMode) { - return Fuseable.NONE; - } - - @Override - @Nullable - public T poll() { - throw new UnsupportedOperationException("Operator does not support fusion"); - } - - @Override - public boolean isEmpty() { - throw new UnsupportedOperationException("Operator does not support fusion"); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Operator does not support fusion"); - } - - @Override - public int size() { - throw new UnsupportedOperationException("Operator does not support fusion"); - } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java index 5842865b8b..69c533c7f3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java @@ -22,7 +22,6 @@ import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; -import reactor.core.Fuseable; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -53,7 +52,7 @@ public Object scanUnsafe(Attr key) { } static final class ContextWriteRestoringThreadLocalsSubscriber - implements InnerOperator, Fuseable.QueueSubscription { + implements InnerOperator { final CoreSubscriber actual; final Context context; @@ -165,31 +164,5 @@ public void cancel() { s.cancel(); } } - - @Override - public int requestFusion(int requestedMode) { - return Fuseable.NONE; - } - - @Override - @Nullable - public T poll() { - throw new UnsupportedOperationException("Operator does not support fusion"); - } - - @Override - public boolean isEmpty() { - throw new UnsupportedOperationException("Operator does not support fusion"); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Operator does not support fusion"); - } - - @Override - public int size() { - throw new UnsupportedOperationException("Operator does not support fusion"); - } } } From 74c954e9bbe6e571c8834bc246e6498d0b23fd2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 24 Mar 2023 10:20:01 +0100 Subject: [PATCH 105/312] Propagate ThreadLocals in tap (#3405) Aside from `contextWrite()`, the `tap` operator also has write access to the `Context`. New classes have been added to handle the automatic context propagation: * `MonoTapRestoringThreadLocals`, * `FluxTapRestoringThreadLocals`. They replace the corresponding implementations of `tap` when `Hooks.enableAutomaticContextPropagation()` is called, and when the `context-propagation` library is on the class path. As a result of these changes, `FluxTapTest` was also moved to the `withMicrometerTest` source set, where the `context-propagation` library is present. Fixes #3395. --- .../MicrometerObservationIntegrationTest.java | 231 ++++++----- .../MicrometerObservationListenerTest.java | 178 +++++++-- .../core/publisher/ContextPropagation.java | 28 +- .../java/reactor/core/publisher/Flux.java | 3 + .../java/reactor/core/publisher/FluxTap.java | 4 +- .../core/publisher/FluxTapFuseable.java | 4 +- .../FluxTapRestoringThreadLocals.java | 362 ++++++++++++++++++ .../java/reactor/core/publisher/Mono.java | 3 + .../java/reactor/core/publisher/MonoTap.java | 4 +- .../core/publisher/MonoTapFuseable.java | 4 +- .../MonoTapRestoringThreadLocals.java | 89 +++++ .../reactor/core/publisher/FluxTapTest.java | 191 +++++++-- 12 files changed, 927 insertions(+), 174 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/MonoTapRestoringThreadLocals.java rename reactor-core/src/{test => withMicrometerTest}/java/reactor/core/publisher/FluxTapTest.java (85%) diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java index ecd6edd642..eab7efb989 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,25 @@ package reactor.core.observability.micrometer; -import java.time.Duration; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - import io.micrometer.tracing.Span; import io.micrometer.tracing.test.SampleTestRunner; import io.micrometer.tracing.test.simple.SpansAssert; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; - import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -43,79 +46,87 @@ * * @author Simon Baslé */ -@Tag("slow") -public class MicrometerObservationIntegrationTest extends SampleTestRunner { - - MicrometerObservationIntegrationTest() { - super(SampleTestRunner.SampleRunnerConfig.builder() - .build()); - } - - @Override - public SampleTestRunnerConsumer yourCode() throws Exception { - final Scheduler delayScheduler = Schedulers.newSingle("test"); - final IllegalStateException EXCEPTION = new IllegalStateException("expected error"); - return (bb, meterRegistry) -> { - Span beforeStart = bb.getTracer().currentSpan(); - - Function> querySimulator = id -> - Mono.delay(Duration.ofMillis(500), delayScheduler) - .tag("endpoint", "simulated/" + id) - .map(ignored -> "query for id " + id) - .doOnNext(v -> { - if (id == 2L) throw EXCEPTION; - }) - .name("query" + id) - .tap(Micrometer.observation(getObservationRegistry())); - - Flux.range(0, 100) - .name("testFlux") - .tag("interval", "500ms") - .take(3) - .tag("size", "3") - .concatMap(querySimulator) - .tap(Micrometer.observation(getObservationRegistry())) - .onErrorReturn("ended with error") // prevent error throwing. the tap should still get notified - .blockLast(); - - SpansAssert spansAssert = SpansAssert.assertThat(bb.getFinishedSpans()); - SpansAssert.SpansAssertReturningAssert assertThatMain = spansAssert.assertThatASpanWithNameEqualTo("test-flux"); - SpansAssert.SpansAssertReturningAssert assertThatQuery2 = spansAssert.assertThatASpanWithNameEqualTo("query2"); - - spansAssert.hasSize(4); - - assertThatMain - .hasTag("reactor.status", "error") - .hasTag("reactor.type", "Flux") - .hasTag("interval", "500ms") - .hasTag("size", "3") - //TODO propose new duration assertion? span's timestamps should return Instant, not long. OTel is using nanos, Brave is storing long microsecond -// .satisfies(span -> assertThat(Duration.ofNanos(span.getEndTimestamp() - span.getStartTimestamp())) -// .as("duration") -// .isGreaterThanOrEqualTo(Duration.ofMillis(1500)) -// ) - //OTEL doesn't really capture the exception type, only the message - .thenThrowable().hasMessage(EXCEPTION.getMessage()); - - //query2 span - assertThatQuery2 - .hasTag("endpoint", "simulated/2") - .thenThrowable().hasMessage(EXCEPTION.getMessage()); - - //quick assert query0 and query1 - spansAssert - .thenASpanWithNameEqualTo("query0") - .doesNotHaveEventWithNameEqualTo("exception") - .hasTag("endpoint", "simulated/0") - .backToSpans() - .hasASpanWithName("query1"); - - assertThat(bb.getTracer().currentSpan()) - .as("no leftover span in main thread") - .isNotSameAs(beforeStart) //something happened - .isEqualTo(beforeStart); //original span was restored - - //finally, assert that the delay thread was not polluted either +public class MicrometerObservationIntegrationTest { + + static class ActualRunner extends SampleTestRunner { + + private final boolean automatic; + + public ActualRunner(boolean automatic, SampleRunnerConfig sampleRunnerConfig) { + super(sampleRunnerConfig); + this.automatic = automatic; + } + + @Override + public SampleTestRunner.SampleTestRunnerConsumer yourCode() throws Exception { + if (this.automatic) { + Hooks.enableAutomaticContextPropagation(); + } + final ScheduledExecutorService delayExecutor = Executors.newSingleThreadScheduledExecutor(); + final Scheduler delayScheduler = Schedulers.fromExecutorService(delayExecutor, "test"); + + final IllegalStateException EXCEPTION = new IllegalStateException("expected error"); + return (bb, meterRegistry) -> { + Span beforeStart = bb.getTracer().currentSpan(); + + Function> querySimulator = id -> + Mono.delay(Duration.ofMillis(500), delayScheduler) + .tag("endpoint", "simulated/" + id) + .map(ignored -> "query for id " + id) + .doOnNext(v -> { + if (id == 2L) throw EXCEPTION; + }) + .name("query" + id) + .tap(Micrometer.observation(getObservationRegistry())); + + Flux.range(0, 100) + .name("testFlux") + .tag("interval", "500ms") + .take(3) + .tag("size", "3") + .concatMap(querySimulator) + .tap(Micrometer.observation(getObservationRegistry())) + .onErrorReturn("ended with error") // prevent error throwing. the tap should still get notified + .blockLast(); + + SpansAssert spansAssert = SpansAssert.assertThat(bb.getFinishedSpans()); + SpansAssert.SpansAssertReturningAssert assertThatMain = spansAssert.assertThatASpanWithNameEqualTo("test-flux"); + SpansAssert.SpansAssertReturningAssert assertThatQuery2 = spansAssert.assertThatASpanWithNameEqualTo("query2"); + + spansAssert.hasSize(4); + + assertThatMain + .hasTag("reactor.status", "error") + .hasTag("reactor.type", "Flux") + .hasTag("interval", "500ms") + .hasTag("size", "3") + //TODO propose new duration assertion? span's timestamps should return Instant, not long. OTel is using nanos, Brave is storing long microsecond +// .satisfies(span -> assertThat(Duration.ofNanos(span.getEndTimestamp() - span.getStartTimestamp())) +// .as("duration") +// .isGreaterThanOrEqualTo(Duration.ofMillis(1500)) +// ) + //OTEL doesn't really capture the exception type, only the message + .thenThrowable().hasMessage(EXCEPTION.getMessage()); + + //query2 span + assertThatQuery2 + .hasTag("endpoint", "simulated/2") + .thenThrowable().hasMessage(EXCEPTION.getMessage()); + + //quick assert query0 and query1 + spansAssert + .thenASpanWithNameEqualTo("query0") + .doesNotHaveEventWithNameEqualTo("exception") + .hasTag("endpoint", "simulated/0") + .backToSpans() + .hasASpanWithName("query1"); + + assertThat(bb.getTracer().currentSpan()) + .as("no leftover span in main thread") + .isNotSameAs(beforeStart) //something happened + .isEqualTo(beforeStart); //original span was restored + + //finally, assert that the delay thread was not polluted either /* Impl note: This assertion is a bit redundant since we don't use Scope anyway so there shouldn't be any possibility of polluting ThreadLocals. It used to fail for Brave because Brave defaults to @@ -125,21 +136,45 @@ public SampleTestRunnerConsumer yourCode() throws Exception { by ensuring only the Span capture is done in separate thread (the assertion has to be done in main testing thread). */ - String notCaptured = "tracer.currentSpan() not invoked"; - AtomicReference delaySpanRef = new AtomicReference<>(notCaptured); - CountDownLatch latch = new CountDownLatch(1); - delayScheduler.schedule(() -> { - try { - delaySpanRef.set(bb.getTracer().currentSpan()); - } - finally { - latch.countDown(); - } - }); - latch.await(10, TimeUnit.SECONDS); - assertThat(delaySpanRef.get()) - .as("no leftover span in delay thread") - .isNull(); - }; - } + String notCaptured = "tracer.currentSpan() not invoked"; + AtomicReference delaySpanRef = new AtomicReference<>(notCaptured); + CountDownLatch latch = new CountDownLatch(1); + + // Instead of using the delayScheduler, we run the check directly on the delayExecutor. + // That is because we have a span in scope, and in case of automatic context propagation, + // the span is restored when the provided Runnable is run. + delayExecutor.execute(() -> { + try { + delaySpanRef.set(bb.getTracer().currentSpan()); + } finally { + latch.countDown(); + } + }); + latch.await(10, TimeUnit.SECONDS); + assertThat(delaySpanRef.get()) + .as("no leftover span in delay thread") + .isNull(); + + delayExecutor.shutdownNow(); + }; + } + } + + @Tag("slow") + @Nested + class PlainTest extends ActualRunner { + PlainTest() { + super(false, + SampleTestRunner.SampleRunnerConfig.builder().build()); + } + } + + @Tag("slow") + @Nested + class AutomaticTest extends ActualRunner { + AutomaticTest() { + super(true, + SampleTestRunner.SampleRunnerConfig.builder().build()); + } + } } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java index 8f8778420a..ee86e1c245 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package reactor.core.observability.micrometer; +import java.time.Duration; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Clock; @@ -24,10 +26,12 @@ import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import io.micrometer.observation.tck.TestObservationRegistry; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; +import reactor.test.ParameterizedTestWithName; import reactor.util.context.Context; import reactor.util.context.ContextView; @@ -68,8 +72,12 @@ public long monotonicTime() { subscriberContext = Context.of("contextKey", "contextValue"); } - @Test - void whenStartedFluxWithDefaultName() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void whenStartedFluxWithDefaultName(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) @@ -97,8 +105,12 @@ void whenStartedFluxWithDefaultName() { .hasLowCardinalityKeyValue("testTag2", "testTagValue2"); } - @Test - void whenStartedFluxWithCustomName() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void whenStartedFluxWithCustomName(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( "testName", //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) @@ -127,8 +139,12 @@ void whenStartedFluxWithCustomName() { .hasLowCardinalityKeyValue("testTag2", "testTagValue2"); } - @Test - void whenStartedMono() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void whenStartedMono(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromMono (which is tested separately) @@ -157,8 +173,12 @@ void whenStartedMono() { .hasLowCardinalityKeyValue("testTag2", "testTagValue2"); } - @Test - void tapFromFluxWithTags() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void tapFromFluxWithTags(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } Flux flux = Flux.just(1) .name("testFlux") .tag("testTag1", "testTagValue1") @@ -183,8 +203,12 @@ void tapFromFluxWithTags() { .hasKeyValuesCount(4); } - @Test - void tapFromMonoWithTags() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void tapFromMonoWithTags(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } Mono mono = Mono.just(1) .name("testMono") .tag("testTag1", "testTagValue1") @@ -209,8 +233,12 @@ void tapFromMonoWithTags() { .hasKeyValuesCount(4); } - @Test - void observationStoppedByCancellation() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationStoppedByCancellation(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( "flux", KeyValues.of("forcedType", "Flux"), @@ -234,8 +262,12 @@ void observationStoppedByCancellation() { .doesNotHaveError(); } - @Test - void observationStoppedByCompleteEmpty() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationStoppedByCompleteEmpty(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( "emptyFlux", KeyValues.of("forcedType", "Flux"), @@ -259,8 +291,12 @@ void observationStoppedByCompleteEmpty() { .doesNotHaveError(); } - @Test - void observationStoppedByCompleteWithValues() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationStoppedByCompleteWithValues(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( "flux", KeyValues.of("forcedType", "Flux"), @@ -285,8 +321,12 @@ void observationStoppedByCompleteWithValues() { .doesNotHaveError(); } - @Test - void observationMonoStoppedByOnNext() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationMonoStoppedByOnNext(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( "valuedMono", KeyValues.of("forcedType", "Mono"), @@ -320,8 +360,12 @@ void observationMonoStoppedByOnNext() { .hasLowCardinalityKeyValue("reactor.status", expectedStatus); } - @Test - void observationEmptyMonoStoppedByOnComplete() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationEmptyMonoStoppedByOnComplete(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( "emptyMono", KeyValues.of("forcedType", "Mono"), @@ -344,8 +388,12 @@ void observationEmptyMonoStoppedByOnComplete() { .doesNotHaveError(); } - @Test - void observationStoppedByError() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationStoppedByError(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( "errorFlux", KeyValues.of("forcedType", "Flux"), @@ -370,8 +418,12 @@ void observationStoppedByError() { .hasError(exception); } - @Test - void observationGetsParentFromContext() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationGetsParentFromContext(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) @@ -423,8 +475,70 @@ void observationGetsParentFromContext() { .satisfies(r -> assertThat(r.getCurrentObservationScope()).as("no leftover currentObservationScope()").isNull()); } - @Test - void observationWithEmptyContextHasNoParent() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationHierarchyCreatedInMonoCase(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } + Observation parent = Observation.start("testParent", registry); + Context contextWithAParent = Context.of(subscriberContext).put(ObservationThreadLocalAccessor.KEY, parent); + + AtomicReference inner = new AtomicReference<>(); + AtomicReference outer = new AtomicReference<>(); + + Mono.just("hello") + .delayElement(Duration.ofMillis(1)) + .handle((v, s) -> { + inner.set(registry.getCurrentObservation()); + s.next(v); + }) + .tap(Micrometer.observation(registry)) + .handle((v, s) -> { + outer.set(registry.getCurrentObservation()); + s.next(v); + }) + .contextWrite(contextWithAParent) + .block(); + + assertThat(inner.get().getContext().getParentObservation()).isEqualTo(outer.get()); + } + + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationHierarchyCreatedInFluxCase(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } + Observation parent = Observation.start("testParent", registry); + Context contextWithAParent = Context.of(subscriberContext).put(ObservationThreadLocalAccessor.KEY, parent); + + AtomicReference inner = new AtomicReference<>(); + AtomicReference outer = new AtomicReference<>(); + + Flux.just("hello") + .delayElements(Duration.ofMillis(1)) + .handle((v, s) -> { + inner.set(registry.getCurrentObservation()); + s.next(v); + }) + .tap(Micrometer.observation(registry)) + .handle((v, s) -> { + outer.set(registry.getCurrentObservation()); + s.next(v); + }) + .contextWrite(contextWithAParent) + .blockLast(); + + assertThat(inner.get().getContext().getParentObservation()).isEqualTo(outer.get()); + } + + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationWithEmptyContextHasNoParent(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) @@ -464,8 +578,12 @@ void observationWithEmptyContextHasNoParent() { .satisfies(r -> assertThat(r.getCurrentObservationScope()).as("no leftover currentObservationScope()").isNull()); } - @Test - void observationWithEmptyContextHasParentWhenExternalScopeOpened() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void observationWithEmptyContextHasParentWhenExternalScopeOpened(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } configuration = new MicrometerObservationListenerConfiguration( MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 4c1f0f0633..8b45999c4f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -150,6 +150,17 @@ static Function contextCapture(Predicate captureKeyPre .updateContext(target); } + /** + * When context-propagation library + * is available on the classpath, the provided {@link BiConsumer handler} will be + * called with {@link ThreadLocal} values restored from the provided {@link Context}. + * @param handler user provided handler + * @param contextSupplier supplies the potentially modified {@link Context} to + * restore {@link ThreadLocal} values from + * @return potentially wrapped {@link BiConsumer} or the original + * @param type of handled values + * @param the transformed type + */ static BiConsumer> contextRestoreForHandle(BiConsumer> handler, Supplier contextSupplier) { if (propagateContextToThreadLocals || !ContextPropagation.isContextPropagationAvailable()) { return handler; @@ -165,8 +176,23 @@ static BiConsumer> contextRestoreForHandle(BiConsum }; } + /** + * When context-propagation library + * is available on the classpath, the provided {@link SignalListener} will be wrapped + * with another one that restores {@link ThreadLocal} values from the provided + * {@link Context}. + *

    Note, this is only applied to {@link FluxTap}, {@link FluxTapFuseable}, + * {@link MonoTap}, and {@link MonoTapFuseable}. The automatic propagation + * variants: {@link FluxTapRestoringThreadLocals} and + * {@link MonoTapRestoringThreadLocals} do not use this method. + * @param original the original {@link SignalListener} from the user + * @param contextSupplier supplies the potentially modified {@link Context} to + * restore {@link ThreadLocal} values from + * @return potentially wrapped {@link SignalListener} or the original + * @param type of handled values + */ static SignalListener contextRestoreForTap(final SignalListener original, Supplier contextSupplier) { - if (propagateContextToThreadLocals || !ContextPropagation.isContextPropagationAvailable()) { + if (!ContextPropagation.isContextPropagationAvailable()) { return original; } final Context ctx = contextSupplier.get(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 5194b054e2..24213b6a78 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -9249,6 +9249,9 @@ public SignalListener createListener(Publisher ignored1, Context * @see #tap(Function) */ public final Flux tap(SignalListenerFactory listenerFactory) { + if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + return onAssembly(new FluxTapRestoringThreadLocals<>(this, listenerFactory)); + } if (this instanceof Fuseable) { return onAssembly(new FluxTapFuseable<>(this, listenerFactory)); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java index 044dbc91fe..01ac70bc85 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ final class FluxTap extends InternalFluxOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) throws Throwable { - //if the SequenceObserver cannot be created, all we can do is error the subscriber. + //if the SignalListener cannot be created, all we can do is error the subscriber. //after it is created, in case doFirst fails we can additionally try to invoke doFinally. //note that if the later handler also fails, then that exception is thrown. SignalListener signalListener; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java index ae30d0d02e..2d1aa17376 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ final class FluxTapFuseable extends InternalFluxOperator impleme @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) throws Throwable { - //if the SequenceObserver cannot be created, all we can do is error the subscriber. + //if the SignalListener cannot be created, all we can do is error the subscriber. //after it is created, in case doFirst fails we can additionally try to invoke doFinally. //note that if the later handler also fails, then that exception is thrown. SignalListener signalListener; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java new file mode 100644 index 0000000000..24c34f4596 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import io.micrometer.context.ContextSnapshot; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Fuseable.ConditionalSubscriber; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +/** + * A generic per-Subscription side effect {@link Flux} that notifies a {@link SignalListener} of most events. + * + * @author Simon Baslé + */ +final class FluxTapRestoringThreadLocals extends FluxOperator { + + final SignalListenerFactory tapFactory; + final STATE commonTapState; + + FluxTapRestoringThreadLocals(Flux source, SignalListenerFactory tapFactory) { + super(source); + this.tapFactory = tapFactory; + this.commonTapState = tapFactory.initializePublisherState(source); + } + + @Override + public void subscribe(CoreSubscriber actual) { + //if the SignalListener cannot be created, all we can do is error the subscriber. + //after it is created, in case doFirst fails we can additionally try to invoke doFinally. + //note that if the later handler also fails, then that exception is thrown. + SignalListener signalListener; + try { + //TODO replace currentContext() with contextView() when available + signalListener = tapFactory.createListener(source, actual.currentContext().readOnly(), commonTapState); + } + catch (Throwable generatorError) { + Operators.error(actual, generatorError); + return; + } + + try { + signalListener.doFirst(); + } + catch (Throwable listenerError) { + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return; + } + + // invoked AFTER doFirst + Context alteredContext; + try { + alteredContext = signalListener.addToContext(actual.currentContext()); + } + catch (Throwable e) { + signalListener.handleListenerError(new IllegalStateException("Unable to augment tap Context at construction via addToContext", e)); + alteredContext = actual.currentContext(); + } + + try (ContextSnapshot.Scope ignored = ContextPropagation.setThreadLocals(alteredContext)) { + source.subscribe(new TapSubscriber<>(actual, signalListener, alteredContext)); + } + } + + @Nullable + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return super.scanUnsafe(key); + } + + //TODO support onErrorContinue around listener errors + static class TapSubscriber implements ConditionalSubscriber, InnerOperator { + + final CoreSubscriber actual; + final ConditionalSubscriber actualConditional; + final Context context; + final SignalListener listener; + + boolean done; + Subscription s; + + TapSubscriber(CoreSubscriber actual, SignalListener signalListener, Context ctx) { + this.actual = actual; + this.listener = signalListener; + this.context = ctx; + if (actual instanceof ConditionalSubscriber) { + this.actualConditional = (ConditionalSubscriber) actual; + } else { + this.actualConditional = null; + } + } + + @Override + public CoreSubscriber actual() { + return this.actual; + } + + @Override + public Context currentContext() { + return this.context; + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return s; + if (key == Attr.TERMINATED) return done; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return InnerOperator.super.scanUnsafe(key); + } + + /** + * Cancel the prepared subscription, pass the listener error to {@link SignalListener#handleListenerError(Throwable)} + * and then terminate the downstream directly with same error (without invoking any other handler). + * + * @param listenerError the exception thrown from a handler method before the subscription was set + * @param toCancel the {@link Subscription} that was prepared but not sent downstream + */ + protected void handleListenerErrorPreSubscription(Throwable listenerError, Subscription toCancel) { + toCancel.cancel(); + listener.handleListenerError(listenerError); + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + Operators.error(actual, listenerError); + } + } + + /** + * Cancel the active subscription, pass the listener error to {@link SignalListener#handleListenerError(Throwable)} + * and then terminate the downstream directly with same error (without invoking any other handler). + * + * @param listenerError the exception thrown from a handler method + */ + protected void handleListenerErrorAndTerminate(Throwable listenerError) { + s.cancel(); + listener.handleListenerError(listenerError); + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onError(listenerError); //TODO hooks ? + } + } + + /** + * Cancel the active subscription, pass the listener error to {@link SignalListener#handleListenerError(Throwable)}, + * combine it with the original error and then terminate the downstream directly this combined exception + * (without invoking any other handler). + * + * @param listenerError the exception thrown from a handler method + * @param originalError the exception that was about to occur when handler was invoked + */ + protected void handleListenerErrorMultipleAndTerminate(Throwable listenerError, Throwable originalError) { + s.cancel(); + listener.handleListenerError(listenerError); + RuntimeException multiple = Exceptions.multiple(listenerError, originalError); + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onError(multiple); //TODO hooks ? + } + } + + /** + * After the downstream is considered terminated (or cancelled), pass the listener error to + * {@link SignalListener#handleListenerError(Throwable)} then drop that error. + * + * @param listenerError the exception thrown from a handler method happening after sequence termination + */ + protected void handleListenerErrorPostTermination(Throwable listenerError) { + listener.handleListenerError(listenerError); + Operators.onErrorDropped(listenerError, actual.currentContext()); + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + + try { + listener.doOnSubscription(); + } catch (Throwable observerError) { + handleListenerErrorPreSubscription(observerError, s); + return; + } + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onSubscribe(this); + } + } + } + + @Override + public void onNext(T t) { + if (done) { + try { + listener.doOnMalformedOnNext(t); + } catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } finally { + Operators.onNextDropped(t, currentContext()); + } + return; + } + try { + listener.doOnNext(t); + } catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); + return; + } + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onNext(t); + } + } + + @Override + public boolean tryOnNext(T t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (actualConditional != null) { + if (!actualConditional.tryOnNext(t)) { + return false; + } + } else { + actual.onNext(t); + } + } + try { + listener.doOnNext(t); + } catch (Throwable listenerError) { + handleListenerErrorAndTerminate(listenerError); + } + return true; + } + + @Override + public void onError(Throwable t) { + if (done) { + try { + listener.doOnMalformedOnError(t); + } catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } finally { + Operators.onErrorDropped(t, currentContext()); + } + return; + } + done = true; + + try { + listener.doOnError(t); + } catch (Throwable observerError) { + //any error in the hooks interrupts other hooks, including doFinally + handleListenerErrorMultipleAndTerminate(observerError, t); + return; + } + + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onError(t); //RS: onError MUST terminate normally and not throw + } + + try { + listener.doAfterError(t); + listener.doFinally(SignalType.ON_ERROR); + } catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } + } + + @Override + public void onComplete() { + if (done) { + try { + listener.doOnMalformedOnComplete(); + } catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } + return; + } + done = true; + + try { + listener.doOnComplete(); + } catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); + return; + } + + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onComplete(); //RS: onComplete MUST terminate normally and not throw + } + + try { + listener.doAfterComplete(); + listener.doFinally(SignalType.ON_COMPLETE); + } catch (Throwable observerError) { + handleListenerErrorPostTermination(observerError); + } + } + + @Override + public void request(long n) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(this.context)) { + if (Operators.validate(n)) { + try { + listener.doOnRequest(n); + } catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); + return; + } + s.request(n); + } + } + } + + @Override + public void cancel() { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(this.context)) { + try { + listener.doOnCancel(); + } catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); + return; + } + + try { + s.cancel(); + } finally { + try { + listener.doFinally(SignalType.CANCEL); + } catch (Throwable observerError) { + handleListenerErrorAndTerminate(observerError); //redundant s.cancel + } + } + } + } + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index e385004d6a..c3411ad710 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -4740,6 +4740,9 @@ public SignalListener createListener(Publisher ignored1, Context * @see #tap(Function) */ public final Mono tap(SignalListenerFactory listenerFactory) { + if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + return onAssembly(new MonoTapRestoringThreadLocals<>(this, listenerFactory)); + } if (this instanceof Fuseable) { return onAssembly(new MonoTapFuseable<>(this, listenerFactory)); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java index 58edad7417..c5ebb81978 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ final class MonoTap extends InternalMonoOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) throws Throwable { - //if the SequenceObserver cannot be created, all we can do is error the subscriber. + //if the SignalListener cannot be created, all we can do is error the subscriber. //after it is created, in case doFirst fails we can additionally try to invoke doFinally. //note that if the later handler also fails, then that exception is thrown. SignalListener signalListener; diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java index be1efb4ae1..59c598f595 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ final class MonoTapFuseable extends InternalMonoOperator impleme @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) throws Throwable { - //if the SequenceObserver cannot be created, all we can do is error the subscriber. + //if the SignalListener cannot be created, all we can do is error the subscriber. //after it is created, in case doFirst fails we can additionally try to invoke doFinally. //note that if the later handler also fails, then that exception is thrown. SignalListener signalListener; diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapRestoringThreadLocals.java new file mode 100644 index 0000000000..14b329c99b --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapRestoringThreadLocals.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import io.micrometer.context.ContextSnapshot; +import reactor.core.CoreSubscriber; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +/** + * A generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. + * + * @author Simon Baslé + */ +final class MonoTapRestoringThreadLocals extends MonoOperator { + + final SignalListenerFactory tapFactory; + final STATE commonTapState; + + MonoTapRestoringThreadLocals(Mono source, SignalListenerFactory tapFactory) { + super(source); + this.tapFactory = tapFactory; + this.commonTapState = tapFactory.initializePublisherState(source); + } + + @Override + public void subscribe(CoreSubscriber actual) { + //if the SignalListener cannot be created, all we can do is error the subscriber. + //after it is created, in case doFirst fails we can additionally try to invoke doFinally. + //note that if the later handler also fails, then that exception is thrown. + SignalListener signalListener; + try { + //TODO replace currentContext() with contextView() when available + signalListener = tapFactory.createListener(source, actual.currentContext().readOnly(), commonTapState); + } + catch (Throwable generatorError) { + Operators.error(actual, generatorError); + return; + } + + try { + signalListener.doFirst(); + } + catch (Throwable listenerError) { + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return; + } + + // invoked AFTER doFirst + Context alteredContext; + try { + alteredContext = signalListener.addToContext(actual.currentContext()); + } + catch (Throwable e) { + signalListener.handleListenerError(new IllegalStateException("Unable to augment tap Context at construction via addToContext", e)); + alteredContext = actual.currentContext(); + } + + try (ContextSnapshot.Scope ignored = ContextPropagation.setThreadLocals(alteredContext)) { + source.subscribe(new FluxTapRestoringThreadLocals.TapSubscriber<>(actual, signalListener, alteredContext)); + } + } + + @Nullable + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PREFETCH) return -1; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return super.scanUnsafe(key); + } +} diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxTapTest.java similarity index 85% rename from reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java rename to reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxTapTest.java index 394fe97264..6b7f1fae37 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxTapTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxTapTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; @@ -35,6 +36,7 @@ import reactor.test.ParameterizedTestWithName; import reactor.test.publisher.TestPublisher; import reactor.test.subscriber.TestSubscriber; +import reactor.util.context.Context; import reactor.util.context.ContextView; import reactor.core.observability.SignalListener; import reactor.core.observability.SignalListenerFactory; @@ -162,8 +164,12 @@ public SignalListener createListener(Publisher source, ContextVi } } - @Test - void scenarioTerminatingOnComplete() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void scenarioTerminatingOnComplete(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } TestSignalListener testSignalListener = new TestSignalListener<>(); Flux fullFlux = Flux.just(1, 2, 3).hide(); @@ -187,8 +193,12 @@ void scenarioTerminatingOnComplete() { ); } - @Test - void scenarioTerminatingOnError() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void scenarioTerminatingOnError(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } TestSignalListener testSignalListener = new TestSignalListener<>(); RuntimeException expectedError = new RuntimeException("expected"); @@ -213,8 +223,12 @@ void scenarioTerminatingOnError() { ); } - @Test - void multipleRequests() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void multipleRequests(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } TestSignalListener testSignalListener = new TestSignalListener<>(); TestSubscriber testSubscriber = TestSubscriber.builder().initialRequest(0L).build(); @@ -268,8 +282,12 @@ void multipleRequests() { ); } - @Test - void withCancellation() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void withCancellation(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } TestSignalListener testSignalListener = new TestSignalListener<>(); TestSubscriber testSubscriber = TestSubscriber.builder().initialRequest(0L).build(); @@ -308,8 +326,16 @@ void withCancellation() { } @ParameterizedTestWithName - @ValueSource(strings = {"ON_COMPLETE", "ON_ERROR"}) - void malformedOnNext(SignalType termination) { + @CsvSource({ + "ON_COMPLETE, true", + "ON_COMPLETE, false", + "ON_ERROR, true", + "ON_ERROR, false", + }) + void malformedOnNext(SignalType termination, boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } AtomicReference dropped = new AtomicReference<>(); Hooks.onNextDropped(dropped::set); @@ -367,8 +393,16 @@ void malformedOnNext(SignalType termination) { } @ParameterizedTestWithName - @ValueSource(strings = {"ON_COMPLETE", "ON_ERROR"}) - void malformedOnComplete(SignalType termination) { + @CsvSource({ + "ON_COMPLETE, true", + "ON_COMPLETE, false", + "ON_ERROR, true", + "ON_ERROR, false", + }) + void malformedOnComplete(SignalType termination, boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } TestSignalListener testSignalListener = new TestSignalListener<>(); TestPublisher testPublisher = TestPublisher.createNoncompliant(TestPublisher.Violation.CLEANUP_ON_TERMINATE); TestSubscriber ignored = TestSubscriber.create(); @@ -421,8 +455,16 @@ void malformedOnComplete(SignalType termination) { } @ParameterizedTestWithName - @ValueSource(strings = {"ON_COMPLETE", "ON_ERROR"}) - void malformedOnError(SignalType termination) { + @CsvSource({ + "ON_COMPLETE, true", + "ON_COMPLETE, false", + "ON_ERROR, true", + "ON_ERROR, false", + }) + void malformedOnError(SignalType termination, boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } AtomicReference dropped = new AtomicReference<>(); Hooks.onErrorDropped(dropped::set); @@ -479,26 +521,36 @@ void malformedOnError(SignalType termination) { assertThat(dropped).as("malformed error was dropped").hasValue(malformedError); } - - @Test - void throwingCreateListener() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void throwingCreateListener(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } TestSubscriber testSubscriber = TestSubscriber.create(); - FluxTap test = new FluxTap<>(Flux.just(1), - new SignalListenerFactory() { - @Override - public Void initializePublisherState(Publisher source) { - return null; - } + SignalListenerFactory listenerFactory = new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } - @Override - public SignalListener createListener(Publisher source, - ContextView listenerContext, Void publisherContext) { - throw new IllegalStateException("expected"); - } - }); + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + throw new IllegalStateException("expected"); + } + }; - assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) - .doesNotThrowAnyException(); + if (automatic) { + FluxTapRestoringThreadLocals test = + new FluxTapRestoringThreadLocals<>(Flux.just(1), listenerFactory); + assertThatCode(() -> test.subscribe(testSubscriber)) + .doesNotThrowAnyException(); + } else { + FluxTap test = new FluxTap<>(Flux.just(1), listenerFactory); + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + } assertThat(testSubscriber.expectTerminalError()) .as("downstream error") @@ -506,8 +558,12 @@ public SignalListener createListener(Publisher sourc .hasMessage("expected"); } - @Test - void doFirstListenerError() { + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void doFirstListenerError(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } Throwable listenerError = new IllegalStateException("expected from doFirst"); TestSubscriber testSubscriber = TestSubscriber.create(); @@ -518,10 +574,18 @@ public void doFirst() throws Throwable { } }; - FluxTap test = new FluxTap<>(Flux.just(1), factoryOf(listener)); + if (automatic) { + FluxTapRestoringThreadLocals test = + new FluxTapRestoringThreadLocals<>(Flux.just(1), factoryOf(listener)); - assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) - .doesNotThrowAnyException(); + assertThatCode(() -> test.subscribe(testSubscriber)) + .doesNotThrowAnyException(); + } else { + FluxTap test = new FluxTap<>(Flux.just(1), factoryOf(listener)); + + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + } assertThat(listener.listenerErrors) .as("listenerErrors") @@ -897,6 +961,21 @@ void scanFluxTap() { assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(source); } + @Test + void scanFluxTapRestoringThreadLocals() { + Flux source = Flux.just(1); + FluxTapRestoringThreadLocals testPublisher = + new FluxTapRestoringThreadLocals<>(source, ignoredFactory()); + + Scannable test = Scannable.from(testPublisher); + assertThat(test).isSameAs(testPublisher) + .matches(Scannable::isScanAvailable, "isScanAvailable"); + + assertThat(test.scan(Attr.PREFETCH)).as("PREFETCH").isEqualTo(-1); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isEqualTo(RunStyle.SYNC); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(source); + } + @Test void scanFluxTapFuseable() { Flux source = Flux.just(1); @@ -925,6 +1004,21 @@ void scanMonoListen() { assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(source); } + @Test + void scanMonoListenRestoringThreadLocals() { + Mono source = Mono.just(1); + MonoTapRestoringThreadLocals testPublisher = + new MonoTapRestoringThreadLocals<>(source, ignoredFactory()); + + Scannable test = Scannable.from(testPublisher); + assertThat(test).isSameAs(testPublisher) + .matches(Scannable::isScanAvailable, "isScanAvailable"); + + assertThat(test.scan(Attr.PREFETCH)).as("PREFETCH").isEqualTo(-1); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isEqualTo(RunStyle.SYNC); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(source); + } + @Test void scanMonoListenFuseable() { Mono source = Mono.just(1); @@ -961,6 +1055,29 @@ void scanListenSubscriber() { assertThat(test.scan(Attr.TERMINATED)).as("TERMINATED").isTrue(); } + @Test + void scanListenSubscriberRestoringThreadLocals() { + CoreSubscriber actual = Operators.drainSubscriber(); + Subscription subscription = Operators.emptySubscription(); + + FluxTapRestoringThreadLocals.TapSubscriber subscriber = + new FluxTapRestoringThreadLocals.TapSubscriber<>(actual, + new TestSignalListener<>(), Context.empty()); + + subscriber.onSubscribe(subscription); + + Scannable test = Scannable.from(subscriber); + assertThat(test.isScanAvailable()).as("isScanAvailable").isTrue(); + assertThat(test).isSameAs(subscriber); + + assertThat(test.scanUnsafe(Attr.ACTUAL)).as("ACTUAL").isSameAs(actual); + assertThat(test.scanUnsafe(Attr.PARENT)).as("PARENT").isSameAs(subscription); + assertThat(test.scan(Attr.RUN_STYLE)).as("RUN_STYLE").isSameAs(RunStyle.SYNC); + + subscriber.onComplete(); + assertThat(test.scan(Attr.TERMINATED)).as("TERMINATED").isTrue(); + } + @Test void scanListenConditionalSubscriber() { ConditionalSubscriber actual = Operators.toConditionalSubscriber(Operators.drainSubscriber()); From 28ae2a0c91d69ed27f96d06609cd0a39d205fd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 3 Apr 2023 16:49:08 +0200 Subject: [PATCH 106/312] Propagate ThreadLocals for non-Reactor upstream sources (#3418) Factory methods for creating `Flux` and `Mono` from non-Reactor sources now restore `ThreadLocal` values when `Hooks.enableAutomaticContextPropagation()`was called in the following cases: * `Flux.from(Publisher)` * `Mono.from(Publisher)` * `Mono.fromDirect(Publisher)` * `Mono.fromFuture(CompletableFuture)` * `Mono.fromCompletionStage(CompletionStage)` and relevant overrides. Fixes #3366. --- .../reactor/core/publisher/FluxSource.java | 102 ++++++++- .../core/publisher/MonoCompletionStage.java | 112 ++++++++-- .../core/publisher/MonoFromPublisher.java | 6 +- .../reactor/core/publisher/MonoSource.java | 100 ++++++++- .../publisher/ContextPropagationTest.java | 209 ++++++++++++++++++ 5 files changed, 504 insertions(+), 25 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java index 73e285895d..3957cf5cf2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,16 @@ import java.util.Objects; +import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; import reactor.core.CorePublisher; import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.util.annotation.Nullable; +import reactor.util.context.Context; /** * A connecting {@link Flux} Publisher (right-to-left from a composition chain perspective) @@ -64,7 +68,11 @@ final class FluxSource extends Flux implements SourceProducer, @Override @SuppressWarnings("unchecked") public void subscribe(CoreSubscriber actual) { - source.subscribe(actual); + if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + source.subscribe(new FluxSourceRestoringThreadLocalsSubscriber<>(actual)); + } else { + source.subscribe(actual); + } } @Override @@ -91,4 +99,94 @@ public Object scanUnsafe(Attr key) { return null; } + static final class FluxSourceRestoringThreadLocalsSubscriber + implements Fuseable.ConditionalSubscriber, InnerConsumer { + + final CoreSubscriber actual; + final Fuseable.ConditionalSubscriber actualConditional; + + Subscription s; + + @SuppressWarnings("unchecked") + FluxSourceRestoringThreadLocalsSubscriber(CoreSubscriber actual) { + this.actual = actual; + if (actual instanceof Fuseable.ConditionalSubscriber) { + this.actualConditional = (Fuseable.ConditionalSubscriber) actual; + } + else { + this.actualConditional = null; + } + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) { + return s; + } + if (key == Attr.RUN_STYLE) { + return Attr.RunStyle.SYNC; + } + if (key == Attr.ACTUAL) { + return actual; + } + return null; + } + + @Override + public Context currentContext() { + return actual.currentContext(); + } + + @SuppressWarnings("try") + @Override + public void onSubscribe(Subscription s) { + // This is needed, as the downstream can then switch threads, + // continue the subscription using different primitives and omit this operator + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onSubscribe(s); + } + } + + @SuppressWarnings("try") + @Override + public void onNext(T t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onNext(t); + } + } + + @SuppressWarnings("try") + @Override + public boolean tryOnNext(T t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (actualConditional != null) { + return actualConditional.tryOnNext(t); + } + actual.onNext(t); + return true; + } + } + + @SuppressWarnings("try") + @Override + public void onError(Throwable t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onError(t); + } + } + + @SuppressWarnings("try") + @Override + public void onComplete() { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onComplete(); + } + } + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java index 0e5a092aac..04d93b45c8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,9 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.function.BiFunction; +import io.micrometer.context.ContextSnapshot; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; -import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -40,7 +40,7 @@ * @param the value type */ final class MonoCompletionStage extends Mono - implements Fuseable, Scannable { + implements Scannable { final CompletionStage future; final boolean suppressCancellation; @@ -52,7 +52,14 @@ final class MonoCompletionStage extends Mono @Override public void subscribe(CoreSubscriber actual) { - actual.onSubscribe(new MonoCompletionStageSubscription<>(actual, future, suppressCancellation)); + if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + actual.onSubscribe( + new MonoCompletionStageRestoringThreadLocalsSubscription<>( + actual, future, suppressCancellation)); + } else { + actual.onSubscribe(new MonoCompletionStageSubscription<>( + actual, future, suppressCancellation)); + } } @Override @@ -62,8 +69,6 @@ public Object scanUnsafe(Attr key) { } static class MonoCompletionStageSubscription implements InnerProducer, - Fuseable, - QueueSubscription, BiFunction { final CoreSubscriber actual; @@ -154,29 +159,104 @@ public void cancel() { ((Future) future).cancel(true); } } + } - @Override - public int requestFusion(int requestedMode) { - return NONE; + static class MonoCompletionStageRestoringThreadLocalsSubscription + implements InnerProducer, BiFunction { + + final CoreSubscriber actual; + final CompletionStage future; + final boolean suppressCancellation; + + volatile int requestedOnce; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater REQUESTED_ONCE = + AtomicIntegerFieldUpdater.newUpdater(MonoCompletionStageRestoringThreadLocalsSubscription.class, "requestedOnce"); + + volatile boolean cancelled; + + MonoCompletionStageRestoringThreadLocalsSubscription( + CoreSubscriber actual, + CompletionStage future, + boolean suppressCancellation) { + this.actual = actual; + this.future = future; + this.suppressCancellation = suppressCancellation; } @Override - public T poll() { - return null; + public CoreSubscriber actual() { + return this.actual; } @Override - public int size() { - return 0; + public Void apply(@Nullable T value, @Nullable Throwable e) { + final CoreSubscriber actual = this.actual; + + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (this.cancelled) { + //nobody is interested in the Mono anymore, don't risk dropping errors + final Context ctx = actual.currentContext(); + if (e == null || e instanceof CancellationException) { + //we discard any potential value and ignore Future cancellations + Operators.onDiscard(value, ctx); + } + else { + //we make sure we keep _some_ track of a Future failure AFTER the Mono cancellation + Operators.onErrorDropped(e, ctx); + //and we discard any potential value just in case both e and v are not null + Operators.onDiscard(value, ctx); + } + + return null; + } + + try { + if (e instanceof CompletionException) { + actual.onError(e.getCause()); + } + else if (e != null) { + actual.onError(e); + } + else if (value != null) { + actual.onNext(value); + actual.onComplete(); + } + else { + actual.onComplete(); + } + } + catch (Throwable e1) { + Operators.onErrorDropped(e1, actual.currentContext()); + throw Exceptions.bubble(e1); + } + return null; + } } @Override - public boolean isEmpty() { - return true; + public void request(long n) { + if (this.cancelled) { + return; + } + + if (this.requestedOnce == 1 || !REQUESTED_ONCE.compareAndSet(this, 0 , 1)) { + return; + } + + future.handle(this); } @Override - public void clear() { + public void cancel() { + this.cancelled = true; + + final CompletionStage future = this.future; + if (!suppressCancellation && future instanceof Future) { + //noinspection unchecked + ((Future) future).cancel(true); + } } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java index ac3e95ce5e..3ac5db298e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +55,10 @@ final class MonoFromPublisher extends Mono implements Scannable, @Override @SuppressWarnings("unchecked") public void subscribe(CoreSubscriber actual) { + if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + actual = new MonoSource.MonoSourceRestoringThreadLocalsSubscriber<>(actual); + } + try { CoreSubscriber subscriber = subscribeOrReturn(actual); if (subscriber == null) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java index 7aacd3d0ea..716e518480 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,15 @@ import java.util.Objects; +import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; import reactor.core.CorePublisher; import reactor.core.CoreSubscriber; import reactor.core.Scannable; import reactor.util.annotation.Nullable; - -import static reactor.core.Scannable.Attr.RUN_STYLE; -import static reactor.core.Scannable.Attr.RunStyle.SYNC; +import reactor.util.context.Context; /** * A decorating {@link Mono} {@link Publisher} that exposes {@link Mono} API over an arbitrary {@link Publisher} @@ -64,9 +64,12 @@ final class MonoSource extends Mono implements Scannable, SourceProducer actual) { - source.subscribe(actual); + if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + source.subscribe(new MonoSourceRestoringThreadLocalsSubscriber<>(actual)); + } else { + source.subscribe(actual); + } } @Override @@ -96,4 +99,89 @@ public Object scanUnsafe(Attr key) { return null; } + static final class MonoSourceRestoringThreadLocalsSubscriber + implements InnerConsumer { + + final CoreSubscriber actual; + + Subscription s; + boolean done; + + MonoSourceRestoringThreadLocalsSubscriber(CoreSubscriber actual) { + this.actual = actual; + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) { + return s; + } + if (key == Attr.RUN_STYLE) { + return Attr.RunStyle.SYNC; + } + if (key == Attr.ACTUAL) { + return actual; + } + return null; + } + + @Override + public Context currentContext() { + return actual.currentContext(); + } + + @SuppressWarnings("try") + @Override + public void onSubscribe(Subscription s) { + // This is needed, as the downstream can then switch threads, + // continue the subscription using different primitives and omit this operator + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onSubscribe(s); + } + } + + @SuppressWarnings("try") + @Override + public void onNext(T t) { + this.done = true; + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onNext(t); + actual.onComplete(); + } + } + + @SuppressWarnings("try") + @Override + public void onError(Throwable t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (this.done) { + Operators.onErrorDropped(t, actual.currentContext()); + return; + } + + this.done = true; + + actual.onError(t); + } + } + + @SuppressWarnings("try") + @Override + public void onComplete() { + if (this.done) { + return; + } + + this.done = true; + + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onComplete(); + } + } + } } diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index e6a804b663..5d9f7e1e83 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -25,7 +25,14 @@ import java.util.Map; import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; @@ -40,7 +47,9 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; +import org.reactivestreams.FlowAdapters; import org.reactivestreams.Publisher; +import reactor.adapter.JdkFlowAdapter; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.core.Scannable; @@ -52,6 +61,7 @@ import reactor.core.publisher.FluxHandleFuseable.HandleFuseableSubscriber; import reactor.core.scheduler.Schedulers; import reactor.test.ParameterizedTestWithName; +import reactor.test.publisher.TestPublisher; import reactor.test.subscriber.TestSubscriber; import reactor.test.subscriber.TestSubscriberBuilder; import reactor.util.concurrent.Queues; @@ -423,6 +433,205 @@ void captureWithFiltering() { } } + @Nested + class NonReactorSources { + @Test + void fluxFromPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Hooks.enableAutomaticContextPropagation(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Flux.from(nonReactorPublisher) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Hooks.enableAutomaticContextPropagation(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.from(nonReactorPublisher) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromPublisherIgnoringContract() + throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Hooks.enableAutomaticContextPropagation(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.fromDirect(nonReactorPublisher) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromCompletionStage() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + Hooks.enableAutomaticContextPropagation(); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(); + + // we need to delay delivery to ensure the completion signal is delivered + // on a Thread from executorService + CompletionStage completionStage = CompletableFuture.supplyAsync(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + return "test"; + }, executorService); + + TestSubscriber testSubscriber = TestSubscriber.create(); + + Mono.fromCompletionStage(completionStage) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(testSubscriber); + + latch.countDown(); + testSubscriber.block(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromFuture() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + Hooks.enableAutomaticContextPropagation(); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(); + + // we need to delay delivery to ensure the completion signal is delivered + // on a Thread from executorService + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + return "test"; + }, executorService); + + TestSubscriber testSubscriber = TestSubscriber.create(); + + Mono.fromFuture(future) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(testSubscriber); + + latch.countDown(); + testSubscriber.block(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + } + + // TAP AND HANDLE OPERATOR TESTS + static private enum Cases { NORMAL_NO_CONTEXT(false, false, false), NORMAL_WITH_CONTEXT(false, false, true), From bc38129a449107e4e9affa51d30a4dfa50000dab Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 10 Apr 2023 15:27:11 +0300 Subject: [PATCH 107/312] removes jcstress tests from publish workflow (#3429) --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a0ee221b1e..f626c458e5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,7 +51,7 @@ jobs: id: slowerTests uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2 with: - arguments: reactor-core:test -Pjunit-tags=slow jcstress + arguments: reactor-core:test -Pjunit-tags=slow #deploy the snapshot artifacts to Artifactory deploySnapshot: From d31f1bc36210485b61de21fb2684a7b3d8801efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 10 Apr 2023 16:41:50 +0200 Subject: [PATCH 108/312] ensures SignalListener#addToContext exceptions are handled (#3415) --- .../java/reactor/core/publisher/FluxTap.java | 34 ++-- .../core/publisher/FluxTapFuseable.java | 29 ++- .../FluxTapRestoringThreadLocals.java | 7 +- .../java/reactor/core/publisher/MonoTap.java | 18 +- .../core/publisher/MonoTapFuseable.java | 19 +- .../reactor/core/publisher/FluxTapTest.java | 186 +++++++++++++++++- 6 files changed, 261 insertions(+), 32 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java index 01ac70bc85..1da50b0cf5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java @@ -69,11 +69,24 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act return null; } + // Invoked AFTER doFirst + Context ctx; + try { + ctx = signalListener.addToContext(actual.currentContext()); + } + catch (Throwable e) { + IllegalStateException listenerError = new IllegalStateException( + "Unable to augment tap Context at subscription via addToContext", e); + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return null; + } + if (actual instanceof ConditionalSubscriber) { //noinspection unchecked - return new TapConditionalSubscriber<>((ConditionalSubscriber) actual, signalListener); + return new TapConditionalSubscriber<>((ConditionalSubscriber) actual, signalListener, ctx); } - return new TapSubscriber<>(actual, signalListener); + return new TapSubscriber<>(actual, signalListener, ctx); } @Nullable @@ -94,18 +107,10 @@ static class TapSubscriber implements InnerOperator { boolean done; Subscription s; - TapSubscriber(CoreSubscriber actual, SignalListener signalListener) { + TapSubscriber(CoreSubscriber actual, + SignalListener signalListener, Context ctx) { this.actual = actual; this.listener = signalListener; - //note that since we're in the subscriber, this is technically invoked AFTER doFirst - Context ctx; - try { - ctx = signalListener.addToContext(actual.currentContext()); - } - catch (Throwable e) { - signalListener.handleListenerError(new IllegalStateException("Unable to augment tap Context at construction via addToContext", e)); - ctx = actual.currentContext(); - } this.context = ctx; } @@ -330,8 +335,9 @@ static final class TapConditionalSubscriber extends TapSubscriber implemen final ConditionalSubscriber actualConditional; - public TapConditionalSubscriber(ConditionalSubscriber actual, SignalListener signalListener) { - super(actual, signalListener); + public TapConditionalSubscriber(ConditionalSubscriber actual, + SignalListener signalListener, Context ctx) { + super(actual, signalListener, ctx); this.actualConditional = actual; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java index 2d1aa17376..efe3821a97 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java @@ -24,6 +24,7 @@ import reactor.core.observability.SignalListener; import reactor.core.observability.SignalListenerFactory; import reactor.util.annotation.Nullable; +import reactor.util.context.Context; /** * A {@link reactor.core.Fuseable} generic per-Subscription side effect {@link Flux} that notifies a @@ -69,11 +70,25 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act return null; } + // Invoked AFTER doFirst + Context ctx; + try { + ctx = signalListener.addToContext(actual.currentContext()); + } + catch (Throwable e) { + IllegalStateException listenerError = new IllegalStateException( + "Unable to augment tap Context at subscription via addToContext", e); + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return null; + } + if (actual instanceof ConditionalSubscriber) { //noinspection unchecked - return new TapConditionalFuseableSubscriber<>((ConditionalSubscriber) actual, signalListener); + return new TapConditionalFuseableSubscriber<>( + (ConditionalSubscriber) actual, signalListener, ctx); } - return new TapFuseableSubscriber<>(actual, signalListener); + return new TapFuseableSubscriber<>(actual, signalListener, ctx); } @Nullable @@ -90,8 +105,9 @@ static class TapFuseableSubscriber extends FluxTap.TapSubscriber implement int mode; QueueSubscription qs; - TapFuseableSubscriber(CoreSubscriber actual, SignalListener signalListener) { - super(actual, signalListener); + TapFuseableSubscriber(CoreSubscriber actual, + SignalListener signalListener, Context ctx) { + super(actual, signalListener, ctx); } /** @@ -268,8 +284,9 @@ static final class TapConditionalFuseableSubscriber extends TapFuseableSubscr final ConditionalSubscriber actualConditional; - public TapConditionalFuseableSubscriber(ConditionalSubscriber actual, SignalListener signalListener) { - super(actual, signalListener); + public TapConditionalFuseableSubscriber(ConditionalSubscriber actual, + SignalListener signalListener, Context ctx) { + super(actual, signalListener, ctx); this.actualConditional = actual; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java index 24c34f4596..21d90be019 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java @@ -72,8 +72,11 @@ public void subscribe(CoreSubscriber actual) { alteredContext = signalListener.addToContext(actual.currentContext()); } catch (Throwable e) { - signalListener.handleListenerError(new IllegalStateException("Unable to augment tap Context at construction via addToContext", e)); - alteredContext = actual.currentContext(); + IllegalStateException listenerError = new IllegalStateException( + "Unable to augment tap Context at subscription via addToContext", e); + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return; } try (ContextSnapshot.Scope ignored = ContextPropagation.setThreadLocals(alteredContext)) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java index c5ebb81978..ba72852296 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java @@ -22,6 +22,7 @@ import reactor.core.observability.SignalListenerFactory; import reactor.core.publisher.FluxTap.TapSubscriber; import reactor.util.annotation.Nullable; +import reactor.util.context.Context; /** * A generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. @@ -66,11 +67,24 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act return null; } + // Invoked AFTER doFirst + Context ctx; + try { + ctx = signalListener.addToContext(actual.currentContext()); + } + catch (Throwable e) { + IllegalStateException listenerError = new IllegalStateException( + "Unable to augment tap Context at subscription via addToContext", e); + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return null; + } + if (actual instanceof Fuseable.ConditionalSubscriber) { //noinspection unchecked - return new FluxTap.TapConditionalSubscriber<>((Fuseable.ConditionalSubscriber) actual, signalListener); + return new FluxTap.TapConditionalSubscriber<>((Fuseable.ConditionalSubscriber) actual, signalListener, ctx); } - return new TapSubscriber<>(actual, signalListener); + return new TapSubscriber<>(actual, signalListener, ctx); } @Nullable diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java index 59c598f595..ce83cb4f25 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java @@ -21,6 +21,7 @@ import reactor.core.observability.SignalListener; import reactor.core.observability.SignalListenerFactory; import reactor.util.annotation.Nullable; +import reactor.util.context.Context; /** * A {@link Fuseable} generic per-Subscription side effect {@link Mono} that notifies a {@link SignalListener} of most events. @@ -65,11 +66,25 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act return null; } + // Invoked AFTER doFirst + Context ctx; + try { + ctx = signalListener.addToContext(actual.currentContext()); + } + catch (Throwable e) { + IllegalStateException listenerError = new IllegalStateException( + "Unable to augment tap Context at subscription via addToContext", e); + signalListener.handleListenerError(listenerError); + Operators.error(actual, listenerError); + return null; + } + if (actual instanceof ConditionalSubscriber) { //noinspection unchecked - return new FluxTapFuseable.TapConditionalFuseableSubscriber<>((ConditionalSubscriber) actual, signalListener); + return new FluxTapFuseable.TapConditionalFuseableSubscriber<>( + (ConditionalSubscriber) actual, signalListener, ctx); } - return new FluxTapFuseable.TapFuseableSubscriber<>(actual, signalListener); + return new FluxTapFuseable.TapFuseableSubscriber<>(actual, signalListener, ctx); } @Nullable diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxTapTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxTapTest.java index 6b7f1fae37..034c5a6f75 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxTapTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxTapTest.java @@ -558,6 +558,57 @@ public SignalListener createListener(Publisher sourc .hasMessage("expected"); } + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void throwingAlterContext(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener testSignalListener = + new TestSignalListener() { + @Override + public Context addToContext(Context originalContext) { + throw new IllegalStateException("expected"); + } + }; + + if (automatic) { + FluxTapRestoringThreadLocals test = + new FluxTapRestoringThreadLocals<>(Flux.just(1), factoryOf(testSignalListener)); + assertThatCode(() -> test.subscribe(testSubscriber)) + .doesNotThrowAnyException(); + } else { + FluxTap test = new FluxTap<>(Flux.just(1), factoryOf(testSignalListener)); + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + } + + assertThat(testSubscriber.expectTerminalError()) + .as("downstream error") + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to augment tap Context at subscription via addToContext") + .extracting(Throwable::getCause) + .satisfies(t -> assertThat(t) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected")); + + assertThat(testSignalListener.listenerErrors) + .as("listenerErrors") + .satisfies(errors -> { + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.stream().findFirst().get()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to augment tap Context at subscription via addToContext") + .extracting(Throwable::getCause) + .satisfies(t -> assertThat(t) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected")); + }); + assertThat(testSignalListener.events) + .containsExactly("doFirst"); + } + @ParameterizedTestWithName @ValueSource(booleans = {true, false}) void doFirstListenerError(boolean automatic) { @@ -661,6 +712,47 @@ void doFirst() { ); } + @Test + void throwingAlterContext() { + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener testSignalListener = + new TestSignalListener() { + @Override + public Context addToContext(Context originalContext) { + throw new IllegalStateException("expected"); + } + }; + + FluxTapFuseable test = new FluxTapFuseable<>( + Flux.just(1), factoryOf(testSignalListener)); + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(testSubscriber.expectTerminalError()) + .as("downstream error") + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to augment tap Context at subscription via addToContext") + .extracting(Throwable::getCause) + .satisfies(t -> assertThat(t) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected")); + + assertThat(testSignalListener.listenerErrors) + .as("listenerErrors") + .satisfies(errors -> { + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.stream().findFirst().get()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to augment tap Context at subscription via addToContext") + .extracting(Throwable::getCause) + .satisfies(t -> assertThat(t) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected")); + }); + assertThat(testSignalListener.events) + .containsExactly("doFirst"); + } + @Test void doFirstListenerError() { Throwable listenerError = new IllegalStateException("expected from doFirst"); @@ -816,6 +908,47 @@ void doFirst() { ); } + @Test + void throwingAlterContext() { + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener testSignalListener = + new TestSignalListener() { + @Override + public Context addToContext(Context originalContext) { + throw new IllegalStateException("expected"); + } + }; + + MonoTap test = new MonoTap<>( + Mono.just(1), factoryOf(testSignalListener)); + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(testSubscriber.expectTerminalError()) + .as("downstream error") + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to augment tap Context at subscription via addToContext") + .extracting(Throwable::getCause) + .satisfies(t -> assertThat(t) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected")); + + assertThat(testSignalListener.listenerErrors) + .as("listenerErrors") + .satisfies(errors -> { + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.stream().findFirst().get()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to augment tap Context at subscription via addToContext") + .extracting(Throwable::getCause) + .satisfies(t -> assertThat(t) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected")); + }); + assertThat(testSignalListener.events) + .containsExactly("doFirst"); + } + @Test void doFirstListenerError() { Throwable listenerError = new IllegalStateException("expected from doFirst"); @@ -917,6 +1050,47 @@ void doFirst() { ); } + @Test + void throwingAlterContext() { + TestSubscriber testSubscriber = TestSubscriber.create(); + TestSignalListener testSignalListener = + new TestSignalListener() { + @Override + public Context addToContext(Context originalContext) { + throw new IllegalStateException("expected"); + } + }; + + MonoTapFuseable test = new MonoTapFuseable<>( + Mono.just(1), factoryOf(testSignalListener)); + assertThatCode(() -> test.subscribeOrReturn(testSubscriber)) + .doesNotThrowAnyException(); + + assertThat(testSubscriber.expectTerminalError()) + .as("downstream error") + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to augment tap Context at subscription via addToContext") + .extracting(Throwable::getCause) + .satisfies(t -> assertThat(t) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected")); + + assertThat(testSignalListener.listenerErrors) + .as("listenerErrors") + .satisfies(errors -> { + assertThat(errors.size()).isEqualTo(1); + assertThat(errors.stream().findFirst().get()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unable to augment tap Context at subscription via addToContext") + .extracting(Throwable::getCause) + .satisfies(t -> assertThat(t) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected")); + }); + assertThat(testSignalListener.events) + .containsExactly("doFirst"); + } + @Test void doFirstListenerError() { Throwable listenerError = new IllegalStateException("expected from doFirst"); @@ -1039,7 +1213,7 @@ void scanListenSubscriber() { Subscription subscription = Operators.emptySubscription(); FluxTap.TapSubscriber subscriber = new FluxTap.TapSubscriber<>( - actual, new TestSignalListener<>()); + actual, new TestSignalListener<>(), actual.currentContext()); subscriber.onSubscribe(subscription); @@ -1084,7 +1258,7 @@ void scanListenConditionalSubscriber() { Subscription subscription = Operators.emptySubscription(); FluxTap.TapConditionalSubscriber subscriber = new FluxTap.TapConditionalSubscriber<>( - actual, new TestSignalListener<>()); + actual, new TestSignalListener<>(), actual.currentContext()); subscriber.onSubscribe(subscription); @@ -1106,7 +1280,7 @@ void scanListenFuseableSubscriber() { Subscription subscription = Operators.emptySubscription(); FluxTapFuseable.TapFuseableSubscriber subscriber = new FluxTapFuseable.TapFuseableSubscriber<>( - actual, new TestSignalListener<>()); + actual, new TestSignalListener<>(), actual.currentContext()); subscriber.onSubscribe(subscription); @@ -1127,9 +1301,9 @@ void scanListenConditionalFuseableSubscriber() { ConditionalSubscriber actual = Operators.toConditionalSubscriber(Operators.drainSubscriber()); Subscription subscription = Operators.emptySubscription(); - FluxTapFuseable.TapConditionalFuseableSubscriber - subscriber = new FluxTapFuseable.TapConditionalFuseableSubscriber<>( - actual, new TestSignalListener<>()); + FluxTapFuseable.TapConditionalFuseableSubscriber subscriber = + new FluxTapFuseable.TapConditionalFuseableSubscriber<>( + actual, new TestSignalListener<>(), actual.currentContext()); subscriber.onSubscribe(subscription); From 9ca5bf3b08d06206901fd2ecac2d52362de6402d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 10 Apr 2023 17:42:13 +0300 Subject: [PATCH 109/312] updates micrometer lib version (#3431) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac0aa41adf..0b6dbf8d89 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.5" +micrometer = "1.10.6" kotlin = "1.5.32" reactiveStreams = "1.0.4" From 32368408437864728f727127ba721c1d86b0285b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 11 Apr 2023 08:52:17 +0300 Subject: [PATCH 110/312] updates micrometer-tracing-integration-test to 1.0.4 (#3433) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0b6dbf8d89..13e99600f5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.2" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.3" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.4" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 324a64c3d7e21b9ec51c4e421a88c8a8689263b5 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 11 Apr 2023 15:25:45 +0200 Subject: [PATCH 111/312] [release] Prepare and release 3.5.5 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f7ee9d23a9..667ae31c0c 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.4" - testCompile "io.projectreactor:reactor-test:3.5.4" + compile "io.projectreactor:reactor-core:3.5.5" + testCompile "io.projectreactor:reactor-test:3.5.5" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.5-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.5-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.6-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.6-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.4" + // implementation "io.projectreactor:reactor-tools:3.5.5" } ``` diff --git a/gradle.properties b/gradle.properties index e47d8d70eb..2b8be4deec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.5-SNAPSHOT -bomVersion=2022.0.5 -metricsMicrometerVersion=1.0.5-SNAPSHOT +version=3.5.5 +bomVersion=2022.0.6 +metricsMicrometerVersion=1.0.5 From 96cde5be59fbd64b867a1006cc29d2104c7e48bf Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 11 Apr 2023 16:27:13 +0200 Subject: [PATCH 112/312] [release] Next development version 3.5.6-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2b8be4deec..f10089f185 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.5 +version=3.5.6-SNAPSHOT bomVersion=2022.0.6 -metricsMicrometerVersion=1.0.5 +metricsMicrometerVersion=1.0.6-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13e99600f5..71167bbeca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.4" -baselinePerfCore = "3.5.4" +baseline-core-api = "3.5.5" +baselinePerfCore = "3.5.5" baselinePerfExtra = "3.5.1" # Other shared versions From 8ed8ee7c2a9091713fff1ea69bf2b1cc4e7fb306 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 9 May 2023 10:14:01 +0300 Subject: [PATCH 113/312] Update Micrometer version to 1.10.7 (#3453) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 71167bbeca..71bac5079d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.6" +micrometer = "1.10.7" kotlin = "1.5.32" reactiveStreams = "1.0.4" From bad040e3315bae67e47ef137609083a3ac59196a Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 9 May 2023 10:21:09 +0300 Subject: [PATCH 114/312] [release] Prepare and release 3.5.6 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 667ae31c0c..4ef0365bab 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.5" - testCompile "io.projectreactor:reactor-test:3.5.5" + compile "io.projectreactor:reactor-core:3.5.6" + testCompile "io.projectreactor:reactor-test:3.5.6" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.6-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.6-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.7-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.7-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.5" + // implementation "io.projectreactor:reactor-tools:3.5.6" } ``` diff --git a/gradle.properties b/gradle.properties index f10089f185..36a7fbc439 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.6-SNAPSHOT -bomVersion=2022.0.6 -metricsMicrometerVersion=1.0.6-SNAPSHOT +version=3.5.6 +bomVersion=2022.0.7 +metricsMicrometerVersion=1.0.6 From 0ad1dd4fedf1bf27e6a7d70c1f1f4606593cedfa Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 9 May 2023 10:50:26 +0300 Subject: [PATCH 115/312] [release] Next development version 3.5.7-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 36a7fbc439..d6f7f40fac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.6 +version=3.5.7-SNAPSHOT bomVersion=2022.0.7 -metricsMicrometerVersion=1.0.6 +metricsMicrometerVersion=1.0.7-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 71bac5079d..0fc96888b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.5" -baselinePerfCore = "3.5.5" +baseline-core-api = "3.5.6" +baselinePerfCore = "3.5.6" baselinePerfExtra = "3.5.1" # Other shared versions From 46567369bf715a013513cd87ac8fb33ecf15018a Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Wed, 17 May 2023 14:09:38 +0200 Subject: [PATCH 116/312] avoids IllegalStateException: Cannot resolve type description (#3459) Avoid IllegalStateException: Cannot resolve type description for io.micrometer.context.ContextRegistry (#3459) --- .../core/publisher/ContextPropagation.java | 79 ++++--------------- .../publisher/ContextPropagationSupport.java | 62 +++++++++++++++ .../java/reactor/core/publisher/Flux.java | 9 +-- .../reactor/core/publisher/FluxHandle.java | 5 +- .../core/publisher/FluxHandleFuseable.java | 5 +- .../reactor/core/publisher/FluxSource.java | 2 +- .../java/reactor/core/publisher/FluxTap.java | 3 +- .../core/publisher/FluxTapFuseable.java | 3 +- .../java/reactor/core/publisher/Hooks.java | 8 +- .../java/reactor/core/publisher/Mono.java | 8 +- .../core/publisher/MonoCompletionStage.java | 2 +- .../core/publisher/MonoFromPublisher.java | 2 +- .../reactor/core/publisher/MonoHandle.java | 5 +- .../core/publisher/MonoHandleFuseable.java | 5 +- .../reactor/core/publisher/MonoSource.java | 2 +- .../java/reactor/core/publisher/MonoTap.java | 3 +- .../core/publisher/MonoTapFuseable.java | 3 +- .../ContextPropagationNotThereSmokeTest.java | 4 +- .../publisher/ContextPropagationTest.java | 10 +-- 19 files changed, 120 insertions(+), 100 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 8b45999c4f..edb65e97a3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -17,7 +17,6 @@ package reactor.core.publisher; import java.util.AbstractQueue; -import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -33,8 +32,6 @@ import io.micrometer.context.ThreadLocalAccessor; import reactor.core.observability.SignalListener; -import reactor.util.Logger; -import reactor.util.Loggers; import reactor.util.annotation.Nullable; import reactor.util.context.Context; import reactor.util.context.ContextView; @@ -47,58 +44,13 @@ */ final class ContextPropagation { - static final Logger LOGGER; - - // Note: If reflection is used for this field, then the name of the field should end with 'Available'. - // The preprocessing for native-image support is in Spring Framework, and is a short term solution. - // The field should end with 'Available'. See org.springframework.aot.nativex.feature.PreComputeFieldFeature. - // Ultimately the long term solution should be provided by Reactor Core. - static final boolean isContextPropagationOnClasspath; - static boolean propagateContextToThreadLocals = false; - static final Predicate PREDICATE_TRUE = v -> true; static final Function NO_OP = c -> c; static final Function WITH_GLOBAL_REGISTRY_NO_PREDICATE; - static { - LOGGER = Loggers.getLogger(ContextPropagation.class); - - Function contextCaptureFunction; - boolean contextPropagation; - try { - // The following line will throw a LinkageError (NoClassDefFoundError) in case context propagation is not available - ContextRegistry globalRegistry = ContextRegistry.getInstance(); - contextCaptureFunction = new ContextCaptureNoPredicate(globalRegistry); - contextPropagation = true; - } - catch (LinkageError t) { - // Context-Propagation library is not available - contextCaptureFunction = NO_OP; - contextPropagation = false; - } - catch (Throwable t) { - contextCaptureFunction = NO_OP; - contextPropagation = false; - LOGGER.error("Unexpected exception while detecting ContextPropagation feature." + - " The feature is considered disabled due to this:", t); - } - - isContextPropagationOnClasspath = contextPropagation; - WITH_GLOBAL_REGISTRY_NO_PREDICATE = contextCaptureFunction; - } - - /** - * Is Micrometer {@code context-propagation} API on the classpath? - * - * @return true if context-propagation is available at runtime, false otherwise - */ - static boolean isContextPropagationAvailable() { - return isContextPropagationOnClasspath; - } - - static boolean shouldPropagateContextToThreadLocals() { - return isContextPropagationOnClasspath && propagateContextToThreadLocals; + WITH_GLOBAL_REGISTRY_NO_PREDICATE = ContextPropagationSupport.isContextPropagationAvailable() ? + new ContextCaptureNoPredicate(ContextRegistry.getInstance()) : NO_OP; } public static Function scopePassingOnScheduleHook() { @@ -120,9 +72,6 @@ public static Function scopePassingOnScheduleHook() { * @return the {@link Context} augmented with captured entries */ static Function contextCapture() { - if (!isContextPropagationOnClasspath) { - return NO_OP; - } return WITH_GLOBAL_REGISTRY_NO_PREDICATE; } @@ -143,7 +92,7 @@ static Function contextCapture() { * @return a {@link Function} augmenting {@link Context} with captured entries */ static Function contextCapture(Predicate captureKeyPredicate) { - if (!isContextPropagationOnClasspath) { + if (!ContextPropagationSupport.isContextPropagationOnClasspath) { return NO_OP; } return target -> ContextSnapshot.captureAllUsing(captureKeyPredicate, ContextRegistry.getInstance()) @@ -162,18 +111,20 @@ static Function contextCapture(Predicate captureKeyPre * @param the transformed type */ static BiConsumer> contextRestoreForHandle(BiConsumer> handler, Supplier contextSupplier) { - if (propagateContextToThreadLocals || !ContextPropagation.isContextPropagationAvailable()) { - return handler; + if (ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators()) { + final Context ctx = contextSupplier.get(); + if (ctx.isEmpty()) { + return handler; + } + return (v, sink) -> { + try (ContextSnapshot.Scope ignored = ContextSnapshot.setAllThreadLocalsFrom(ctx)) { + handler.accept(v, sink); + } + }; } - final Context ctx = contextSupplier.get(); - if (ctx.isEmpty()) { + else { return handler; } - return (v, sink) -> { - try (ContextSnapshot.Scope ignored = ContextSnapshot.setAllThreadLocalsFrom(ctx)) { - handler.accept(v, sink); - } - }; } /** @@ -192,7 +143,7 @@ static BiConsumer> contextRestoreForHandle(BiConsum * @param type of handled values */ static SignalListener contextRestoreForTap(final SignalListener original, Supplier contextSupplier) { - if (!ContextPropagation.isContextPropagationAvailable()) { + if (!ContextPropagationSupport.isContextPropagationAvailable()) { return original; } final Context ctx = contextSupplier.get(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java new file mode 100644 index 0000000000..29ff8ad640 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import reactor.util.Logger; +import reactor.util.Loggers; + +final class ContextPropagationSupport { + static final Logger LOGGER = Loggers.getLogger(ContextPropagationSupport.class); + + // Note: If reflection is used for this field, then the name of the field should end with 'Available'. + // The preprocessing for native-image support is in Spring Framework, and is a short term solution. + // The field should end with 'Available'. See org.springframework.aot.nativex.feature.PreComputeFieldFeature. + // Ultimately the long term solution should be provided by Reactor Core. + static final boolean isContextPropagationOnClasspath; + static boolean propagateContextToThreadLocals = false; + + static { + boolean contextPropagation = false; + try { + Class.forName("io.micrometer.context.ContextRegistry"); + contextPropagation = true; + } catch (ClassNotFoundException notFound) { + } catch (LinkageError linkageErr) { + } catch (Throwable err) { + LOGGER.error("Unexpected exception while detecting ContextPropagation feature." + + " The feature is considered disabled due to this:", err); + } + isContextPropagationOnClasspath = contextPropagation; + } + + /** + * Is Micrometer {@code context-propagation} API on the classpath? + * + * @return true if context-propagation is available at runtime, false otherwise + */ + static boolean isContextPropagationAvailable() { + return isContextPropagationOnClasspath; + } + + static boolean shouldPropagateContextToThreadLocals() { + return isContextPropagationOnClasspath && propagateContextToThreadLocals; + } + + static boolean shouldRestoreThreadLocalsInSomeOperators() { + return isContextPropagationOnClasspath && !propagateContextToThreadLocals; + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 24213b6a78..b56c2c3567 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -44,7 +44,6 @@ import java.util.function.Supplier; import java.util.logging.Level; import java.util.stream.Collector; -import java.util.stream.Collectors; import java.util.stream.Stream; import io.micrometer.core.instrument.MeterRegistry; @@ -4163,10 +4162,10 @@ public final Flux concatWith(Publisher other) { * @see #tap(SignalListenerFactory) */ public final Flux contextCapture() { - if (!ContextPropagation.isContextPropagationAvailable()) { + if (!ContextPropagationSupport.isContextPropagationAvailable()) { return this; } - if (ContextPropagation.propagateContextToThreadLocals) { + if (ContextPropagationSupport.propagateContextToThreadLocals) { return onAssembly(new FluxContextWriteRestoringThreadLocals<>( this, ContextPropagation.contextCapture() )); @@ -4217,7 +4216,7 @@ public final Flux contextWrite(ContextView contextToAppend) { * @see Context */ public final Flux contextWrite(Function contextModifier) { - if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { return onAssembly(new FluxContextWriteRestoringThreadLocals<>( this, contextModifier )); @@ -9249,7 +9248,7 @@ public SignalListener createListener(Publisher ignored1, Context * @see #tap(Function) */ public final Flux tap(SignalListenerFactory listenerFactory) { - if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { return onAssembly(new FluxTapRestoringThreadLocals<>(this, listenerFactory)); } if (this instanceof Fuseable) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java b/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java index 21cf1cd460..54cbfbd55c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,8 @@ final class FluxHandle extends InternalFluxOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext); + BiConsumer> handler2 = ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators() ? + ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext) : this.handler; if (actual instanceof Fuseable.ConditionalSubscriber) { @SuppressWarnings("unchecked") Fuseable.ConditionalSubscriber cs = (Fuseable.ConditionalSubscriber) actual; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java index a7204f84dc..5cdd780421 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxHandleFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,8 @@ final class FluxHandleFuseable extends InternalFluxOperator implemen @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext); + BiConsumer> handler2 = ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators() ? + ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext) : this.handler; if (actual instanceof ConditionalSubscriber) { @SuppressWarnings("unchecked") ConditionalSubscriber cs = (ConditionalSubscriber) actual; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java index 3957cf5cf2..92fdfc4cfc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java @@ -68,7 +68,7 @@ final class FluxSource extends Flux implements SourceProducer, @Override @SuppressWarnings("unchecked") public void subscribe(CoreSubscriber actual) { - if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { source.subscribe(new FluxSourceRestoringThreadLocalsSubscriber<>(actual)); } else { source.subscribe(actual); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java index 1da50b0cf5..9753dc6c44 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTap.java @@ -58,7 +58,8 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act } // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods // (only if ContextPropagation.isContextPropagationAvailable() is true) - signalListener = ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext); + signalListener = ContextPropagationSupport.isContextPropagationAvailable() ? + ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext) : signalListener; try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java index efe3821a97..2e7916643e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapFuseable.java @@ -59,7 +59,8 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act } // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods // (only if ContextPropagation.isContextPropagationAvailable() is true) - signalListener = ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext); + signalListener = ContextPropagationSupport.isContextPropagationAvailable() ? + ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext) : signalListener; try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java index 1c256aea13..b56e750e00 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java @@ -535,7 +535,7 @@ public static void disableContextLossTracking() { * a logical boundary for the context propagation mechanism. */ public static void enableAutomaticContextPropagation() { - if (ContextPropagation.isContextPropagationOnClasspath) { + if (ContextPropagationSupport.isContextPropagationOnClasspath) { Hooks.addQueueWrapper( CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.ContextQueue::new ); @@ -543,7 +543,7 @@ public static void enableAutomaticContextPropagation() { CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.scopePassingOnScheduleHook() ); - ContextPropagation.propagateContextToThreadLocals = true; + ContextPropagationSupport.propagateContextToThreadLocals = true; } } @@ -552,10 +552,10 @@ public static void enableAutomaticContextPropagation() { * @see #enableAutomaticContextPropagation() */ public static void disableAutomaticContextPropagation() { - if (ContextPropagation.isContextPropagationOnClasspath) { + if (ContextPropagationSupport.isContextPropagationOnClasspath) { Hooks.removeQueueWrapper(CONTEXT_IN_THREAD_LOCALS_KEY); Schedulers.resetOnScheduleHook(CONTEXT_IN_THREAD_LOCALS_KEY); - ContextPropagation.propagateContextToThreadLocals = false; + ContextPropagationSupport.propagateContextToThreadLocals = false; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index c3411ad710..993884cdbf 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -2281,10 +2281,10 @@ public final Flux concatWith(Publisher other) { * @see #tap(SignalListenerFactory) */ public final Mono contextCapture() { - if (!ContextPropagation.isContextPropagationAvailable()) { + if (!ContextPropagationSupport.isContextPropagationAvailable()) { return this; } - if (ContextPropagation.propagateContextToThreadLocals) { + if (ContextPropagationSupport.propagateContextToThreadLocals) { return onAssembly(new MonoContextWriteRestoringThreadLocals<>( this, ContextPropagation.contextCapture() )); @@ -2335,7 +2335,7 @@ public final Mono contextWrite(ContextView contextToAppend) { * @see Context */ public final Mono contextWrite(Function contextModifier) { - if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { return onAssembly(new MonoContextWriteRestoringThreadLocals<>( this, contextModifier )); @@ -4740,7 +4740,7 @@ public SignalListener createListener(Publisher ignored1, Context * @see #tap(Function) */ public final Mono tap(SignalListenerFactory listenerFactory) { - if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { return onAssembly(new MonoTapRestoringThreadLocals<>(this, listenerFactory)); } if (this instanceof Fuseable) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java index 04d93b45c8..2265fb782f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java @@ -52,7 +52,7 @@ final class MonoCompletionStage extends Mono @Override public void subscribe(CoreSubscriber actual) { - if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { actual.onSubscribe( new MonoCompletionStageRestoringThreadLocalsSubscription<>( actual, future, suppressCancellation)); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java index 3ac5db298e..9aa1156288 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java @@ -55,7 +55,7 @@ final class MonoFromPublisher extends Mono implements Scannable, @Override @SuppressWarnings("unchecked") public void subscribe(CoreSubscriber actual) { - if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { actual = new MonoSource.MonoSourceRestoringThreadLocalsSubscriber<>(actual); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java b/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java index 1dafb86b70..72cef28698 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,8 @@ final class MonoHandle extends InternalMonoOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext); + BiConsumer> handler2 = ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators() ? + ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext) : this.handler; return new FluxHandle.HandleSubscriber<>(actual, handler2); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java index 807df6eb6d..c89df42f69 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoHandleFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,8 @@ final class MonoHandleFuseable extends InternalMonoOperator @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - BiConsumer> handler2 = ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext); + BiConsumer> handler2 = ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators() ? + ContextPropagation.contextRestoreForHandle(this.handler, actual::currentContext) : this.handler; return new FluxHandleFuseable.HandleFuseableSubscriber<>(actual, handler2); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java index 716e518480..d4bcccf472 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java @@ -65,7 +65,7 @@ final class MonoSource extends Mono implements Scannable, SourceProducer actual) { - if (ContextPropagation.shouldPropagateContextToThreadLocals()) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { source.subscribe(new MonoSourceRestoringThreadLocalsSubscriber<>(actual)); } else { source.subscribe(actual); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java index ba72852296..12fcf25e35 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTap.java @@ -56,7 +56,8 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act } // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods // (only if ContextPropagation.isContextPropagationAvailable() is true) - signalListener = ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext); + signalListener = ContextPropagationSupport.isContextPropagationAvailable() ? + ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext) : signalListener; try { signalListener.doFirst(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java index ce83cb4f25..1c8fe2dfc6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapFuseable.java @@ -55,7 +55,8 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act } // Attempt to wrap the SignalListener with one that restores ThreadLocals from Context on each listener methods // (only if ContextPropagation.isContextPropagationAvailable() is true) - signalListener = ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext); + signalListener = ContextPropagationSupport.isContextPropagationAvailable() ? + ContextPropagation.contextRestoreForTap(signalListener, actual::currentContext) : signalListener; try { signalListener.doFirst(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java b/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java index 780a7a8ca5..1632ad1590 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ class ContextPropagationNotThereSmokeTest { @Test void contextPropagationIsNotAvailable() { - assertThat(ContextPropagation.isContextPropagationAvailable()).isFalse(); + assertThat(ContextPropagationSupport.isContextPropagationAvailable()).isFalse(); } @Test diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index 5d9f7e1e83..8b93a669ae 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -328,7 +328,7 @@ void queueWrapperWorksWithQueues() { @Test void isContextPropagationAvailable() { - assertThat(ContextPropagation.isContextPropagationAvailable()).isTrue(); + assertThat(ContextPropagationSupport.isContextPropagationAvailable()).isTrue(); } @Test @@ -890,8 +890,8 @@ void publicMethodChecksForContextNotEmptyBeforeWrapping(boolean withContext) { context = Context.empty(); } - BiConsumer> decoratedHandler = ContextPropagation.contextRestoreForHandle(originalHandler, - () -> context); + BiConsumer> decoratedHandler = ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators() ? + ContextPropagation.contextRestoreForHandle(originalHandler, () -> context) : originalHandler; if (withContext) { assertThat(decoratedHandler).as("context not empty: decorated handler").isNotSameAs(originalHandler); @@ -912,8 +912,8 @@ void classContextRestoreHandleConsumerRestoresThreadLocal() { final String expected = "bar=expected"; final Context context = Context.of(KEY1, "expected"); - BiConsumer> decoratedHandler = - ContextPropagation.contextRestoreForHandle(originalHandler, () -> context); + BiConsumer> decoratedHandler = ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators() ? + ContextPropagation.contextRestoreForHandle(originalHandler, () -> context) : originalHandler; SynchronousSink mockSink = Mockito.mock(SynchronousSink.class); decoratedHandler.accept("bar", mockSink); From 17ac6f44e73e8a62f5c28fe2af196006c7e6abf4 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 31 May 2023 10:49:42 +0200 Subject: [PATCH 117/312] context-propagation: Use new ThreadLocalAccessor contract (#3460) Since 1.0.3 of context-propagation library, ThreadLocalAccessor distinguishes reset operations: * setValue when entering a scope (no-arg when the scope is empty) * restore when closing a scope (no-arg when coming back to an empty scope) This change uses these methods in order to allow Micrometer to take advantage of the distinction and provide a proper mechanism for manipulating Observation and tracing (Brave/OTEL) scopes. --- gradle/libs.versions.toml | 2 +- .../java/reactor/core/publisher/ContextPropagation.java | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0fc96888b0..9ff58c6bf2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ jsr305 = "com.google.code.findbugs:jsr305:3.0.1" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.2" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.3-SNAPSHOT" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.4" diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index edb65e97a3..6a6eef8418 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -419,9 +419,8 @@ private static Map setThreadLocal(Object key, @Nullable V va previousValues.put(key, accessor.getValue()); if (value != null) { ((ThreadLocalAccessor) accessor).setValue(value); - } - else { - accessor.reset(); + } else { + accessor.setValue(); } return previousValues; } @@ -454,7 +453,7 @@ private void resetThreadLocalValue(ThreadLocalAccessor accessor, @Nullabl ((ThreadLocalAccessor) accessor).restore(previousValue); } else { - accessor.reset(); + accessor.restore(); } } From 2eaa584460338b910da572e4bab9b468470f9f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 5 Jun 2023 10:29:08 +0200 Subject: [PATCH 118/312] Transparent contextCapture in block operators (#3420) Operators that block to retrieve the result from the reactive chain are usually used in imperative contexts. If `ThreadLocal` values are present at call site, they can be captured automatically. This change frees the user from calling `contextCapture` everywhere that they block. The following operators were altered to capture automatically if the context-propagation library is on the classpath: * `Mono#block`, * `Mono#blockOptional`, * `Flux#blockFirst`, * `Flux#blockLast`, * `Flux#toIterable`, * `Flux#toStream`. --- .../publisher/BlockingFirstSubscriber.java | 8 +- .../core/publisher/BlockingIterable.java | 17 ++- .../publisher/BlockingLastSubscriber.java | 8 +- .../publisher/BlockingMonoSubscriber.java | 8 +- .../BlockingOptionalMonoSubscriber.java | 9 +- .../publisher/BlockingSingleSubscriber.java | 9 +- .../core/publisher/ContextPropagation.java | 4 + .../java/reactor/core/publisher/Flux.java | 26 +++- .../java/reactor/core/publisher/Hooks.java | 16 ++- .../java/reactor/core/publisher/Mono.java | 18 ++- .../core/publisher/BlockingIterableTest.java | 26 ++-- .../BlockingOptionalMonoSubscriberTest.java | 24 ++-- .../BlockingSingleSubscriberTest.java | 6 +- .../reactor/core/publisher/OperatorsTest.java | 4 +- .../publisher/ContextPropagationTest.java | 121 ++++++++++++++++++ 15 files changed, 248 insertions(+), 56 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/BlockingFirstSubscriber.java b/reactor-core/src/main/java/reactor/core/publisher/BlockingFirstSubscriber.java index 1dc0d4ebb7..dd488da5f4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/BlockingFirstSubscriber.java +++ b/reactor-core/src/main/java/reactor/core/publisher/BlockingFirstSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package reactor.core.publisher; +import reactor.util.context.Context; + /** * Blocks until the upstream signals its first value or completes. * @@ -24,6 +26,10 @@ */ final class BlockingFirstSubscriber extends BlockingSingleSubscriber { + public BlockingFirstSubscriber(Context context) { + super(context); + } + @Override public void onNext(T t) { if (value == null) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/BlockingIterable.java b/reactor-core/src/main/java/reactor/core/publisher/BlockingIterable.java index b8362d2372..996d1cf9fb 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/BlockingIterable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/BlockingIterable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,9 +54,13 @@ final class BlockingIterable implements Iterable, Scannable { final Supplier> queueSupplier; + final Supplier contextSupplier; + BlockingIterable(CorePublisher source, int batchSize, - Supplier> queueSupplier) { + Supplier> queueSupplier, + Supplier contextSupplier) { + this.contextSupplier = contextSupplier; if (batchSize <= 0) { throw new IllegalArgumentException("batchSize > 0 required but it was " + batchSize); } @@ -114,7 +118,7 @@ SubscriberIterator createIterator() { throw Exceptions.propagate(e); } - return new SubscriberIterator<>(q, batchSize); + return new SubscriberIterator<>(q, contextSupplier.get(), batchSize); } static final class SubscriberIterator @@ -130,6 +134,8 @@ static final class SubscriberIterator final Condition condition; + final Context context; + long produced; volatile Subscription s; @@ -142,17 +148,18 @@ static final class SubscriberIterator volatile boolean done; Throwable error; - SubscriberIterator(Queue queue, int batchSize) { + SubscriberIterator(Queue queue, Context context, int batchSize) { this.queue = queue; this.batchSize = batchSize; this.limit = Operators.unboundedOrLimit(batchSize); this.lock = new ReentrantLock(); this.condition = lock.newCondition(); + this.context = context; } @Override public Context currentContext() { - return Context.empty(); + return this.context; } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/BlockingLastSubscriber.java b/reactor-core/src/main/java/reactor/core/publisher/BlockingLastSubscriber.java index 6f7d8eb4a3..b2090608a8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/BlockingLastSubscriber.java +++ b/reactor-core/src/main/java/reactor/core/publisher/BlockingLastSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package reactor.core.publisher; +import reactor.util.context.Context; + /** * Blocks until the upstream signals its last value or completes. * @@ -24,6 +26,10 @@ */ final class BlockingLastSubscriber extends BlockingSingleSubscriber { + public BlockingLastSubscriber(Context context) { + super(context); + } + @Override public void onNext(T t) { value = t; diff --git a/reactor-core/src/main/java/reactor/core/publisher/BlockingMonoSubscriber.java b/reactor-core/src/main/java/reactor/core/publisher/BlockingMonoSubscriber.java index 7baf5a9782..04d9df964a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/BlockingMonoSubscriber.java +++ b/reactor-core/src/main/java/reactor/core/publisher/BlockingMonoSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package reactor.core.publisher; +import reactor.util.context.Context; + /** * Blocks assuming the upstream is a Mono, until it signals its value or completes. * Compared to {@link BlockingFirstSubscriber}, this variant doesn't cancel the upstream @@ -26,6 +28,10 @@ */ final class BlockingMonoSubscriber extends BlockingSingleSubscriber { + public BlockingMonoSubscriber(Context context) { + super(context); + } + @Override public void onNext(T t) { if (value == null) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/BlockingOptionalMonoSubscriber.java b/reactor-core/src/main/java/reactor/core/publisher/BlockingOptionalMonoSubscriber.java index 0faf6492b5..28c3ddf12d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/BlockingOptionalMonoSubscriber.java +++ b/reactor-core/src/main/java/reactor/core/publisher/BlockingOptionalMonoSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,8 +45,11 @@ final class BlockingOptionalMonoSubscriber extends CountDownLatch volatile boolean cancelled; - BlockingOptionalMonoSubscriber() { + final Context context; + + BlockingOptionalMonoSubscriber(Context context) { super(1); + this.context = context; } @Override @@ -80,7 +83,7 @@ public final void onComplete() { @Override public Context currentContext() { - return Context.empty(); + return this.context; } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/BlockingSingleSubscriber.java b/reactor-core/src/main/java/reactor/core/publisher/BlockingSingleSubscriber.java index 7852356497..2e97b1c6fc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/BlockingSingleSubscriber.java +++ b/reactor-core/src/main/java/reactor/core/publisher/BlockingSingleSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,10 +37,13 @@ abstract class BlockingSingleSubscriber extends CountDownLatch Subscription s; + final Context context; + volatile boolean cancelled; - BlockingSingleSubscriber() { + BlockingSingleSubscriber(Context context) { super(1); + this.context = context; } @Override @@ -58,7 +61,7 @@ public final void onComplete() { @Override public Context currentContext() { - return Context.empty(); + return this.context; } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 6a6eef8418..a19faf32b4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -75,6 +75,10 @@ static Function contextCapture() { return WITH_GLOBAL_REGISTRY_NO_PREDICATE; } + static Context contextCaptureToEmpty() { + return contextCapture().apply(Context.empty()); + } + /** * Create a support function that takes a snapshot of thread locals and merges them with the * provided {@link Context}, resulting in a new {@link Context} which includes entries diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index b56c2c3567..51b9ae8dca 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -2696,7 +2696,9 @@ public final

    P as(Function, P> transformer) { */ @Nullable public final T blockFirst() { - BlockingFirstSubscriber subscriber = new BlockingFirstSubscriber<>(); + Context context = ContextPropagationSupport.shouldPropagateContextToThreadLocals() + ? ContextPropagation.contextCaptureToEmpty() : Context.empty(); + BlockingFirstSubscriber subscriber = new BlockingFirstSubscriber<>(context); subscribe((Subscriber) subscriber); return subscriber.blockingGet(); } @@ -2719,7 +2721,9 @@ public final T blockFirst() { */ @Nullable public final T blockFirst(Duration timeout) { - BlockingFirstSubscriber subscriber = new BlockingFirstSubscriber<>(); + Context context = ContextPropagationSupport.shouldPropagateContextToThreadLocals() + ? ContextPropagation.contextCaptureToEmpty() : Context.empty(); + BlockingFirstSubscriber subscriber = new BlockingFirstSubscriber<>(context); subscribe((Subscriber) subscriber); return subscriber.blockingGet(timeout.toNanos(), TimeUnit.NANOSECONDS); } @@ -2741,7 +2745,9 @@ public final T blockFirst(Duration timeout) { */ @Nullable public final T blockLast() { - BlockingLastSubscriber subscriber = new BlockingLastSubscriber<>(); + Context context = ContextPropagationSupport.shouldPropagateContextToThreadLocals() + ? ContextPropagation.contextCaptureToEmpty() : Context.empty(); + BlockingLastSubscriber subscriber = new BlockingLastSubscriber<>(context); subscribe((Subscriber) subscriber); return subscriber.blockingGet(); } @@ -2765,7 +2771,9 @@ public final T blockLast() { */ @Nullable public final T blockLast(Duration timeout) { - BlockingLastSubscriber subscriber = new BlockingLastSubscriber<>(); + Context context = ContextPropagationSupport.shouldPropagateContextToThreadLocals() + ? ContextPropagation.contextCaptureToEmpty() : Context.empty(); + BlockingLastSubscriber subscriber = new BlockingLastSubscriber<>(context); subscribe((Subscriber) subscriber); return subscriber.blockingGet(timeout.toNanos(), TimeUnit.NANOSECONDS); } @@ -9652,7 +9660,10 @@ public final Iterable toIterable(int batchSize, @Nullable Supplier> else{ provider = () -> Hooks.wrapQueue(queueProvider.get()); } - return new BlockingIterable<>(this, batchSize, provider); + Supplier contextSupplier = + ContextPropagationSupport.shouldPropagateContextToThreadLocals() ? + ContextPropagation::contextCaptureToEmpty : Context::empty; + return new BlockingIterable<>(this, batchSize, provider, contextSupplier); } /** @@ -9690,7 +9701,10 @@ public final Stream toStream() { public final Stream toStream(int batchSize) { final Supplier> provider; provider = Queues.get(batchSize); - return new BlockingIterable<>(this, batchSize, provider).stream(); + Supplier contextSupplier = + ContextPropagationSupport.shouldPropagateContextToThreadLocals() ? + ContextPropagation::contextCaptureToEmpty : Context::empty; + return new BlockingIterable<>(this, batchSize, provider, contextSupplier).stream(); } /** diff --git a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java index b56e750e00..d3d8391322 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java @@ -533,16 +533,18 @@ public static void disableContextLossTracking() { * {@code contextWrite(...)} call and the unmodified (downstream) {@link Context} is * used when signals are delivered downstream, making the {@code contextWrite(...)} * a logical boundary for the context propagation mechanism. + *

    + * This mechanism automatically performs {@link Flux#contextCapture()} + * and {@link Mono#contextCapture()} in {@link Flux#blockFirst()}, + * {@link Flux#blockLast()}, {@link Flux#toIterable()}, and {@link Mono#block()} (and + * their overloads). + * @since 3.5.3 */ public static void enableAutomaticContextPropagation() { if (ContextPropagationSupport.isContextPropagationOnClasspath) { - Hooks.addQueueWrapper( - CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.ContextQueue::new - ); - Schedulers.onScheduleHook( - CONTEXT_IN_THREAD_LOCALS_KEY, - ContextPropagation.scopePassingOnScheduleHook() - ); + Hooks.addQueueWrapper(CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.ContextQueue::new); + Schedulers.onScheduleHook(CONTEXT_IN_THREAD_LOCALS_KEY, + ContextPropagation.scopePassingOnScheduleHook()); ContextPropagationSupport.propagateContextToThreadLocals = true; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 993884cdbf..9e482626b7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -1705,7 +1705,9 @@ public final Mono and(Publisher other) { */ @Nullable public T block() { - BlockingMonoSubscriber subscriber = new BlockingMonoSubscriber<>(); + Context context = ContextPropagationSupport.shouldPropagateContextToThreadLocals() + ? ContextPropagation.contextCaptureToEmpty() : Context.empty(); + BlockingMonoSubscriber subscriber = new BlockingMonoSubscriber<>(context); subscribe((Subscriber) subscriber); return subscriber.blockingGet(); } @@ -1729,7 +1731,9 @@ public T block() { */ @Nullable public T block(Duration timeout) { - BlockingMonoSubscriber subscriber = new BlockingMonoSubscriber<>(); + Context context = ContextPropagationSupport.shouldPropagateContextToThreadLocals() + ? ContextPropagation.contextCaptureToEmpty() : Context.empty(); + BlockingMonoSubscriber subscriber = new BlockingMonoSubscriber<>(context); subscribe((Subscriber) subscriber); return subscriber.blockingGet(timeout.toNanos(), TimeUnit.NANOSECONDS); } @@ -1750,7 +1754,10 @@ public T block(Duration timeout) { * @return T the result */ public Optional blockOptional() { - BlockingOptionalMonoSubscriber subscriber = new BlockingOptionalMonoSubscriber<>(); + Context context = ContextPropagationSupport.shouldPropagateContextToThreadLocals() + ? ContextPropagation.contextCaptureToEmpty() : Context.empty(); + BlockingOptionalMonoSubscriber subscriber = + new BlockingOptionalMonoSubscriber<>(context); subscribe((Subscriber) subscriber); return subscriber.blockingGet(); } @@ -1775,7 +1782,10 @@ public Optional blockOptional() { * @return T the result */ public Optional blockOptional(Duration timeout) { - BlockingOptionalMonoSubscriber subscriber = new BlockingOptionalMonoSubscriber<>(); + Context context = ContextPropagationSupport.shouldPropagateContextToThreadLocals() + ? ContextPropagation.contextCaptureToEmpty() : Context.empty(); + BlockingOptionalMonoSubscriber subscriber = + new BlockingOptionalMonoSubscriber<>(context); subscribe((Subscriber) subscriber); return subscriber.blockingGet(timeout.toNanos(), TimeUnit.NANOSECONDS); } diff --git a/reactor-core/src/test/java/reactor/core/publisher/BlockingIterableTest.java b/reactor-core/src/test/java/reactor/core/publisher/BlockingIterableTest.java index a7d0fd9dc2..bfd1044ed8 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/BlockingIterableTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/BlockingIterableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -34,6 +33,7 @@ import reactor.core.Scannable.Attr; import reactor.test.StepVerifier; import reactor.util.concurrent.Queues; +import reactor.util.context.Context; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -146,7 +146,8 @@ public void streamParallel() { @Test public void scanOperator() { Flux source = Flux.range(1, 10); - BlockingIterable test = new BlockingIterable<>(source, 35, Queues.one()); + BlockingIterable test = new BlockingIterable<>( + source, 35, Queues.one(), Context::empty); assertThat(test.scanUnsafe(Scannable.Attr.PARENT)).describedAs("PARENT").isSameAs(source); assertThat(test.scan(Attr.RUN_STYLE)).isSameAs(Attr.RunStyle.SYNC); @@ -159,9 +160,8 @@ public void scanOperator() { @Test public void scanOperatorLargePrefetchIsLimitedToIntMax() { Flux source = Flux.range(1, 10); - BlockingIterable test = new BlockingIterable<>(source, - Integer.MAX_VALUE, - Queues.one()); + BlockingIterable test = new BlockingIterable<>( + source, Integer.MAX_VALUE, Queues.one(), Context::empty); assertThat(test.scan(Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); } @@ -169,7 +169,8 @@ public void scanOperatorLargePrefetchIsLimitedToIntMax() { @Test public void scanSubscriber() { BlockingIterable.SubscriberIterator subscriberIterator = - new BlockingIterable.SubscriberIterator<>(Queues.one().get(), 123); + new BlockingIterable.SubscriberIterator<>(Queues.one().get(), + Context.empty(), 123); Subscription s = Operators.emptySubscription(); subscriberIterator.onSubscribe(s); @@ -187,7 +188,7 @@ public void scanSubscriber() { public void scanSubscriberLargePrefetchIsLimitedToIntMax() { BlockingIterable.SubscriberIterator subscriberIterator = new BlockingIterable.SubscriberIterator<>(Queues.one().get(), - Integer.MAX_VALUE); + Context.empty(), Integer.MAX_VALUE); assertThat(subscriberIterator.scan(Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); //FIXME } @@ -195,7 +196,8 @@ public void scanSubscriberLargePrefetchIsLimitedToIntMax() { @Test public void scanSubscriberTerminated() { BlockingIterable.SubscriberIterator test = - new BlockingIterable.SubscriberIterator<>(Queues.one().get(), 123); + new BlockingIterable.SubscriberIterator<>(Queues.one().get(), + Context.empty(), 123); assertThat(test.scan(Scannable.Attr.TERMINATED)).describedAs("before TERMINATED").isFalse(); @@ -207,8 +209,7 @@ public void scanSubscriberTerminated() { @Test public void scanSubscriberError() { BlockingIterable.SubscriberIterator test = new BlockingIterable.SubscriberIterator<>( - Queues.one().get(), - 123); + Queues.one().get(), Context.empty(), 123); IllegalStateException error = new IllegalStateException("boom"); assertThat(test.scan(Scannable.Attr.ERROR)).describedAs("before ERROR") @@ -224,8 +225,7 @@ public void scanSubscriberError() { @Test public void scanSubscriberCancelled() { BlockingIterable.SubscriberIterator test = new BlockingIterable.SubscriberIterator<>( - Queues.one().get(), - 123); + Queues.one().get(), Context.empty(), 123); //simulate cancellation by offering two elements test.onNext("a"); diff --git a/reactor-core/src/test/java/reactor/core/publisher/BlockingOptionalMonoSubscriberTest.java b/reactor-core/src/test/java/reactor/core/publisher/BlockingOptionalMonoSubscriberTest.java index 7e0d0df66e..deb5061475 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/BlockingOptionalMonoSubscriberTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/BlockingOptionalMonoSubscriberTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; import reactor.core.Scannable; +import reactor.util.context.Context; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -107,7 +108,8 @@ public void timeoutOptionalTimingOut() { @Test public void isDisposedBecauseCancelled() { - BlockingOptionalMonoSubscriber test = new BlockingOptionalMonoSubscriber<>(); + BlockingOptionalMonoSubscriber test = + new BlockingOptionalMonoSubscriber<>(Context.empty()); assertThat(test.isDisposed()).isFalse(); @@ -118,7 +120,8 @@ public void isDisposedBecauseCancelled() { @Test public void isDisposedBecauseValued() { - BlockingOptionalMonoSubscriber test = new BlockingOptionalMonoSubscriber<>(); + BlockingOptionalMonoSubscriber test = + new BlockingOptionalMonoSubscriber<>(Context.empty()); assertThat(test.isDisposed()).isFalse(); @@ -129,7 +132,8 @@ public void isDisposedBecauseValued() { @Test public void isDisposedBecauseComplete() { - BlockingOptionalMonoSubscriber test = new BlockingOptionalMonoSubscriber<>(); + BlockingOptionalMonoSubscriber test = + new BlockingOptionalMonoSubscriber<>(Context.empty()); assertThat(test.isDisposed()).isFalse(); @@ -140,7 +144,8 @@ public void isDisposedBecauseComplete() { @Test public void isDisposedBecauseError() { - BlockingOptionalMonoSubscriber test = new BlockingOptionalMonoSubscriber<>(); + BlockingOptionalMonoSubscriber test = + new BlockingOptionalMonoSubscriber<>(Context.empty()); assertThat(test.isDisposed()).isFalse(); @@ -151,7 +156,8 @@ public void isDisposedBecauseError() { @Test public void scanOperator() { - BlockingOptionalMonoSubscriber test = new BlockingOptionalMonoSubscriber<>(); + BlockingOptionalMonoSubscriber test = + new BlockingOptionalMonoSubscriber<>(Context.empty()); Subscription parent = Operators.emptySubscription(); test.onSubscribe(parent); @@ -178,7 +184,8 @@ public void scanOperator() { @Test public void interruptBlock() throws InterruptedException { - BlockingOptionalMonoSubscriber test = new BlockingOptionalMonoSubscriber<>(); + BlockingOptionalMonoSubscriber test = + new BlockingOptionalMonoSubscriber<>(Context.empty()); AtomicReference errorHandler = new AtomicReference<>(); Thread t = new Thread(test::blockingGet); @@ -197,7 +204,8 @@ public void interruptBlock() throws InterruptedException { @Test public void interruptBlockTimeout() throws InterruptedException { - BlockingOptionalMonoSubscriber test = new BlockingOptionalMonoSubscriber<>(); + BlockingOptionalMonoSubscriber test = + new BlockingOptionalMonoSubscriber<>(Context.empty()); AtomicReference errorHandler = new AtomicReference<>(); Thread t = new Thread(() -> test.blockingGet(2, TimeUnit.SECONDS)); diff --git a/reactor-core/src/test/java/reactor/core/publisher/BlockingSingleSubscriberTest.java b/reactor-core/src/test/java/reactor/core/publisher/BlockingSingleSubscriberTest.java index 2b0a0e57a3..25d0754a4b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/BlockingSingleSubscriberTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/BlockingSingleSubscriberTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,14 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; import reactor.core.Scannable; +import reactor.util.context.Context; import static org.assertj.core.api.Assertions.assertThat; public class BlockingSingleSubscriberTest { - BlockingSingleSubscriber test = new BlockingSingleSubscriber() { + BlockingSingleSubscriber test = + new BlockingSingleSubscriber(Context.empty()) { @Override public void onNext(Object o) { } diff --git a/reactor-core/src/test/java/reactor/core/publisher/OperatorsTest.java b/reactor-core/src/test/java/reactor/core/publisher/OperatorsTest.java index fcc6088fea..b50355b1f1 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/OperatorsTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/OperatorsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1000,7 +1000,7 @@ void meaningfulCancelledSubscriptionStepName() { @Test void meaningfulScalarSubscriptionStepName() { - assertThat(Scannable.from(Operators.scalarSubscription(new BlockingFirstSubscriber<>(), "foo")).stepName()).isEqualTo("scalarSubscription(foo)"); + assertThat(Scannable.from(Operators.scalarSubscription(new BlockingFirstSubscriber<>(Context.empty()), "foo")).stepName()).isEqualTo("scalarSubscription(foo)"); } @Test diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index 8b93a669ae..d581561522 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -982,4 +982,125 @@ void monoHandleVariantsCallTheWrapper() { }); } } + + @Nested + class BlockingOperatorsAutoCapture { + + @Test + void monoBlock() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Mono.just("test") + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnNext(ignored -> value.set(REF1.get())) + .block(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void monoBlockOptional() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Mono.empty() + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF1.get())) + .blockOptional(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxBlockFirst() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnNext(ignored -> value.set(REF1.get())) + .blockFirst(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxBlockLast() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF1.get())) + .blockLast(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxToIterable() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Iterable integers = Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF1.get())) + .toIterable(); + + assertThat(integers).hasSize(10); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + } } From ab255c5ca8c184ebb8b2acf70357294c00ca0d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 7 Jun 2023 11:46:53 +0200 Subject: [PATCH 119/312] context-propagation: use ContextSnapshotFactory (#3489) This change removes the custom logic for setting ThreadLocal values. context-propagation library did not allow removing values missing in the Reactor Context - a feature necessary in order to avoid leaking ThreadLocals. Since version 1.0.3 of context-propagation this feature made it into the public API and now reactor-core is able to use it. --- gradle/libs.versions.toml | 2 +- .../core/publisher/ContextPropagation.java | 162 ++++-------------- .../java/reactor/core/publisher/Hooks.java | 1 + .../ContextPropagationNotThereSmokeTest.java | 6 - .../publisher/ContextPropagationTest.java | 41 +---- 5 files changed, 46 insertions(+), 166 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ff58c6bf2..b7bb3b6a2d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ jsr305 = "com.google.code.findbugs:jsr305:3.0.1" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.3-SNAPSHOT" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.3" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.4" diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index a19faf32b4..64cd60cef1 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -17,20 +17,17 @@ package reactor.core.publisher; import java.util.AbstractQueue; -import java.util.HashMap; import java.util.Iterator; -import java.util.Map; import java.util.Queue; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -import io.micrometer.context.ContextAccessor; import io.micrometer.context.ContextRegistry; import io.micrometer.context.ContextSnapshot; -import io.micrometer.context.ThreadLocalAccessor; +import io.micrometer.context.ContextSnapshotFactory; import reactor.core.observability.SignalListener; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -48,14 +45,31 @@ final class ContextPropagation { static final Function NO_OP = c -> c; static final Function WITH_GLOBAL_REGISTRY_NO_PREDICATE; + static ContextSnapshotFactory globalContextSnapshotFactory = null; + static { WITH_GLOBAL_REGISTRY_NO_PREDICATE = ContextPropagationSupport.isContextPropagationAvailable() ? - new ContextCaptureNoPredicate(ContextRegistry.getInstance()) : NO_OP; + new ContextCaptureNoPredicate() : NO_OP; + + if (ContextPropagationSupport.isContextPropagationAvailable()) { + globalContextSnapshotFactory = ContextSnapshotFactory.builder() + .clearMissing(false) + .build(); + } + } + + static void configureContextSnapshotFactory(boolean clearMissing) { + globalContextSnapshotFactory = + ContextSnapshotFactory.builder().clearMissing(clearMissing).build(); + } + + static ContextSnapshot.Scope setThreadLocals(Object context) { + return globalContextSnapshotFactory.setThreadLocalsFrom(context); } public static Function scopePassingOnScheduleHook() { return delegate -> { - ContextSnapshot contextSnapshot = ContextSnapshot.captureAll(); + ContextSnapshot contextSnapshot = globalContextSnapshotFactory.captureAll(); return contextSnapshot.wrap(delegate); }; } @@ -79,30 +93,6 @@ static Context contextCaptureToEmpty() { return contextCapture().apply(Context.empty()); } - /** - * Create a support function that takes a snapshot of thread locals and merges them with the - * provided {@link Context}, resulting in a new {@link Context} which includes entries - * captured from threadLocals by the Context-Propagation API. - *

    - * The provided {@link Predicate} is used on keys associated to said thread locals - * by the Context-Propagation API to filter which entries should be captured in the - * first place. - *

    - * This variant uses the implicit global {@code ContextRegistry} and captures only from - * available {@code ThreadLocalAccessors} that match the {@link Predicate}. - * - * @param captureKeyPredicate a {@link Predicate} used on keys to determine if each entry - * should be injected into the new {@link Context} - * @return a {@link Function} augmenting {@link Context} with captured entries - */ - static Function contextCapture(Predicate captureKeyPredicate) { - if (!ContextPropagationSupport.isContextPropagationOnClasspath) { - return NO_OP; - } - return target -> ContextSnapshot.captureAllUsing(captureKeyPredicate, ContextRegistry.getInstance()) - .updateContext(target); - } - /** * When context-propagation library * is available on the classpath, the provided {@link BiConsumer handler} will be @@ -121,7 +111,7 @@ static BiConsumer> contextRestoreForHandle(BiConsum return handler; } return (v, sink) -> { - try (ContextSnapshot.Scope ignored = ContextSnapshot.setAllThreadLocalsFrom(ctx)) { + try (ContextSnapshot.Scope ignored = globalContextSnapshotFactory.setThreadLocalsFrom(ctx)) { handler.accept(v, sink); } }; @@ -154,7 +144,7 @@ static SignalListener contextRestoreForTap(final SignalListener origin if (ctx.isEmpty()) { return original; } - return new ContextRestoreSignalListener(original, ctx, null); + return new ContextRestoreSignalListener(original, ctx, globalContextSnapshotFactory); } //the SignalListener implementation can be tested independently with a test-specific ContextRegistry @@ -162,16 +152,17 @@ static final class ContextRestoreSignalListener implements SignalListener final SignalListener original; final ContextView context; - final ContextRegistry registry; + final ContextSnapshotFactory contextSnapshotFactory; - public ContextRestoreSignalListener(SignalListener original, ContextView context, @Nullable ContextRegistry registry) { + public ContextRestoreSignalListener(SignalListener original, + ContextView context, ContextSnapshotFactory contextSnapshotFactory) { this.original = original; this.context = context; - this.registry = registry == null ? ContextRegistry.getInstance() : registry; + this.contextSnapshotFactory = contextSnapshotFactory; } - + ContextSnapshot.Scope restoreThreadLocals() { - return ContextSnapshot.setAllThreadLocalsFrom(this.context, this.registry); + return contextSnapshotFactory.setThreadLocalsFrom(this.context); } @Override @@ -287,6 +278,14 @@ public Context addToContext(Context originalContext) { } } + static final class ContextCaptureNoPredicate implements Function { + + @Override + public Context apply(Context context) { + return globalContextSnapshotFactory.captureAll().updateContext(context); + } + } + static final class ContextQueue extends AbstractQueue { static final String NOT_SUPPORTED_MESSAGE = "ContextQueue wrapper is intended " + @@ -313,7 +312,7 @@ public int size() { @Override public boolean offer(T o) { - ContextSnapshot contextSnapshot = ContextSnapshot.captureAll(); + ContextSnapshot contextSnapshot = globalContextSnapshotFactory.captureAll(); return envelopeQueue.offer(new Envelope<>(o, contextSnapshot)); } @@ -340,7 +339,7 @@ public T poll() { private void restoreTheContext(Envelope envelope) { ContextSnapshot contextSnapshot = envelope.contextSnapshot; // tries to read existing Thread for existing ThreadLocals - ContextSnapshot currentContextSnapshot = ContextSnapshot.captureAll(); + ContextSnapshot currentContextSnapshot = globalContextSnapshotFactory.captureAll(); if (!contextSnapshot.equals(currentContextSnapshot)) { if (!hasPrevious || !Thread.currentThread().equals(this.lastReader)) { // means context was restored form the envelope, @@ -380,90 +379,5 @@ static class Envelope { this.body = body; this.contextSnapshot = contextSnapshot; } - - } - - static final class ContextCaptureNoPredicate implements Function { - final ContextRegistry globalRegistry; - - ContextCaptureNoPredicate(ContextRegistry globalRegistry) { - this.globalRegistry = globalRegistry; - } - @Override - public Context apply(Context context) { - return ContextSnapshot.captureAllUsing(PREDICATE_TRUE, globalRegistry) - .updateContext(context); - } - } - - /* - * Temporary methods not present in context-propagation library that allow - * clearing ThreadLocals not present in Reactor Context. Once context-propagation - * library adds the ability to do this, they can be removed from reactor-core. - */ - - @SuppressWarnings("unchecked") - static ContextSnapshot.Scope setThreadLocals(Object context) { - ContextRegistry registry = ContextRegistry.getInstance(); - ContextAccessor contextAccessor = registry.getContextAccessorForRead(context); - Map previousValues = null; - for (ThreadLocalAccessor threadLocalAccessor : registry.getThreadLocalAccessors()) { - Object key = threadLocalAccessor.key(); - Object value = ((ContextAccessor) contextAccessor).readValue((C) context, key); - previousValues = setThreadLocal(key, value, threadLocalAccessor, previousValues); - } - return ReactorScopeImpl.from(previousValues, registry); - } - - @SuppressWarnings("unchecked") - private static Map setThreadLocal(Object key, @Nullable V value, - ThreadLocalAccessor accessor, @Nullable Map previousValues) { - - previousValues = (previousValues != null ? previousValues : new HashMap<>()); - previousValues.put(key, accessor.getValue()); - if (value != null) { - ((ThreadLocalAccessor) accessor).setValue(value); - } else { - accessor.setValue(); - } - return previousValues; - } - - private static class ReactorScopeImpl implements ContextSnapshot.Scope { - - private final Map previousValues; - - private final ContextRegistry contextRegistry; - - private ReactorScopeImpl(Map previousValues, - ContextRegistry contextRegistry) { - this.previousValues = previousValues; - this.contextRegistry = contextRegistry; - } - - @Override - public void close() { - for (ThreadLocalAccessor accessor : this.contextRegistry.getThreadLocalAccessors()) { - if (this.previousValues.containsKey(accessor.key())) { - Object previousValue = this.previousValues.get(accessor.key()); - resetThreadLocalValue(accessor, previousValue); - } - } - } - - @SuppressWarnings("unchecked") - private void resetThreadLocalValue(ThreadLocalAccessor accessor, @Nullable V previousValue) { - if (previousValue != null) { - ((ThreadLocalAccessor) accessor).restore(previousValue); - } - else { - accessor.restore(); - } - } - - public static ContextSnapshot.Scope from(@Nullable Map previousValues, ContextRegistry registry) { - return (previousValues != null ? new ReactorScopeImpl(previousValues, registry) : () -> { - }); - } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java index d3d8391322..1b73aa56bd 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java @@ -546,6 +546,7 @@ public static void enableAutomaticContextPropagation() { Schedulers.onScheduleHook(CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.scopePassingOnScheduleHook()); ContextPropagationSupport.propagateContextToThreadLocals = true; + ContextPropagation.configureContextSnapshotFactory(true); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java b/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java index 1632ad1590..17fcfd361b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ContextPropagationNotThereSmokeTest.java @@ -32,12 +32,6 @@ void contextPropagationIsNotAvailable() { assertThat(ContextPropagationSupport.isContextPropagationAvailable()).isFalse(); } - @Test - void contextCaptureIsNoOp() { - assertThat(ContextPropagation.contextCapture()).as("without predicate").isSameAs(ContextPropagation.NO_OP); - assertThat(ContextPropagation.contextCapture(v -> true)).as("with predicate").isSameAs(ContextPropagation.NO_OP); - } - @Test void contextCaptureFluxApiIsNoOp() { Flux source = Flux.empty(); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index d581561522..aeb8808411 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -31,13 +31,12 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Flow; -import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; import io.micrometer.context.ContextRegistry; +import io.micrometer.context.ContextSnapshotFactory; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -47,9 +46,7 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; -import org.reactivestreams.FlowAdapters; import org.reactivestreams.Publisher; -import reactor.adapter.JdkFlowAdapter; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.core.Scannable; @@ -338,19 +335,6 @@ void contextCaptureWithNoPredicateReturnsTheConstantFunction() { .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE); } - @Test - void contextCaptureWithPredicateReturnsNewFunctionWithGlobalRegistry() { - Function test = ContextPropagation.contextCapture(ContextPropagation.PREDICATE_TRUE); - - assertThat(test) - .as("predicate, no registry") - .isNotNull() - .isNotSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) - .isNotSameAs(ContextPropagation.NO_OP) - // as long as a predicate is supplied, the method creates new instances of the Function - .isNotSameAs(ContextPropagation.contextCapture(ContextPropagation.PREDICATE_TRUE)); - } - @Test void fluxApiUsesContextPropagationConstantFunction() { Flux source = Flux.empty(); @@ -415,22 +399,6 @@ void contextCaptureFunctionWithoutFiltering() { .containsEntry(KEY2, "expected2") .hasSize(2); } - - @Test - void captureWithFiltering() { - Function test = ContextPropagation.contextCapture(k -> k.toString().equals(KEY2)); - - REF1.set("not_expected"); - REF2.set("expected"); - - Context ctx = test.apply(Context.empty()); - Map asMap = new HashMap<>(); - ctx.forEach(asMap::put); //easier to assert - - assertThat(asMap) - .containsEntry(KEY2, "expected") - .hasSize(1); - } } @Nested @@ -818,8 +786,11 @@ void threadLocalRestoredInSignalListener() throws InterruptedException { }); ContextPropagation.ContextRestoreSignalListener listener = - new ContextPropagation.ContextRestoreSignalListener<>(tlReadingListener, context, - null); + new ContextPropagation.ContextRestoreSignalListener<>( + tlReadingListener, context, + ContextSnapshotFactory.builder() + .contextRegistry(ContextRegistry.getInstance()) + .build()); Thread t = new Thread(() -> { try { From 59f7cb398d0bb145b35fa4b63f4ccd285a8acee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 7 Jun 2023 13:44:31 +0200 Subject: [PATCH 120/312] Buffer timeout with fair backpressure (#3332) This change adds a variant of bufferTimeout that honors backpressure of a slow downstream Subscriber better than the current implementation (which just errors in the face of backpressure and timeouts). The problem with the existing implementation is that it requests n * maxSize items from the upstream with the assumption that the items will fill a buffer which was requested. However, if timeouts occur, the amount of buffers created and delivered can exceed the accumulated demand. In such case, a backpressure error happens. The new variant uses prefetching with 4 * maxSize items from the upstream. Internally it uses a queue, which is used to satisfy the downstream demand with no more buffers than have been requested. It uses a watermark to replenish the queue to be ready to deliver buffers in an efficient manner when the demand increases. --- .../FluxBufferTimeoutStressTest.java | 421 ++++++++++++++++ .../java/reactor/core/publisher/Flux.java | 97 +++- .../core/publisher/FluxBufferTimeout.java | 462 ++++++++++++++++- ...FluxBufferTimeoutFairBackpressureTest.java | 469 ++++++++++++++++++ .../core/publisher/FluxBufferTimeoutTest.java | 8 +- .../publisher/FluxMergeSequentialTest.java | 38 ++ .../publisher/OnDiscardShouldNotLeakTest.java | 4 +- .../test/java/reactor/test/MemoryUtils.java | 13 +- 8 files changed, 1497 insertions(+), 15 deletions(-) create mode 100644 reactor-core/src/jcstress/java/reactor/core/publisher/FluxBufferTimeoutStressTest.java create mode 100644 reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutFairBackpressureTest.java diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxBufferTimeoutStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxBufferTimeoutStressTest.java new file mode 100644 index 0000000000..d19e1122e4 --- /dev/null +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxBufferTimeoutStressTest.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.Expect; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.LLL_Result; +import reactor.core.util.FastLogger; +import reactor.test.scheduler.VirtualTimeScheduler; + +public class FluxBufferTimeoutStressTest { + + @JCStressTest + @Outcome(id = "1, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "2, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @State + public static class FluxBufferTimeoutStressTestRaceDeliveryAndTimeout { + + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); + + final StressSubscriber> subscriber = new StressSubscriber<>(); + + final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), null); + + final StressSubscription subscription = new StressSubscription<>(bufferTimeoutSubscriber); + + { + bufferTimeoutSubscriber.onSubscribe(subscription); + } + + @Actor + public void next() { + bufferTimeoutSubscriber.onNext(0L); + bufferTimeoutSubscriber.onNext(1L); + bufferTimeoutSubscriber.onComplete(); + } + + @Actor + public void timeout() { + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + } + + @Arbiter + public void arbiter(LLL_Result result) { + result.r1 = subscriber.onNextCalls.get(); + result.r2 = subscriber.onCompleteCalls.get(); + result.r3 = subscription.requestsCount.get(); + + if (subscriber.onCompleteCalls.get() > 1) { + throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + } + if (subscriber.concurrentOnComplete.get()) { + throw new IllegalStateException("subscriber concurrent onComplete"); + } + if (subscriber.concurrentOnNext.get()) { + throw new IllegalStateException("subscriber concurrent onNext"); + } + } + } + + @JCStressTest + @Outcome(id = "3, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "4, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @State + public static class FluxBufferTimeoutStressTestRaceDeliveryAndMoreTimeouts { + + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); + + final StressSubscriber> subscriber = new StressSubscriber<>(); + + final FastLogger fastLogger = new FastLogger(getClass().getName()); + final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); + + final StressSubscription subscription = new StressSubscription<>(bufferTimeoutSubscriber); + + { + bufferTimeoutSubscriber.onSubscribe(subscription); + } + + @Actor + public void next() { + bufferTimeoutSubscriber.onNext(0L); + bufferTimeoutSubscriber.onNext(1L); + bufferTimeoutSubscriber.onNext(2L); + bufferTimeoutSubscriber.onNext(3L); + bufferTimeoutSubscriber.onNext(4L); + + bufferTimeoutSubscriber.onComplete(); + } + + @Actor + public void timeout() { + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + } + + @Arbiter + public void arbiter(LLL_Result result) { + result.r1 = subscriber.onNextCalls.get(); + result.r2 = subscriber.onCompleteCalls.get(); + result.r3 = subscription.requestsCount.get(); + + if (subscriber.onCompleteCalls.get() != 1) { + throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + } + if (subscriber.concurrentOnComplete.get()) { + throw new IllegalStateException("subscriber concurrent onComplete"); + } + if (subscriber.concurrentOnNext.get()) { + throw new IllegalStateException("subscriber concurrent onNext"); + } + if (subscriber.receivedValues.stream().anyMatch(List::isEmpty)) { + throw new IllegalStateException("received an empty buffer: " + subscriber.receivedValues + "; result=" + result + "\n" + fastLogger); + } + } + } + + @JCStressTest + @Outcome(id = "5, 1, 2", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 1, 3", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 1, 4", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 1, 5", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 0, 2", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 0, 3", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 0, 4", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 0, 5", expect = Expect.ACCEPTABLE, desc = "") + @State + public static class FluxBufferTimeoutStressTestRaceDeliveryAndMoreTimeoutsPossiblyIncomplete { + + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); + + final StressSubscriber> subscriber = new StressSubscriber<>(1); + + final FastLogger fastLogger = new FastLogger(getClass().getName()); + final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); + + Sinks.Many proxy = Sinks.unsafe().many().unicast().onBackpressureBuffer(); + final AtomicLong requested = new AtomicLong(); + { + proxy.asFlux() + .doOnRequest(r -> requested.incrementAndGet()) + .subscribe(bufferTimeoutSubscriber); + } + + @Actor + public void next() { + proxy.tryEmitNext(0L); + proxy.tryEmitNext(1L); + proxy.tryEmitNext(2L); + proxy.tryEmitNext(3L); + proxy.tryEmitNext(4L); + + proxy.tryEmitNext(5L); + proxy.tryEmitNext(6L); + proxy.tryEmitNext(7L); + proxy.tryEmitNext(8L); + proxy.tryEmitNext(9L); + proxy.tryEmitComplete(); + } + + @Actor + public void timeout() { + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + } + + @Actor + public void request() { + subscriber.request(1); + subscriber.request(1); + subscriber.request(1); + subscriber.request(1); + } + + @Arbiter + public void arbiter(LLL_Result result) { + result.r1 = subscriber.onNextCalls.get(); + result.r2 = subscriber.onCompleteCalls.get(); + result.r3 = requested.get(); + + if (subscriber.onCompleteCalls.get() == 0) { + if (subscriber.receivedValues.stream() + .noneMatch(buf -> buf.size() == 1)) { + throw new IllegalStateException("incomplete but received all two " + + "element buffers. received: " + subscriber.receivedValues + "; result=" + result + "\n" + fastLogger); + } + } + + if (subscriber.onNextCalls.get() < 5 && subscriber.onCompleteCalls.get() == 0) { + throw new IllegalStateException("incomplete. received: " + subscriber.receivedValues + "; requested=" + requested.get() + "; result=" + result + "\n" + fastLogger); + } + + if (subscriber.onCompleteCalls.get() > 1) { + throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + } + if (subscriber.concurrentOnComplete.get()) { + throw new IllegalStateException("subscriber concurrent onComplete"); + } + if (subscriber.concurrentOnNext.get()) { + throw new IllegalStateException("subscriber concurrent onNext"); + } + if (subscriber.receivedValues.stream().anyMatch(List::isEmpty)) { + throw new IllegalStateException("received an empty buffer: " + subscriber.receivedValues + "; result=" + result + "\n" + fastLogger); + } + } + } + + @JCStressTest + @Outcome(id = "1, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "0, 0, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "1, 0, 1", expect = Expect.ACCEPTABLE, desc = "") + @State + public static class FluxBufferTimeoutStressTestRaceDeliveryAndCancel { + + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); + + final StressSubscriber> subscriber = new StressSubscriber<>(); + + final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), null); + + final StressSubscription subscription = new StressSubscription<>(bufferTimeoutSubscriber); + + { + bufferTimeoutSubscriber.onSubscribe(subscription); + } + + @Actor + public void next() { + bufferTimeoutSubscriber.onNext(0L); + bufferTimeoutSubscriber.onNext(1L); + bufferTimeoutSubscriber.onComplete(); + } + + @Actor + public void cancel() { + bufferTimeoutSubscriber.cancel(); + } + + @Arbiter + public void arbiter(LLL_Result result) { + result.r1 = subscriber.onNextCalls.get(); + result.r2 = subscriber.onCompleteCalls.get(); + result.r3 = subscription.requestsCount.get(); + + if (subscriber.onCompleteCalls.get() > 1) { + throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + } + if (subscriber.concurrentOnComplete.get()) { + throw new IllegalStateException("subscriber concurrent onComplete"); + } + if (subscriber.concurrentOnNext.get()) { + throw new IllegalStateException("subscriber concurrent onNext"); + } + } + } + + @JCStressTest + @Outcome(id = "0, 0, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "1, 0, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "1, 0, 2", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "2, 0, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "2, 0, 2", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "2, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "2, 1, 2", expect = Expect.ACCEPTABLE, desc = "") + @State + public static class FluxBufferTimeoutStressTestRaceDeliveryAndCancelWithBackpressure { + + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); + + final StressSubscriber> subscriber = new StressSubscriber<>(1); + + final FastLogger fastLogger = new FastLogger(getClass().getName()); + final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); + + Sinks.Many proxy = Sinks.unsafe().many().unicast().onBackpressureBuffer(); + final AtomicLong requested = new AtomicLong(); + { + proxy.asFlux() + .doOnRequest(r -> requested.incrementAndGet()) + .subscribe(bufferTimeoutSubscriber); + } + + @Actor + public void next() { + proxy.tryEmitNext(0L); + proxy.tryEmitNext(1L); + + proxy.tryEmitNext(2L); + proxy.tryEmitNext(3L); + proxy.tryEmitComplete(); + } + + @Actor + public void request() { + subscriber.request(1); + } + + @Actor + public void cancel() { + subscriber.cancel(); + } + + @Arbiter + public void arbiter(LLL_Result result) { + result.r1 = subscriber.onNextCalls.get(); + result.r2 = subscriber.onCompleteCalls.get(); + result.r3 = requested.get(); + + if (subscriber.onCompleteCalls.get() > 1) { + throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + } + if (subscriber.concurrentOnComplete.get()) { + throw new IllegalStateException("subscriber concurrent onComplete"); + } + if (subscriber.concurrentOnNext.get()) { + throw new IllegalStateException("subscriber concurrent onNext"); + } + if (subscriber.receivedValues.stream().anyMatch(List::isEmpty)) { + throw new IllegalStateException("received an empty buffer: " + subscriber.receivedValues + "; result=" + result + "\n" + fastLogger); + } + } + } + + @JCStressTest + @Outcome(id = "1, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "2, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "0, 0, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "1, 0, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "2, 0, 1", expect = Expect.ACCEPTABLE, desc = "") + @State + public static class FluxBufferTimeoutStressTestRaceDeliveryAndCancelAndTimeout { + + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); + + final StressSubscriber> subscriber = new StressSubscriber<>(); + + final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), null); + + final StressSubscription subscription = new StressSubscription<>(bufferTimeoutSubscriber); + + { + bufferTimeoutSubscriber.onSubscribe(subscription); + } + + @Actor + public void next() { + bufferTimeoutSubscriber.onNext(0L); + bufferTimeoutSubscriber.onNext(1L); + bufferTimeoutSubscriber.onComplete(); + } + + @Actor + public void cancel() { + bufferTimeoutSubscriber.cancel(); + } + + @Actor + public void timeout() { + virtualTimeScheduler.advanceTimeBy(Duration.ofSeconds(1)); + } + + @Arbiter + public void arbiter(LLL_Result result) { + result.r1 = subscriber.onNextCalls.get(); + result.r2 = subscriber.onCompleteCalls.get(); + result.r3 = subscription.requestsCount.get(); + + if (subscriber.onCompleteCalls.get() > 1) { + throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + } + if (subscriber.concurrentOnComplete.get()) { + throw new IllegalStateException("subscriber concurrent onComplete"); + } + if (subscriber.concurrentOnNext.get()) { + throw new IllegalStateException("subscriber concurrent onNext"); + } + } + } + + private static Supplier> bufferSupplier() { + return ArrayList::new; + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 51b9ae8dca..923c44eb4a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -44,6 +44,7 @@ import java.util.function.Supplier; import java.util.logging.Level; import java.util.stream.Collector; +import java.util.stream.IntStream; import java.util.stream.Stream; import io.micrometer.core.instrument.MeterRegistry; @@ -3110,7 +3111,101 @@ public final Flux> bufferTimeout(int maxSize, Duration maxTime, Schedule */ public final > Flux bufferTimeout(int maxSize, Duration maxTime, Scheduler timer, Supplier bufferSupplier) { - return onAssembly(new FluxBufferTimeout<>(this, maxSize, maxTime.toNanos(), TimeUnit.NANOSECONDS, timer, bufferSupplier)); + return onAssembly(new FluxBufferTimeout<>(this, maxSize, maxTime.toNanos(), TimeUnit.NANOSECONDS, timer, bufferSupplier, + false)); + } + + /** + * Collect incoming values into multiple {@link List} buffers that will be emitted + * by the returned {@link Flux} each time the buffer reaches a maximum size OR the + * maxTime {@link Duration} elapses. + *

    + * + * + *

    Discard Support: This operator discards the currently open buffer upon cancellation or error triggered by a data signal. + * + * @param maxSize the max collected size + * @param maxTime the timeout enforcing the release of a partial buffer + * @param fairBackpressure If {@code true}, prefetches {@code maxSize * 4} from upstream and replenishes the buffer when the downstream demand is satisfactory. + * When {@code false}, no prefetching takes place and a single buffer is always ready to be pushed downstream. + * + * @return a microbatched {@link Flux} of {@link List} delimited by given size or a given period timeout + */ + public final Flux> bufferTimeout(int maxSize, + Duration maxTime, boolean fairBackpressure) { + return bufferTimeout(maxSize, maxTime, Schedulers.parallel(), + listSupplier(), fairBackpressure); + } + + /** + * Collect incoming values into multiple {@link List} buffers that will be emitted + * by the returned {@link Flux} each time the buffer reaches a maximum size OR the + * maxTime {@link Duration} elapses, as measured on the provided {@link Scheduler}. + *

    + * + * + *

    Discard Support: This operator discards the currently open buffer upon cancellation or error triggered by a data signal. + * + * @param maxSize the max collected size + * @param maxTime the timeout enforcing the release of a partial buffer + * @param timer a time-capable {@link Scheduler} instance to run on + * @param fairBackpressure If {@code true}, prefetches {@code maxSize * 4} from upstream and replenishes the buffer when the downstream demand is satisfactory. + * When {@code false}, no prefetching takes place and a single buffer is always ready to be pushed downstream. + * + * @return a microbatched {@link Flux} of {@link List} delimited by given size or a given period timeout + */ + public final Flux> bufferTimeout(int maxSize, Duration maxTime, + Scheduler timer, boolean fairBackpressure) { + return bufferTimeout(maxSize, maxTime, timer, listSupplier(), fairBackpressure); + } + + /** + * Collect incoming values into multiple user-defined {@link Collection} buffers that + * will be emitted by the returned {@link Flux} each time the buffer reaches a maximum + * size OR the maxTime {@link Duration} elapses. + *

    + * + * + *

    Discard Support: This operator discards the currently open buffer upon cancellation or error triggered by a data signal. + * + * @param maxSize the max collected size + * @param maxTime the timeout enforcing the release of a partial buffer + * @param bufferSupplier a {@link Supplier} of the concrete {@link Collection} to use for each buffer + * @param the {@link Collection} buffer type + * @param fairBackpressure If {@code true}, prefetches {@code maxSize * 4} from upstream and replenishes the buffer when the downstream demand is satisfactory. + * When {@code false}, no prefetching takes place and a single buffer is always ready to be pushed downstream. + * + * @return a microbatched {@link Flux} of {@link Collection} delimited by given size or a given period timeout + */ + public final > Flux bufferTimeout(int maxSize, Duration maxTime, + Supplier bufferSupplier, boolean fairBackpressure) { + return bufferTimeout(maxSize, maxTime, Schedulers.parallel(), bufferSupplier, + fairBackpressure); + } + + /** + * Collect incoming values into multiple user-defined {@link Collection} buffers that + * will be emitted by the returned {@link Flux} each time the buffer reaches a maximum + * size OR the maxTime {@link Duration} elapses, as measured on the provided {@link Scheduler}. + *

    + * + * + *

    Discard Support: This operator discards the currently open buffer upon cancellation or error triggered by a data signal. + * + * @param maxSize the max collected size + * @param maxTime the timeout enforcing the release of a partial buffer + * @param timer a time-capable {@link Scheduler} instance to run on + * @param bufferSupplier a {@link Supplier} of the concrete {@link Collection} to use for each buffer + * @param the {@link Collection} buffer type + * @param fairBackpressure If {@code true}, prefetches {@code maxSize * 4} from upstream and replenishes the buffer when the downstream demand is satisfactory. + * When {@code false}, no prefetching takes place and a single buffer is always ready to be pushed downstream. + * + * @return a microbatched {@link Flux} of {@link Collection} delimited by given size or a given period timeout + */ + public final > Flux bufferTimeout(int maxSize, Duration maxTime, + Scheduler timer, Supplier bufferSupplier, boolean fairBackpressure) { + return onAssembly(new FluxBufferTimeout<>(this, maxSize, maxTime.toNanos(), TimeUnit.NANOSECONDS, timer, bufferSupplier, + fairBackpressure)); } /** diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java index 09c69eb416..9b2508255a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.Objects; +import java.util.Queue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; @@ -30,7 +31,9 @@ import reactor.core.Disposable; import reactor.core.Exceptions; import reactor.core.scheduler.Scheduler; +import reactor.util.Logger; import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; import reactor.util.context.Context; /** @@ -38,18 +41,20 @@ */ final class FluxBufferTimeout> extends InternalFluxOperator { - final int batchSize; - final Supplier bufferSupplier; - final Scheduler timer; - final long timespan; - final TimeUnit unit; + final int batchSize; + final Supplier bufferSupplier; + final Scheduler timer; + final long timespan; + final TimeUnit unit; + final boolean fairBackpressure; FluxBufferTimeout(Flux source, int maxSize, long timespan, TimeUnit unit, Scheduler timer, - Supplier bufferSupplier) { + Supplier bufferSupplier, + boolean fairBackpressure) { super(source); if (timespan <= 0) { throw new IllegalArgumentException("Timeout period must be strictly positive"); @@ -62,10 +67,20 @@ final class FluxBufferTimeout> extends Intern this.unit = Objects.requireNonNull(unit, "unit"); this.batchSize = maxSize; this.bufferSupplier = Objects.requireNonNull(bufferSupplier, "bufferSupplier"); + this.fairBackpressure = fairBackpressure; } @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { + if (fairBackpressure) { + return new BufferTimeoutWithBackpressureSubscriber<>(actual, + batchSize, + timespan, + unit, + timer.createWorker(), + bufferSupplier, + null); + } return new BufferTimeoutSubscriber<>( Operators.serialize(actual), batchSize, @@ -84,6 +99,435 @@ public Object scanUnsafe(Attr key) { return super.scanUnsafe(key); } + final static class BufferTimeoutWithBackpressureSubscriber> + implements InnerOperator { + + @Nullable + final Logger logger; + final CoreSubscriber actual; + final int batchSize; + final int prefetch; + final long timeSpan; + final TimeUnit unit; + final Scheduler.Worker timer; + final Supplier bufferSupplier; + + // tracks unsatisfied downstream demand (expressed in # of buffers) + volatile long requested; + @SuppressWarnings("rawtypes") + private AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "requested"); + + // tracks undelivered values in the current buffer + volatile int index; + @SuppressWarnings("rawtypes") + private AtomicIntegerFieldUpdater INDEX = + AtomicIntegerFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "index"); + + // tracks # of values requested from upstream but not delivered yet via this + // .onNext(v) + volatile long outstanding; + @SuppressWarnings("rawtypes") + private AtomicLongFieldUpdater OUTSTANDING = + AtomicLongFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "outstanding"); + + // indicates some thread is draining + volatile int wip; + @SuppressWarnings("rawtypes") + private AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "wip"); + + private volatile int terminated = NOT_TERMINATED; + @SuppressWarnings("rawtypes") + private AtomicIntegerFieldUpdater TERMINATED = + AtomicIntegerFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "terminated"); + + final static int NOT_TERMINATED = 0; + final static int TERMINATED_WITH_SUCCESS = 1; + final static int TERMINATED_WITH_ERROR = 2; + final static int TERMINATED_WITH_CANCEL = 3; + + @Nullable + private Subscription subscription; + + private Queue queue; + + @Nullable + Throwable error; + + boolean completed; + + Disposable currentTimeoutTask; + + public BufferTimeoutWithBackpressureSubscriber( + CoreSubscriber actual, + int batchSize, + long timeSpan, + TimeUnit unit, + Scheduler.Worker timer, + Supplier bufferSupplier, + @Nullable Logger logger) { + this.actual = actual; + this.batchSize = batchSize; + this.timeSpan = timeSpan; + this.unit = unit; + this.timer = timer; + this.bufferSupplier = bufferSupplier; + this.logger = logger; + this.prefetch = batchSize << 2; + this.queue = Queues.get(prefetch).get(); + } + + private void trace(Logger logger, String msg) { + logger.trace(String.format("[%s][%s]", Thread.currentThread().getId(), msg)); + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.subscription, s)) { + this.subscription = s; + this.actual.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (logger != null) { + trace(logger, "onNext: " + t); + } + // check if terminated (cancelled / error / completed) -> discard value if so + + // increment index + // append to buffer + // drain + + if (terminated == NOT_TERMINATED) { + // assume no more deliveries than requested + if (!queue.offer(t)) { + Context ctx = currentContext(); + Throwable error = Operators.onOperatorError(this.subscription, + Exceptions.failWithOverflow(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL), + t, actual.currentContext()); + this.error = error; + if (!TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_ERROR)) { + Operators.onErrorDropped(error, ctx); + return; + } + Operators.onDiscard(t, ctx); + drain(); + return; + } + + boolean shouldDrain = false; + for (;;) { + int index = this.index; + if (INDEX.compareAndSet(this, index, index + 1)) { + if (index == 0) { + try { + if (logger != null) { + trace(logger, "timerStart"); + } + currentTimeoutTask = timer.schedule(this::bufferTimedOut, + timeSpan, + unit); + } catch (RejectedExecutionException ree) { + if (logger != null) { + trace(logger, "Timer rejected for " + t); + } + Context ctx = actual.currentContext(); + Throwable error = Operators.onRejectedExecution(ree, subscription, null, t, ctx); + this.error = error; + if (!TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_ERROR)) { + Operators.onDiscard(t, ctx); + Operators.onErrorDropped(error, ctx); + return; + } + if (logger != null) { + trace(logger, "Discarding upon timer rejection" + t); + } + Operators.onDiscard(t, ctx); + drain(); + return; + } + } + if ((index + 1) % batchSize == 0) { + shouldDrain = true; + } + break; + } + } + if (shouldDrain) { + if (currentTimeoutTask != null) { + // TODO: it can happen that AFTER I dispose, the timeout + // anyway kicks during/after another onNext(), the buffer is + // delivered, and THEN drain is entered -> + // it would emit a buffer that is too small potentially. + // ALSO: + // It is also possible that here we deliver the buffer, but the + // timeout is happening for a new buffer! + currentTimeoutTask.dispose(); + } + this.index = 0; + drain(); + } + } else { + if (logger != null) { + trace(logger, "Discarding onNext: " + t); + } + Operators.onDiscard(t, currentContext()); + } + } + + @Override + public void onError(Throwable t) { + // set error flag + // set terminated as error + + // drain (WIP++ ?) + + if (currentTimeoutTask != null) { + currentTimeoutTask.dispose(); + } + timer.dispose(); + + if (!TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_ERROR)) { + Operators.onErrorDropped(t, currentContext()); + return; + } + this.error = t; // wip in drain will publish the error + drain(); + } + + @Override + public void onComplete() { + // set terminated as completed + // drain + if (currentTimeoutTask != null) { + currentTimeoutTask.dispose(); + } + timer.dispose(); + + if (TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_SUCCESS)) { + drain(); + } + } + + @Override + public CoreSubscriber actual() { + return this.actual; + } + + @Override + public void request(long n) { + // add cap to currently requested + // if previous requested was 0 -> drain first to deliver outdated values + // if the cap increased, request more ? + + // drain + + if (Operators.validate(n)) { + if (queue.isEmpty() && terminated != NOT_TERMINATED) { + return; + } + + if (Operators.addCap(REQUESTED, this, n) == 0) { + // there was no demand before - try to fulfill the demand if there + // are buffered values + drain(); + } + + if (batchSize == Integer.MAX_VALUE || n == Long.MAX_VALUE) { + requestMore(Long.MAX_VALUE); + } else { + long requestLimit = prefetch; + if (requestLimit > outstanding) { + if (logger != null) { + trace(logger, "requestMore: " + (requestLimit - outstanding) + ", outstanding: " + outstanding); + } + requestMore(requestLimit - outstanding); + } + } + } + } + + private void requestMore(long n) { + Subscription s = this.subscription; + if (s != null) { + Operators.addCap(OUTSTANDING, this, n); + s.request(n); + } + } + + @Override + public void cancel() { + // set terminated flag + // cancel upstream subscription + // dispose timer + // drain for proper cleanup + + if (logger != null) { + trace(logger, "cancel"); + } + if (TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_CANCEL)) { + if (this.subscription != null) { + this.subscription.cancel(); + } + } + if (currentTimeoutTask != null) { + currentTimeoutTask.dispose(); + } + timer.dispose(); + drain(); + } + + void bufferTimedOut() { + // called when buffer times out + + // reset index to 0 + // drain + + // TODO: try comparing against current reference and see if it was not + // cancelled -> to do this, replace Disposable timeoutTask with volatile + // and use CAS. + if (logger != null) { + trace(logger, "timerFire"); + } + this.index = 0; // if currently being drained, it means the buffer is + // delivered due to reaching the batchSize + drain(); + } + + private void drain() { + // entering this should be guarded by WIP getAndIncrement == 0 + // if we're here it means do a flush if there is downstream demand + // regardless of queue size + + // loop: + // if terminated -> check error -> deliver; else complete downstream + // if cancelled + + if (WIP.getAndIncrement(this) == 0) { + for (;;) { + int wip = this.wip; + if (logger != null) { + trace(logger, "drain. wip: " + wip); + } + if (terminated == NOT_TERMINATED) { + // is there demand? + while (flushABuffer()) { + // no-op + } + // make another spin if there's more work + } else { + if (completed) { + // if queue is empty, the discard is ignored + if (logger != null) { + trace(logger, "Discarding entire queue of " + queue.size()); + } + Operators.onDiscardQueueWithClear(queue, currentContext(), + null); + return; + } + // TODO: potentially the below can be executed twice? + if (terminated == TERMINATED_WITH_CANCEL) { + if (logger != null) { + trace(logger, "Discarding entire queue of " + queue.size()); + } + Operators.onDiscardQueueWithClear(queue, currentContext(), + null); + return; + } + while (flushABuffer()) { + // no-op + } + if (queue.isEmpty()) { + completed = true; + if (this.error != null) { + actual.onError(this.error); + } + else { + actual.onComplete(); + } + } else { + if (logger != null) { + trace(logger, "Queue not empty after termination"); + } + } + } + if (WIP.compareAndSet(this, wip, 0)) { + break; + } + } + } + } + + boolean flushABuffer() { + long requested = this.requested; + if (requested != 0) { + T element; + C buffer; + + element = queue.poll(); + if (element == null) { + // there is demand, but queue is empty + return false; + } + buffer = bufferSupplier.get(); + int i = 0; + do { + buffer.add(element); + } while ((++i < batchSize) && ((element = queue.poll()) != null)); + + if (requested != Long.MAX_VALUE) { + requested = REQUESTED.decrementAndGet(this); + } + + if (logger != null) { + trace(logger, "flush: " + buffer + ", now requested: " + requested); + } + + actual.onNext(buffer); + + if (requested != Long.MAX_VALUE) { + if (logger != null) { + trace(logger, "outstanding(" + outstanding + ") -= " + i); + } + long remaining = OUTSTANDING.addAndGet(this, -i); + if (terminated == NOT_TERMINATED) { + int replenishMark = prefetch >> 1; // TODO: create field limit instead + if (remaining < replenishMark) { + if (logger != null) { + trace(logger, "replenish: " + (prefetch - remaining) + ", outstanding: " + outstanding); + } + requestMore(prefetch - remaining); + } + } + + if (requested <= 0) { + return false; + } + } + // continue to see if there's more + return true; + } + return false; + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return this.subscription; + if (key == Attr.CANCELLED) return terminated == TERMINATED_WITH_CANCEL; + if (key == Attr.TERMINATED) return terminated == TERMINATED_WITH_ERROR || terminated == TERMINATED_WITH_SUCCESS; + if (key == Attr.REQUESTED_FROM_DOWNSTREAM) return requested; + if (key == Attr.CAPACITY) return prefetch; // TODO: revise + if (key == Attr.BUFFERED) return queue.size(); + if (key == Attr.RUN_ON) return timer; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + + return InnerOperator.super.scanUnsafe(key); + } + } + final static class BufferTimeoutSubscriber> implements InnerOperator { @@ -238,7 +682,9 @@ public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return terminated == TERMINATED_WITH_ERROR || terminated == TERMINATED_WITH_SUCCESS; if (key == Attr.REQUESTED_FROM_DOWNSTREAM) return requested; if (key == Attr.CAPACITY) return batchSize; - if (key == Attr.BUFFERED) return batchSize - index; + if (key == Attr.BUFFERED) return batchSize - index; // TODO: shouldn't this + // be index instead ? as it currently stands, the returned value represents + // anticipated items left to fill buffer if completed before timeout if (key == Attr.RUN_ON) return timer; if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutFairBackpressureTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutFairBackpressureTest.java new file mode 100644 index 0000000000..01b21276a5 --- /dev/null +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutFairBackpressureTest.java @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import java.util.stream.LongStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.Exceptions; +import reactor.core.Scannable; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.StepVerifierOptions; +import reactor.test.publisher.TestPublisher; +import reactor.test.scheduler.VirtualTimeScheduler; +import reactor.test.util.RaceTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static reactor.core.Scannable.from; +import static reactor.core.publisher.Sinks.EmitFailureHandler.FAIL_FAST; + +public class FluxBufferTimeoutFairBackpressureTest { + + @AfterEach + public void tearDown() { + VirtualTimeScheduler.reset(); + } + + @Test + void backpressureSupported() throws InterruptedException { + final int eventProducerDelayMillis = 200; + // Event Producer emits requested items to downstream with a 200ms delay + Flux eventProducer = Flux.create(sink -> { + sink.onRequest(request -> { + if (request == Long.MAX_VALUE) { + sink.error(new RuntimeException("No_Backpressure unsupported")); + } else { + System.out.println("Backpressure Request(" + request + ")"); + LongStream.range(0, request) + .mapToObj(String::valueOf) + .forEach(message -> { + try { + TimeUnit.MILLISECONDS.sleep(eventProducerDelayMillis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("Producing: " + message); + sink.next(message); + }); + } + }); + }).subscribeOn(Schedulers.boundedElastic()); + + // Event Consumer buffers 5 items or up to 150ms from first item. + // Because the producer emits every 200ms, we expect only 1 item per each buffer. + // Every buffer is delayed for 500ms before delivering to the subscriber. + // We expect all items to be delivered with proper backpressure which involves + // time passing. + + Scheduler scheduler = Schedulers.newBoundedElastic(10, 10_000, "queued-tasks"); + Semaphore completed = new Semaphore(1); + AtomicBoolean hasError = new AtomicBoolean(false); + completed.acquire(); + + final int bufferTimeoutMillis = 150; + final int consumerDelayMillis = 500; + + Disposable subscription = eventProducer + .bufferTimeout(5, Duration.ofMillis(bufferTimeoutMillis), true) + .doOnRequest(r -> System.out.println("BUFFER requested " + r)) + .concatMap(b -> { + System.out.println("CONCATMAP delivered " + b); + return Flux.just(b) + .delayElements(Duration.ofMillis(consumerDelayMillis), scheduler); + }, 1) + .subscribe( + buffer -> { + System.out.println("Received buffer of size " + buffer.size()); + for (String message : buffer) { + System.out.println("Consuming: " + message); + }}, + error -> { + System.err.println("Error: " + error); + hasError.set(true); + completed.release(); + }, + () -> { + System.out.println("Completed."); + completed.release(); + } + ); + + try { + Assertions.assertFalse(completed.tryAcquire(1, TimeUnit.MINUTES), "Should " + + "not finish before subscription cancelled."); + Assertions.assertFalse(hasError.get(), "Should not have received an error."); + } finally { + subscription.dispose(); + } + + System.out.println("Completed test."); + } + + @Test + void downstreamNoReplenishButTimeout() { + Sinks.Many sink = Sinks.many() + .unicast() + .onBackpressureBuffer(); + StepVerifier.withVirtualTime( + () -> sink.asFlux() + .bufferTimeout(10, Duration.ofMillis(100), true), + 1 + ) + .expectSubscription() + .then(() -> sink.tryEmitNext(0)) + .thenAwait(Duration.ofMillis(100)) + .assertNext(l -> assertThat(l).hasSize(1)) + .then(() -> sink.tryEmitNext(1)) + .thenAwait(Duration.ofMillis(100)) + .expectNoEvent(Duration.ofMillis(300)) + .thenCancel() + .verify(); + } + + Flux> scenario_bufferWithTimeoutAccumulateOnSize() { + return Flux.range(1, 6) + .delayElements(Duration.ofMillis(300)) + .bufferTimeout(5, Duration.ofMillis(2000), true); + } + + @Test + public void bufferWithTimeoutAccumulateOnSize() { + StepVerifier.withVirtualTime(this::scenario_bufferWithTimeoutAccumulateOnSize) + .thenAwait(Duration.ofMillis(1500)) + .assertNext(s -> assertThat(s).containsExactly(1, 2, 3, 4, 5)) + .thenAwait(Duration.ofMillis(2000)) + .assertNext(s -> assertThat(s).containsExactly(6)) + .verifyComplete(); + } + + Flux> scenario_bufferWithTimeoutAccumulateOnTime() { + return Flux.range(1, 6) + .delayElements(Duration.ofNanos(300)).log("delayed") + .bufferTimeout(15, Duration.ofNanos(1500), true).log("buffered"); + } + + @Test + public void bufferWithTimeoutAccumulateOnTime() { + StepVerifier.withVirtualTime(this::scenario_bufferWithTimeoutAccumulateOnTime) + .thenAwait(Duration.ofNanos(1800)) + .assertNext(s -> assertThat(s).containsExactly(1, 2, 3, 4, 5)) + .thenAwait(Duration.ofNanos(2000)) + .assertNext(s -> assertThat(s).containsExactly(6)) + .verifyComplete(); + } + + @Test + void bufferWithTimeoutAvoidingNegativeRequests() { + final List requestPattern = new CopyOnWriteArrayList<>(); + + StepVerifier.withVirtualTime(() -> + Flux.range(1, 3) + .delayElements(Duration.ofMillis(100)) + .doOnRequest(requestPattern::add) + .bufferTimeout(5, Duration.ofMillis(100), true), + 0) + .expectSubscription() + .expectNoEvent(Duration.ofMillis(100)) + .thenRequest(2) + .expectNoEvent(Duration.ofMillis(100)) + .assertNext(s -> assertThat(s).containsExactly(1)) + .expectNoEvent(Duration.ofMillis(100)) + .assertNext(s -> assertThat(s).containsExactly(2)) + .thenRequest(1) // This should not cause a negative upstream request + .expectNoEvent(Duration.ofMillis(100)) + .thenCancel() + .verify(); + + assertThat(requestPattern).allSatisfy(r -> assertThat(r).isPositive()); + } + + @Test + void processesLargeDataset() { + TreeSet set = new TreeSet<>(); + Flux.fromStream(IntStream.range(0, 1_000_000).boxed()) + .hide() + .bufferTimeout(1000, Duration.ofSeconds(1), true) + .doOnNext(set::addAll) + .blockLast(); + + assertThat(set.size()).isEqualTo(1_000_000); + } + + @Test + public void scanSubscriber() { + CoreSubscriber> actual = new LambdaSubscriber<>(null, e -> {}, null, null); + + final Scheduler.Worker worker = Schedulers.boundedElastic() + .createWorker(); + FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> test = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber>( + actual, 123, 1000, TimeUnit.HOURS, + worker, ArrayList::new, null); + + try { + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + test.requested = 3L; + for (int i = 0; i < 100; i++) { + test.onNext(Integer.toString(i)); + } + + assertThat(test.scan(Scannable.Attr.RUN_ON)).isSameAs(worker); + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(subscription); + assertThat(test.scan(Scannable.Attr.ACTUAL)).isSameAs(actual); + + assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(3L); + assertThat(test.scan(Scannable.Attr.CAPACITY)).isEqualTo(123 << 2); + assertThat(test.scan(Scannable.Attr.BUFFERED)).isEqualTo(100); + assertThat(test.scan(Scannable.Attr.RUN_STYLE)).isSameAs(Scannable.Attr.RunStyle.ASYNC); + + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + + test.onError(new IllegalStateException("boom")); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + } + finally { + worker.dispose(); + } + } + @Test + public void shouldShowActualSubscriberDemand() { + Subscription[] subscriptionsHolder = new Subscription[1]; + CoreSubscriber> actual = new LambdaSubscriber<>( + null, e -> {}, null, s -> subscriptionsHolder[0] = s + ); + + FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> test = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber>( + actual, 123, 1000, TimeUnit.MILLISECONDS, + Schedulers.boundedElastic().createWorker(), ArrayList::new, null + ); + + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + subscriptionsHolder[0].request(10); + assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(10L); + subscriptionsHolder[0].request(5); + assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(15L); + } + + @Test + public void downstreamDemandShouldBeAbleToDecreaseOnFullBuffer() { + Subscription[] subscriptionsHolder = new Subscription[1]; + CoreSubscriber> actual = new LambdaSubscriber<>( + null, e -> {}, null, s -> subscriptionsHolder[0] = s + ); + + FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> test = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber>( + actual, 5, 1000, TimeUnit.MILLISECONDS, + Schedulers.boundedElastic().createWorker(), ArrayList::new, null + ); + + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + subscriptionsHolder[0].request(1); + assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(1L); + + for (int i = 0; i < 5; i++) { + test.onNext(String.valueOf(i)); + } + + assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(0L); + } + + @Test + public void downstreamDemandShouldBeAbleToDecreaseOnTimeSpan() { + Subscription[] subscriptionsHolder = new Subscription[1]; + CoreSubscriber> actual = new LambdaSubscriber<>( + null, e -> {}, null, s -> subscriptionsHolder[0] = s + ); + + VirtualTimeScheduler timeScheduler = VirtualTimeScheduler.getOrSet(); + FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> test = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber>( + actual, 5, 100, TimeUnit.MILLISECONDS, + timeScheduler.createWorker(), ArrayList::new, null + ); + + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + subscriptionsHolder[0].request(1); + assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(1L); + timeScheduler.advanceTimeBy(Duration.ofMillis(100)); + assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(1L); + test.onNext("0"); + timeScheduler.advanceTimeBy(Duration.ofMillis(100)); + assertThat(test.scan(Scannable.Attr.REQUESTED_FROM_DOWNSTREAM)).isEqualTo(0L); + } + + @Test + public void requestedFromUpstreamShouldNotExceedDownstreamDemand() { + Sinks.Many sink = Sinks.many().multicast().onBackpressureBuffer(); + Flux emitter = sink.asFlux(); + + AtomicLong requestedOutstanding = new AtomicLong(0); + + VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); + + Flux> flux = emitter.doOnRequest(requestedOutstanding::addAndGet) + .bufferTimeout( + 5, Duration.ofMillis(100), scheduler, true + ) + .doOnNext(list -> + requestedOutstanding.addAndGet(-list.size()) + ); + + StepVerifier.withVirtualTime(() -> flux, () -> scheduler, 0) + .expectSubscription() + .then(() -> assertThat(requestedOutstanding).hasValue(0)) + .thenRequest(2) + .then(() -> assertThat(requestedOutstanding.get()).isEqualTo(20)) + // prefetch size is 5 << 2, so 5 * 4 = 20 + .then(() -> sink.emitNext("a", FAIL_FAST)) + .thenAwait(Duration.ofMillis(100)) + .assertNext(s -> assertThat(s).containsExactly("a")) + .then(() -> assertThat(requestedOutstanding).hasValue(19)) + .thenRequest(1) + .then(() -> assertThat(requestedOutstanding).hasValue(20)) + .thenCancel() + .verify(); + } + + @Test + public void scanSubscriberCancelled() { + CoreSubscriber> + actual = new LambdaSubscriber<>(null, e -> {}, null, null); + + FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> test = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber>( + actual, 123, 1000, TimeUnit.MILLISECONDS, + Schedulers.boundedElastic().createWorker(), ArrayList::new, null + ); + + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + + test.cancel(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + } + + @Test + public void bufferTimeoutShouldNotRaceWithNext() { + Set seen = new HashSet<>(); + Consumer> consumer = integers -> { + for (Integer i : integers) { + if (!seen.add(i)) { + throw new IllegalStateException("Duplicate! " + i); + } + } + }; + CoreSubscriber> actual = new LambdaSubscriber<>(consumer, null, null, null); + + FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> test = + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber>( + actual, 3, 1000, TimeUnit.MILLISECONDS, + Schedulers.boundedElastic().createWorker(), ArrayList::new, null + ); + test.onSubscribe(Operators.emptySubscription()); + + AtomicInteger counter = new AtomicInteger(); + for (int i = 0; i < 500; i++) { + RaceTestUtils.race( + () -> test.onNext(counter.getAndIncrement()), + test::bufferTimedOut + ); + } + + test.onComplete(); + + assertThat(seen.size()).isEqualTo(500); + } + + //see https://github.com/reactor/reactor-core/issues/1247 + @Test + public void rejectedOnNextLeadsToOnError() { + Scheduler scheduler = Schedulers.newSingle("rejectedOnNextLeadsToOnError"); + scheduler.dispose(); + + StepVerifier.create(Flux.just(1, 2, 3) + .bufferTimeout(4, Duration.ofMillis(500), scheduler, true)) + .assertNext(l -> assertThat(l).containsExactly(1)) + .expectError(RejectedExecutionException.class) + .verify(Duration.ofSeconds(1)); + } + + @Test + public void discardOnCancel() { + StepVerifier.create(Flux.just(1, 2, 3) + .concatWith(Mono.never()) + .bufferTimeout(10, Duration.ofMillis(100), true)) + .thenAwait(Duration.ofMillis(10)) + .thenCancel() + .verifyThenAssertThat() + .hasDiscardedExactly(1, 2, 3); + } + + @Test + public void discardOnTimerRejected() { + Scheduler scheduler = Schedulers.newSingle("discardOnTimerRejected"); + + StepVerifier.create(Flux.just(1, 2, 3) + .doOnNext(n -> scheduler.dispose()) + .bufferTimeout(10, Duration.ofMillis(100), scheduler, true)) + .assertNext(l -> assertThat(l).containsExactly(1)) + .expectErrorSatisfies(e -> assertThat(e).isInstanceOf(RejectedExecutionException.class)) + .verify(); + } + + @Test + public void discardOnError() { + StepVerifier.create(Flux.just(1, 2, 3) + .concatWith(Mono.error(new IllegalStateException("boom"))) + .bufferTimeout(10, Duration.ofMillis(100), true)) + .assertNext(l -> assertThat(l).containsExactly(1, 2, 3)) + .expectErrorMessage("boom") + .verify(); + } +} diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutTest.java index 1d96fe399f..9d703b4daa 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; @@ -33,7 +32,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; - import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Scannable; @@ -333,6 +331,10 @@ public void flushShouldNotRaceWithNext() { () -> test.flushCallback(null) ); } + + test.onComplete(); + + assertThat(seen.size()).isEqualTo(500); } //see https://github.com/reactor/reactor-core/issues/1247 diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxMergeSequentialTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxMergeSequentialTest.java index 9c5312dd4b..4aba61324d 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxMergeSequentialTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxMergeSequentialTest.java @@ -41,8 +41,10 @@ import reactor.core.publisher.FluxConcatMap.ErrorMode; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; +import reactor.test.MemoryUtils; import reactor.test.StepVerifier; import reactor.test.subscriber.AssertSubscriber; +import reactor.test.subscriber.TestSubscriber; import reactor.util.concurrent.Queues; import static org.assertj.core.api.Assertions.assertThat; @@ -871,4 +873,40 @@ public void scanInner() { inner.cancel(); assertThat(inner.scan(Scannable.Attr.CANCELLED)).isTrue(); } + + @Test + void discardsPrefetched() { + Hooks.onNextDropped(MemoryUtils.Tracked::safeRelease); + Hooks.onErrorDropped(e -> {}); + Hooks.onOperatorError((e, v) -> null); + + AssertSubscriber assertSubscriber = new AssertSubscriber<>( + Operators.enableOnDiscard(null, MemoryUtils.Tracked::safeRelease), 0 + ); + + AtomicInteger prefetched = new AtomicInteger(); + MemoryUtils.Tracked tracked1 = new MemoryUtils.Tracked("1", false); + MemoryUtils.Tracked tracked2 = new MemoryUtils.Tracked("2", false); + Flux flux = Flux + .just(tracked1, tracked2) + .map(Collections::singletonList) + .hide() + .doOnNext(t -> prefetched.incrementAndGet()) + .flatMapIterable(Function.identity()); +// .flatMapSequential(Mono::just); + + flux.subscribe(assertSubscriber); + + assertSubscriber.cancel(); + + assertThat(assertSubscriber.values()).isEmpty(); + assertThat(prefetched.get()).isEqualTo(2); + assertThat(tracked1.isReleased()).isTrue(); + assertThat(tracked2.isReleased()).isTrue(); + + Hooks.resetOnNextDropped(); + Hooks.resetOnErrorDropped(); + Hooks.resetOnNextError(); + Hooks.resetOnOperatorError(); + } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java index ca05a985b7..f1cc13d99d 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package reactor.core.publisher; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -156,6 +157,7 @@ public class OnDiscardShouldNotLeakTest { DiscardScenario.fluxSource("monoFilterWhenFalse", main -> main.last().filterWhen(__ -> Mono.just(false).hide())), DiscardScenario.fluxSource("last", main -> main.last(new Tracked("default")).flatMap(f -> Mono.just(f).hide())), DiscardScenario.fluxSource("flatMapIterable", f -> f.flatMapIterable(Arrays::asList)), + DiscardScenario.fluxSource("bufferTimeout", f -> f.bufferTimeout(2, Duration.ofMillis(1), true).flatMapIterable(Function.identity())), DiscardScenario.fluxSource("publishOnDelayErrors", f -> f.publishOn(Schedulers.immediate())), DiscardScenario.fluxSource("publishOnImmediateErrors", f -> f.publishOn(Schedulers.immediate(), false, Queues.SMALL_BUFFER_SIZE)), DiscardScenario.fluxSource("publishOnAndPublishOn", main -> main diff --git a/reactor-core/src/test/java/reactor/test/MemoryUtils.java b/reactor-core/src/test/java/reactor/test/MemoryUtils.java index 54084c403a..55223eeb3f 100644 --- a/reactor-core/src/test/java/reactor/test/MemoryUtils.java +++ b/reactor-core/src/test/java/reactor/test/MemoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; +import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -174,8 +175,16 @@ public static final class Tracked extends AtomicBoolean { * * @param t the arbitrary object */ + @SuppressWarnings("rawtypes") public static void safeRelease(Object t) { - if (t instanceof Tracked) { + if (t instanceof Collection) { + for (Object tt : (Collection) t) { + if (tt instanceof Tracked) { + ((Tracked) tt).release(); + } + } + } + else if (t instanceof Tracked) { ((Tracked) t).release(); } } From 75d48b443b2e0038519e362063417a79d544e200 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:30:34 +0300 Subject: [PATCH 121/312] ensures CP is enabled only when ContextFactory is available (#3490) --- .../java/reactor/core/publisher/ContextPropagationSupport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java index 29ff8ad640..a68538f0f6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java @@ -33,6 +33,7 @@ final class ContextPropagationSupport { boolean contextPropagation = false; try { Class.forName("io.micrometer.context.ContextRegistry"); + Class.forName("io.micrometer.context.ContextSnapshotFactory"); contextPropagation = true; } catch (ClassNotFoundException notFound) { } catch (LinkageError linkageErr) { From 898aea48e9779a23946f52ffe281ff2742a206bf Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 12 Jun 2023 09:04:15 +0300 Subject: [PATCH 122/312] ensures backward compatible with 103 context propagation changes (#3493) this commit ensures that the reactor which uses the new API from the Micrometer Context Propagation library is backward compatible and can be used with older versions of context-propagation lib --- gradle/libs.versions.toml | 1 + reactor-core/build.gradle | 8 + .../core/publisher/ContextPropagation.java | 185 ++- .../publisher/ContextPropagationSupport.java | 12 +- .../publisher/ContextPropagationTest.java | 1072 +++++++++++++++++ .../publisher/ContextPropagationTest.java | 15 +- 6 files changed, 1246 insertions(+), 47 deletions(-) create mode 100644 reactor-core/src/withContextPropagation102Test/java/reactor/core/publisher/ContextPropagationTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7bb3b6a2d..15a11b7cfc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ jsr305 = "com.google.code.findbugs:jsr305:3.0.1" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } +micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.3" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index fb21f6fdbb..29a5e0f6fd 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -47,6 +47,7 @@ testSets { blockHoundTest //TODO once can probably be removed in 3.6.0, but MUST keep the ReactorContextAccessorTest in that case withMicrometerTest + withContextPropagation102Test tckTest } @@ -124,6 +125,12 @@ dependencies { withMicrometerTestImplementation libs.micrometer.contextPropagation withMicrometerTestImplementation sourceSets.test.output + withContextPropagation102TestImplementation platform(libs.micrometer.bom) + withContextPropagation102TestImplementation libs.micrometer.commons + withContextPropagation102TestImplementation libs.micrometer.core + withContextPropagation102TestImplementation libs.micrometer.contextPropagation102 + withContextPropagation102TestImplementation sourceSets.test.output + jcstressImplementation(project(":reactor-test")) { exclude module: 'reactor-core' } @@ -274,6 +281,7 @@ jcstress { // always depend on withMicrometerTest, blockHoundTest and tckTest, skip loops when not releasing // note that this way the tasks can be run individually check { + dependsOn withContextPropagation102Test dependsOn withMicrometerTest dependsOn blockHoundTest dependsOn tckTest diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index 64cd60cef1..f331b6a5c2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -17,17 +17,20 @@ package reactor.core.publisher; import java.util.AbstractQueue; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import java.util.Queue; import java.util.function.BiConsumer; import java.util.function.Function; -import java.util.function.Predicate; import java.util.function.Supplier; +import io.micrometer.context.ContextAccessor; import io.micrometer.context.ContextRegistry; import io.micrometer.context.ContextSnapshot; import io.micrometer.context.ContextSnapshotFactory; +import io.micrometer.context.ThreadLocalAccessor; import reactor.core.observability.SignalListener; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -41,7 +44,6 @@ */ final class ContextPropagation { - static final Predicate PREDICATE_TRUE = v -> true; static final Function NO_OP = c -> c; static final Function WITH_GLOBAL_REGISTRY_NO_PREDICATE; @@ -51,7 +53,7 @@ final class ContextPropagation { WITH_GLOBAL_REGISTRY_NO_PREDICATE = ContextPropagationSupport.isContextPropagationAvailable() ? new ContextCaptureNoPredicate() : NO_OP; - if (ContextPropagationSupport.isContextPropagationAvailable()) { + if (ContextPropagationSupport.isContextPropagation103Available()) { globalContextSnapshotFactory = ContextSnapshotFactory.builder() .clearMissing(false) .build(); @@ -59,17 +61,58 @@ final class ContextPropagation { } static void configureContextSnapshotFactory(boolean clearMissing) { - globalContextSnapshotFactory = - ContextSnapshotFactory.builder().clearMissing(clearMissing).build(); + if (ContextPropagationSupport.isContextPropagation103OnClasspath) { + globalContextSnapshotFactory = ContextSnapshotFactory.builder() + .clearMissing(clearMissing) + .build(); + } } + @SuppressWarnings("unchecked") static ContextSnapshot.Scope setThreadLocals(Object context) { - return globalContextSnapshotFactory.setThreadLocalsFrom(context); + if (ContextPropagationSupport.isContextPropagation103OnClasspath) { + return globalContextSnapshotFactory.setThreadLocalsFrom(context); + } + else { + ContextRegistry registry = ContextRegistry.getInstance(); + ContextAccessor contextAccessor = registry.getContextAccessorForRead(context); + Map previousValues = null; + for (ThreadLocalAccessor threadLocalAccessor : registry.getThreadLocalAccessors()) { + Object key = threadLocalAccessor.key(); + Object value = ((ContextAccessor) contextAccessor).readValue((C) context, key); + previousValues = setThreadLocal(key, value, threadLocalAccessor, previousValues); + } + return ReactorScopeImpl.from(previousValues, registry); + } + } + + @SuppressWarnings("unchecked") + private static Map setThreadLocal(Object key, @Nullable V value, + ThreadLocalAccessor accessor, @Nullable Map previousValues) { + + previousValues = (previousValues != null ? previousValues : new HashMap<>()); + previousValues.put(key, accessor.getValue()); + if (value != null) { + ((ThreadLocalAccessor) accessor).setValue(value); + } + else { + accessor.reset(); + } + return previousValues; + } + + static ContextSnapshot captureThreadLocals() { + if (ContextPropagationSupport.isContextPropagation103OnClasspath) { + return globalContextSnapshotFactory.captureAll(); + } + else { + return ContextSnapshot.captureAll(); + } } public static Function scopePassingOnScheduleHook() { return delegate -> { - ContextSnapshot contextSnapshot = globalContextSnapshotFactory.captureAll(); + ContextSnapshot contextSnapshot = captureThreadLocals(); return contextSnapshot.wrap(delegate); }; } @@ -110,11 +153,21 @@ static BiConsumer> contextRestoreForHandle(BiConsum if (ctx.isEmpty()) { return handler; } - return (v, sink) -> { - try (ContextSnapshot.Scope ignored = globalContextSnapshotFactory.setThreadLocalsFrom(ctx)) { - handler.accept(v, sink); - } - }; + + if (ContextPropagationSupport.isContextPropagation103OnClasspath) { + return (v, sink) -> { + try (ContextSnapshot.Scope ignored = globalContextSnapshotFactory.setThreadLocalsFrom(ctx)) { + handler.accept(v, sink); + } + }; + } + else { + return (v, sink) -> { + try (ContextSnapshot.Scope ignored = ContextSnapshot.setAllThreadLocalsFrom(ctx)) { + handler.accept(v, sink); + } + }; + } } else { return handler; @@ -144,145 +197,165 @@ static SignalListener contextRestoreForTap(final SignalListener origin if (ctx.isEmpty()) { return original; } - return new ContextRestoreSignalListener(original, ctx, globalContextSnapshotFactory); + + if (ContextPropagationSupport.isContextPropagation103OnClasspath) { + return new ContextRestore103SignalListener<>(original, ctx, globalContextSnapshotFactory); + } + else { + return new ContextRestoreSignalListener<>(original, ctx); + } } //the SignalListener implementation can be tested independently with a test-specific ContextRegistry - static final class ContextRestoreSignalListener implements SignalListener { + static class ContextRestoreSignalListener implements SignalListener { final SignalListener original; final ContextView context; - final ContextSnapshotFactory contextSnapshotFactory; public ContextRestoreSignalListener(SignalListener original, - ContextView context, ContextSnapshotFactory contextSnapshotFactory) { + ContextView context) { this.original = original; this.context = context; - this.contextSnapshotFactory = contextSnapshotFactory; } ContextSnapshot.Scope restoreThreadLocals() { - return contextSnapshotFactory.setThreadLocalsFrom(this.context); + return ContextSnapshot.setAllThreadLocalsFrom(this.context); } @Override - public void doFirst() throws Throwable { + public final void doFirst() throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doFirst(); } } @Override - public void doFinally(SignalType terminationType) throws Throwable { + public final void doFinally(SignalType terminationType) throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doFinally(terminationType); } } @Override - public void doOnSubscription() throws Throwable { + public final void doOnSubscription() throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnSubscription(); } } @Override - public void doOnFusion(int negotiatedFusion) throws Throwable { + public final void doOnFusion(int negotiatedFusion) throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnFusion(negotiatedFusion); } } @Override - public void doOnRequest(long requested) throws Throwable { + public final void doOnRequest(long requested) throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnRequest(requested); } } @Override - public void doOnCancel() throws Throwable { + public final void doOnCancel() throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnCancel(); } } @Override - public void doOnNext(T value) throws Throwable { + public final void doOnNext(T value) throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnNext(value); } } @Override - public void doOnComplete() throws Throwable { + public final void doOnComplete() throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnComplete(); } } @Override - public void doOnError(Throwable error) throws Throwable { + public final void doOnError(Throwable error) throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnError(error); } } @Override - public void doAfterComplete() throws Throwable { + public final void doAfterComplete() throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doAfterComplete(); } } @Override - public void doAfterError(Throwable error) throws Throwable { + public final void doAfterError(Throwable error) throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doAfterError(error); } } @Override - public void doOnMalformedOnNext(T value) throws Throwable { + public final void doOnMalformedOnNext(T value) throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnMalformedOnNext(value); } } @Override - public void doOnMalformedOnError(Throwable error) throws Throwable { + public final void doOnMalformedOnError(Throwable error) throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnMalformedOnError(error); } } @Override - public void doOnMalformedOnComplete() throws Throwable { + public final void doOnMalformedOnComplete() throws Throwable { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.doOnMalformedOnComplete(); } } @Override - public void handleListenerError(Throwable listenerError) { + public final void handleListenerError(Throwable listenerError) { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { original.handleListenerError(listenerError); } } @Override - public Context addToContext(Context originalContext) { + public final Context addToContext(Context originalContext) { try (ContextSnapshot.Scope ignored = restoreThreadLocals()) { return original.addToContext(originalContext); } } } + //the SignalListener implementation can be tested independently with a test-specific ContextRegistry + static final class ContextRestore103SignalListener extends ContextRestoreSignalListener { + + final ContextSnapshotFactory contextSnapshotFactory; + + public ContextRestore103SignalListener(SignalListener original, + ContextView context, ContextSnapshotFactory contextSnapshotFactory) { + super(original, context); + this.contextSnapshotFactory = contextSnapshotFactory; + } + + ContextSnapshot.Scope restoreThreadLocals() { + return contextSnapshotFactory.setThreadLocalsFrom(this.context); + } + } + static final class ContextCaptureNoPredicate implements Function { @Override public Context apply(Context context) { - return globalContextSnapshotFactory.captureAll().updateContext(context); + return captureThreadLocals().updateContext(context); } } @@ -312,7 +385,7 @@ public int size() { @Override public boolean offer(T o) { - ContextSnapshot contextSnapshot = globalContextSnapshotFactory.captureAll(); + ContextSnapshot contextSnapshot = captureThreadLocals(); return envelopeQueue.offer(new Envelope<>(o, contextSnapshot)); } @@ -339,7 +412,7 @@ public T poll() { private void restoreTheContext(Envelope envelope) { ContextSnapshot contextSnapshot = envelope.contextSnapshot; // tries to read existing Thread for existing ThreadLocals - ContextSnapshot currentContextSnapshot = globalContextSnapshotFactory.captureAll(); + ContextSnapshot currentContextSnapshot = captureThreadLocals(); if (!contextSnapshot.equals(currentContextSnapshot)) { if (!hasPrevious || !Thread.currentThread().equals(this.lastReader)) { // means context was restored form the envelope, @@ -380,4 +453,42 @@ static class Envelope { this.contextSnapshot = contextSnapshot; } } + + private static class ReactorScopeImpl implements ContextSnapshot.Scope { + + private final Map previousValues; + + private final ContextRegistry contextRegistry; + + private ReactorScopeImpl(Map previousValues, + ContextRegistry contextRegistry) { + this.previousValues = previousValues; + this.contextRegistry = contextRegistry; + } + + @Override + public void close() { + for (ThreadLocalAccessor accessor : this.contextRegistry.getThreadLocalAccessors()) { + if (this.previousValues.containsKey(accessor.key())) { + Object previousValue = this.previousValues.get(accessor.key()); + resetThreadLocalValue(accessor, previousValue); + } + } + } + + @SuppressWarnings("unchecked") + private void resetThreadLocalValue(ThreadLocalAccessor accessor, @Nullable V previousValue) { + if (previousValue != null) { + ((ThreadLocalAccessor) accessor).restore(previousValue); + } + else { + accessor.reset(); + } + } + + public static ContextSnapshot.Scope from(@Nullable Map previousValues, ContextRegistry registry) { + return (previousValues != null ? new ReactorScopeImpl(previousValues, registry) : () -> { + }); + } + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java index a68538f0f6..c705899a40 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java @@ -27,14 +27,17 @@ final class ContextPropagationSupport { // The field should end with 'Available'. See org.springframework.aot.nativex.feature.PreComputeFieldFeature. // Ultimately the long term solution should be provided by Reactor Core. static final boolean isContextPropagationOnClasspath; - static boolean propagateContextToThreadLocals = false; + static final boolean isContextPropagation103OnClasspath; + static boolean propagateContextToThreadLocals = false; static { boolean contextPropagation = false; + boolean contextPropagation103 = false; try { Class.forName("io.micrometer.context.ContextRegistry"); - Class.forName("io.micrometer.context.ContextSnapshotFactory"); contextPropagation = true; + Class.forName("io.micrometer.context.ContextSnapshotFactory"); + contextPropagation103 = true; } catch (ClassNotFoundException notFound) { } catch (LinkageError linkageErr) { } catch (Throwable err) { @@ -42,6 +45,7 @@ final class ContextPropagationSupport { " The feature is considered disabled due to this:", err); } isContextPropagationOnClasspath = contextPropagation; + isContextPropagation103OnClasspath = contextPropagation103; } /** @@ -53,6 +57,10 @@ static boolean isContextPropagationAvailable() { return isContextPropagationOnClasspath; } + static boolean isContextPropagation103Available() { + return isContextPropagation103OnClasspath; + } + static boolean shouldPropagateContextToThreadLocals() { return isContextPropagationOnClasspath && propagateContextToThreadLocals; } diff --git a/reactor-core/src/withContextPropagation102Test/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withContextPropagation102Test/java/reactor/core/publisher/ContextPropagationTest.java new file mode 100644 index 0000000000..daaeab6a2d --- /dev/null +++ b/reactor-core/src/withContextPropagation102Test/java/reactor/core/publisher/ContextPropagationTest.java @@ -0,0 +1,1072 @@ +/* + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import io.micrometer.context.ContextRegistry; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; +import org.reactivestreams.Publisher; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.Scannable; +import reactor.core.observability.SignalListener; +import reactor.core.observability.SignalListenerFactory; +import reactor.core.publisher.FluxHandle.HandleConditionalSubscriber; +import reactor.core.publisher.FluxHandle.HandleSubscriber; +import reactor.core.publisher.FluxHandleFuseable.HandleFuseableConditionalSubscriber; +import reactor.core.publisher.FluxHandleFuseable.HandleFuseableSubscriber; +import reactor.core.scheduler.Schedulers; +import reactor.test.ParameterizedTestWithName; +import reactor.test.publisher.TestPublisher; +import reactor.test.subscriber.TestSubscriber; +import reactor.test.subscriber.TestSubscriberBuilder; +import reactor.util.concurrent.Queues; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Simon Baslé + */ +class ContextPropagationTest { + + private static final String KEY1 = "ContextPropagationTest.key1"; + private static final String KEY2 = "ContextPropagationTest.key2"; + + private static final ThreadLocal REF1 = ThreadLocal.withInitial(() -> "ref1_init"); + private static final ThreadLocal REF2 = ThreadLocal.withInitial(() -> "ref2_init"); + + @BeforeAll + static void initializeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + + globalRegistry.registerThreadLocalAccessor(KEY1, REF1); + globalRegistry.registerThreadLocalAccessor(KEY2, REF2); + } + + //the cleanup of "thread locals" could be especially important if one starts relying on + //the global registry in tests: it would ensure no TL pollution. + @AfterEach + void cleanupThreadLocals() { + REF1.remove(); + REF2.remove(); + } + + @AfterAll + static void removeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + + globalRegistry.removeThreadLocalAccessor(KEY1); + globalRegistry.removeThreadLocalAccessor(KEY2); + + } + + @Test + void threadLocalsPresentAfterSubscribeOn() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doOnNext(i -> tlValue.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentAfterPublishOn() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .publishOn(Schedulers.boundedElastic()) + .doOnNext(i -> tlValue.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInFlatMap() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .flatMap(i -> Mono.just(i) + .doOnNext(j -> tlValue.set(REF1.get()))) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentAfterDelay() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .delayElements(Duration.ofMillis(1)) + .doOnNext(i -> tlValue.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsRestoredAfterPollution() { + // this test validates Queue wrapping takes place + Hooks.enableAutomaticContextPropagation(); + ArrayBlockingQueue modifiedThreadLocals = new ArrayBlockingQueue<>(10); + ArrayBlockingQueue restoredThreadLocals = new ArrayBlockingQueue<>(10); + + Flux.range(0, 10) + .doOnNext(i -> { + REF1.set("i: " + i); + }) + .publishOn(Schedulers.parallel()) + // the validation below shows that modifications to TLs are propagated + // across thread boundaries via queue wrapping, so explicit control + // is required from users to clean up after such modifications + .doOnNext(i -> modifiedThreadLocals.add(REF1.get())) + .contextWrite(Function.identity()) + // the contextWrite above creates a barrier that ensures the downstream + // operator sees TLs from the subscriber context + .doOnNext(i -> restoredThreadLocals.add(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .blockLast(); + + assertThat(modifiedThreadLocals).containsExactly( + "i: 0", "i: 1", "i: 2", "i: 3", "i: 4", + "i: 5", "i: 6", "i: 7", "i: 8", "i: 9" + ); + assertThat(restoredThreadLocals).containsExactly( + Collections.nCopies(10, "present").toArray(new String[] {}) + ); + } + + @Test + @SuppressWarnings("unchecked") + void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference requestTlValue = new AtomicReference<>(); + AtomicReference subscribeTlValue = new AtomicReference<>(); + AtomicReference firstNextTlValue = new AtomicReference<>(); + AtomicReference secondNextTlValue = new AtomicReference<>(); + AtomicReference cancelTlValue = new AtomicReference<>(); + + CountDownLatch itemDelivered = new CountDownLatch(1); + CountDownLatch cancelled = new CountDownLatch(1); + + TestSubscriber subscriber = + TestSubscriber.builder().initialRequest(1).build(); + + REF1.set("downstreamContext"); + + Flux.just(1, 2, 3) + .hide() + .doOnRequest(r -> requestTlValue.set(REF1.get())) + .doOnNext(i -> firstNextTlValue.set(REF1.get())) + .doOnSubscribe(s -> subscribeTlValue.set(REF1.get())) + .doOnCancel(() -> { + cancelTlValue.set(REF1.get()); + cancelled.countDown(); + }) + .delayElements(Duration.ofMillis(1)) + .contextWrite(Context.of(KEY1, "upstreamContext")) + // disabling prefetching to observe cancellation + .publishOn(Schedulers.parallel(), 1) + .doOnNext(i -> { + System.out.println(REF1.get()); + secondNextTlValue.set(REF1.get()); + itemDelivered.countDown(); + }) + .subscribeOn(Schedulers.boundedElastic()) + .contextCapture() + .subscribe(subscriber); + + itemDelivered.await(); + + subscriber.cancel(); + + cancelled.await(); + + assertThat(requestTlValue.get()).isEqualTo("upstreamContext"); + assertThat(subscribeTlValue.get()).isEqualTo("upstreamContext"); + assertThat(firstNextTlValue.get()).isEqualTo("upstreamContext"); + assertThat(cancelTlValue.get()).isEqualTo("upstreamContext"); + assertThat(secondNextTlValue.get()).isEqualTo("downstreamContext"); + } + + @Test + void prefetchingShouldMaintainThreadLocals() { + Hooks.enableAutomaticContextPropagation(); + + // We validate streams of items above default prefetch size + // (max concurrency of flatMap == Queues.SMALL_BUFFER_SIZE == 256) + // are able to maintain the context propagation to ThreadLocals + // in the presence of prefetching + int size = Queues.SMALL_BUFFER_SIZE * 10; + + Flux source = Flux.create(s -> { + for (int i = 0; i < size; i++) { + s.next(i); + } + s.complete(); + }); + + assertThat(REF1.get()).isEqualTo("ref1_init"); + + ArrayBlockingQueue innerThreadLocals = new ArrayBlockingQueue<>(size); + ArrayBlockingQueue outerThreadLocals = new ArrayBlockingQueue<>(size); + + source.publishOn(Schedulers.boundedElastic()) + .flatMap(i -> Mono.just(i) + .delayElement(Duration.ofMillis(1)) + .doOnNext(j -> innerThreadLocals.add(REF1.get()))) + .contextWrite(ctx -> ctx.put(KEY1, "present")) + .publishOn(Schedulers.parallel()) + .doOnNext(i -> outerThreadLocals.add(REF1.get())) + .blockLast(); + + assertThat(innerThreadLocals).containsOnly("present").hasSize(size); + assertThat(outerThreadLocals).containsOnly("ref1_init").hasSize(size); + } + + @Test + void queueWrapperWorksWithQueues() { + Hooks.enableAutomaticContextPropagation(); + Queue queue = Queues.small() + .get(); + + assertThat(queue.offer("1")).isTrue(); + assertThat(queue.poll()).isSameAs("1"); + assertThat(queue.add("2")).isTrue(); + assertThat(queue.remove()).isSameAs("2"); + assertThat(queue.isEmpty()).isTrue(); + assertThat(queue.addAll(Arrays.asList("3", "4", "5"))).isTrue(); + assertThat(queue.peek()).isSameAs("3"); + assertThat(queue.isEmpty()).isFalse(); + assertThat(queue.element()).isSameAs("3"); + assertThat(queue.size()).isEqualTo(3); + queue.clear(); + assertThat(queue.offer("0")).isTrue(); + assertThat(queue.size()).isEqualTo(1); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(queue::iterator); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.contains("0")); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(queue::toArray); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.toArray(new Object[] {})); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.remove("0")); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.containsAll(Collections.singletonList("0"))); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.retainAll(Collections.singletonList("5"))); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> queue.removeAll(Collections.singletonList("0"))); + } + + @Test + void isContextPropagationAvailable() { + assertThat(ContextPropagationSupport.isContextPropagationAvailable()).isTrue(); + } + + @Test + void contextCaptureWithNoPredicateReturnsTheConstantFunction() { + assertThat(ContextPropagation.contextCapture()) + .as("no predicate nor registry") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE); + } + + @Test + void fluxApiUsesContextPropagationConstantFunction() { + Flux source = Flux.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(FluxContextWrite.class, fcw -> + assertThat(fcw.doOnContext) + .as("flux's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Test + void monoApiUsesContextPropagationConstantFunction() { + Mono source = Mono.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(MonoContextWrite.class, fcw -> + assertThat(fcw.doOnContext) + .as("mono's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Test + void fluxApiUsesContextPropagationConstantFunctionWhenAutomaticPropagationEnabled() { + Hooks.enableAutomaticContextPropagation(); + Flux source = Flux.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(FluxContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("flux's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Test + void monoApiUsesContextPropagationConstantFunctionWhenAutomaticPropagationEnabled() { + Hooks.enableAutomaticContextPropagation(); + Mono source = Mono.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(MonoContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("mono's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE)); + } + + @Nested + class ContextCaptureFunctionTest { + + @Test + void contextCaptureFunctionWithoutFiltering() { + Function test = ContextPropagation.contextCapture(); + + REF1.set("expected1"); + REF2.set("expected2"); + + Context ctx = test.apply(Context.empty()); + Map asMap = new HashMap<>(); + ctx.forEach(asMap::put); //easier to assert + + assertThat(asMap) + .containsEntry(KEY1, "expected1") + .containsEntry(KEY2, "expected2") + .hasSize(2); + } + } + + @Nested + class NonReactorSources { + @Test + void fluxFromPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Hooks.enableAutomaticContextPropagation(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Flux.from(nonReactorPublisher) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Hooks.enableAutomaticContextPropagation(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.from(nonReactorPublisher) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromPublisherIgnoringContract() + throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Hooks.enableAutomaticContextPropagation(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.fromDirect(nonReactorPublisher) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromCompletionStage() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + Hooks.enableAutomaticContextPropagation(); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(); + + // we need to delay delivery to ensure the completion signal is delivered + // on a Thread from executorService + CompletionStage completionStage = CompletableFuture.supplyAsync(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + return "test"; + }, executorService); + + TestSubscriber testSubscriber = TestSubscriber.create(); + + Mono.fromCompletionStage(completionStage) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(testSubscriber); + + latch.countDown(); + testSubscriber.block(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromFuture() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + Hooks.enableAutomaticContextPropagation(); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(); + + // we need to delay delivery to ensure the completion signal is delivered + // on a Thread from executorService + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + return "test"; + }, executorService); + + TestSubscriber testSubscriber = TestSubscriber.create(); + + Mono.fromFuture(future) + .doOnNext(s -> value.set(REF1.get())) + .contextWrite(Context.of(KEY1, "present")) + .subscribe(testSubscriber); + + latch.countDown(); + testSubscriber.block(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF1.get())).get(); + + assertThat(value.get()).isEqualTo("ref1_init"); + + // validate the current Thread does not have the value set either + assertThat(REF1.get()).isEqualTo("ref1_init"); + + executorService.shutdownNow(); + } + } + + // TAP AND HANDLE OPERATOR TESTS + + static private enum Cases { + NORMAL_NO_CONTEXT(false, false, false), + NORMAL_WITH_CONTEXT(false, false, true), + CONDITIONAL_NO_CONTEXT(false, true, false), + CONDITIONAL_WITH_CONTEXT(false, true, true), + FUSED_NO_CONTEXT(true, false, false), + FUSED_WITH_CONTEXT(true, false, true), + FUSED_CONDITIONAL_NO_CONTEXT(true, true, false), + FUSED_CONDITIONAL_WITH_CONTEXT(true, true, true); + + final boolean fusion; + final boolean conditional; + final boolean withContext; + + Cases(boolean fusion, boolean conditional, boolean withContext) { + this.fusion = fusion; + this.conditional = conditional; + this.withContext = withContext; + } + } + + @Nested + class ContextRestoreForTap { + + @EnumSource(Cases.class) + @ParameterizedTestWithName + void properWrappingForFluxTap(Cases characteristics) { + SignalListener originalListener = Mockito.mock(SignalListener.class); + SignalListenerFactory originalFactory = new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + return originalListener; + } + }; + + Publisher tap; + TestSubscriberBuilder builder = TestSubscriber.builder(); + if (characteristics.fusion) { + tap = new FluxTapFuseable<>(Flux.empty(), originalFactory); + builder = builder.requireFusion(Fuseable.ANY); + } + else { + tap = new FluxTap<>(Flux.empty(), originalFactory); + builder = builder.requireNotFuseable(); + } + + if (characteristics.withContext) { + builder = builder.contextPut("properWrappingForFluxTap", true); + } + + TestSubscriber testSubscriber; + if (characteristics.conditional) { + testSubscriber = builder.buildConditional(v -> true); + } + else { + testSubscriber = builder.build(); + } + + tap.subscribe(testSubscriber); + Scannable parent = testSubscriber.parents().findFirst().get(); + + if (!characteristics.fusion) { + assertThat(parent).isInstanceOfSatisfying(FluxTap.TapSubscriber.class, + tapSubscriber -> { + if (characteristics.withContext) { + assertThat(tapSubscriber.listener).as("listener wrapped") + .isNotSameAs(originalListener) + .isExactlyInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + } + else { + assertThat(tapSubscriber.listener) + .as("listener not wrapped") + .isSameAs(originalListener); + } + }); + } + else { + assertThat(parent).isInstanceOfSatisfying(FluxTapFuseable.TapFuseableSubscriber.class, + tapSubscriber -> { + if (characteristics.withContext) { + assertThat(tapSubscriber.listener) + .as("listener wrapped") + .isNotSameAs(originalListener) + .isExactlyInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + } + else { + assertThat(tapSubscriber.listener) + .as("listener not wrapped") + .isSameAs(originalListener); + } + }); + } + } + + @EnumSource(Cases.class) + @ParameterizedTestWithName + void properWrappingForMonoTap(Cases characteristics) { + SignalListener originalListener = Mockito.mock(SignalListener.class); + SignalListenerFactory originalFactory = new SignalListenerFactory() { + @Override + public Void initializePublisherState(Publisher source) { + return null; + } + + @Override + public SignalListener createListener(Publisher source, + ContextView listenerContext, Void publisherContext) { + return originalListener; + } + }; + + Mono tap; + TestSubscriberBuilder builder = TestSubscriber.builder(); + if (characteristics.fusion) { + tap = new MonoTapFuseable<>(Mono.empty(), originalFactory); + builder = builder.requireFusion(Fuseable.ANY); + } + else { + tap = new MonoTap<>(Mono.empty(), originalFactory); + builder = builder.requireNotFuseable(); + } + + if (characteristics.withContext) { + builder = builder.contextPut("properWrappingForMonoTap", true); + } + + TestSubscriber testSubscriber; + if (characteristics.conditional) { + testSubscriber = builder.buildConditional(v -> true); + } + else { + testSubscriber = builder.build(); + } + + tap.subscribe(testSubscriber); + Scannable parent = testSubscriber.parents().findFirst().get(); + + if (!characteristics.fusion) { + assertThat(parent).isInstanceOfSatisfying(FluxTap.TapSubscriber.class, + tapSubscriber -> { + if (characteristics.withContext) { + assertThat(tapSubscriber.listener) + .as("listener wrapped") + .isNotSameAs(originalListener) + .isExactlyInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + + } + else { + assertThat(tapSubscriber.listener).as("listener not wrapped").isSameAs(originalListener); + } + }); + } + else { + assertThat(parent).isInstanceOfSatisfying(FluxTapFuseable.TapFuseableSubscriber.class, + tapSubscriber -> { + if (characteristics.withContext) { + assertThat(tapSubscriber.listener) + .as("listener wrapped") + .isNotSameAs(originalListener) + .isExactlyInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + } + else { + assertThat(tapSubscriber.listener).as("listener not wrapped").isSameAs(originalListener); + } + }); + } + } + + @Test + void threadLocalRestoredInSignalListener() throws InterruptedException { + REF1.set(null); + Context context = Context.of(KEY1, "expected"); + List list = new ArrayList<>(); + + SignalListener tlReadingListener = Mockito.mock(SignalListener.class, invocation -> { + list.add(invocation.getMethod().getName() + ": " + REF1.get()); + return null; + }); + + ContextPropagation.ContextRestoreSignalListener listener = + new ContextPropagation.ContextRestoreSignalListener<>(tlReadingListener, context); + + Thread t = new Thread(() -> { + try { + listener.doFirst(); + listener.doOnSubscription(); + + listener.doOnFusion(1); + listener.doOnRequest(1L); + listener.doOnCancel(); + + listener.doOnNext(1); + listener.doOnComplete(); + listener.doOnError(new IllegalStateException("boom")); + + listener.doAfterComplete(); + listener.doAfterError(new IllegalStateException("boom")); + listener.doFinally(SignalType.ON_COMPLETE); + + listener.doOnMalformedOnNext(1); + listener.doOnMalformedOnComplete(); + listener.doOnMalformedOnError(new IllegalStateException("boom")); + + listener.addToContext(Context.empty()); + listener.handleListenerError(new IllegalStateException("boom")); + } + catch (Throwable error) { + error.printStackTrace(); + } + }); + t.start(); + t.join(); + + assertThat(list).as("extracted TLs") + .containsExactly( + "doFirst: expected", + "doOnSubscription: expected", + "doOnFusion: expected", + "doOnRequest: expected", + "doOnCancel: expected", + "doOnNext: expected", + "doOnComplete: expected", + "doOnError: expected", + "doAfterComplete: expected", + "doAfterError: expected", + "doFinally: expected", + "doOnMalformedOnNext: expected", + "doOnMalformedOnComplete: expected", + "doOnMalformedOnError: expected", + "addToContext: expected", + "handleListenerError: expected" + ); + } + + } + + @Nested + class ContextRestoreForHandle { + + @ValueSource(booleans = {true, false}) + @ParameterizedTestWithName + void publicMethodChecksForContextNotEmptyBeforeWrapping(boolean withContext) { + BiConsumer> originalHandler = (v, sink) -> { }; + final Context context; + if (withContext) { + context = Context.of(KEY1, "expected"); + } + else { + context = Context.empty(); + } + + BiConsumer> decoratedHandler = ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators() ? + ContextPropagation.contextRestoreForHandle(originalHandler, () -> context) : originalHandler; + + if (withContext) { + assertThat(decoratedHandler).as("context not empty: decorated handler").isNotSameAs(originalHandler); + } + else { + assertThat(decoratedHandler).as("empty context: same handler").isSameAs(originalHandler); + } + } + + @Test + void classContextRestoreHandleConsumerRestoresThreadLocal() { + BiConsumer> originalHandler = (v, sink) -> { + if (v.equals("bar")) { + sink.next(v + "=" + REF1.get()); + } + }; + + final String expected = "bar=expected"; + final Context context = Context.of(KEY1, "expected"); + + BiConsumer> decoratedHandler = ContextPropagationSupport.shouldRestoreThreadLocalsInSomeOperators() ? + ContextPropagation.contextRestoreForHandle(originalHandler, () -> context) : originalHandler; + + SynchronousSink mockSink = Mockito.mock(SynchronousSink.class); + decoratedHandler.accept("bar", mockSink); + Mockito.verify(mockSink, Mockito.times(1)).next(expected); + } + + @SuppressWarnings("rawtypes") + @Test + void fluxHandleVariantsCallTheWrapper() { + BiConsumer> originalHandler = (v, sink) -> {}; + + FluxHandle publisher = new FluxHandle<>(Flux.empty(), originalHandler); + FluxHandleFuseable publisherFuseable = new FluxHandleFuseable<>(Flux.empty(), originalHandler); + + CoreSubscriber actual = TestSubscriber.builder().contextPut("fluxHandleVariantsCallTheWrapper", true).build(); + Fuseable.ConditionalSubscriber actualCondi = TestSubscriber.builder().contextPut("fluxHandleVariantsCallTheWrapper", true).buildConditional(v -> true); + + HandleSubscriber sub = (HandleSubscriber) publisher.subscribeOrReturn(actual); + HandleConditionalSubscriber subCondi = (HandleConditionalSubscriber) publisher.subscribeOrReturn(actualCondi); + HandleFuseableSubscriber subFused = (HandleFuseableSubscriber) publisherFuseable.subscribeOrReturn(actual); + HandleFuseableConditionalSubscriber subFusedCondi = (HandleFuseableConditionalSubscriber) publisherFuseable.subscribeOrReturn(actualCondi); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(publisher.handler).as("publisher.handler").isSameAs(originalHandler); + softly.assertThat(publisherFuseable.handler).as("publisherFuseable.handler").isSameAs(originalHandler); + + softly.assertThat(sub.handler) + .as("sub.handler") + .isNotSameAs(originalHandler); + softly.assertThat(subCondi.handler) + .as("subCondi.handler") + .isNotSameAs(originalHandler); + softly.assertThat(subFused.handler) + .as("subFused.handler") + .isNotSameAs(originalHandler); + softly.assertThat(subFusedCondi.handler) + .as("subFusedCondi.handler") + .isNotSameAs(originalHandler); + }); + } + + @SuppressWarnings("rawtypes") + @Test + void monoHandleVariantsCallTheWrapper() { + BiConsumer> originalHandler = (v, sink) -> {}; + + MonoHandle publisher = new MonoHandle<>(Mono.empty(), originalHandler); + MonoHandleFuseable publisherFuseable = new MonoHandleFuseable<>(Mono.empty(), originalHandler); + + CoreSubscriber actual = TestSubscriber.builder().contextPut("monoHandleVariantsCallTheWrapper", true).build(); + + HandleSubscriber sub = (HandleSubscriber) publisher.subscribeOrReturn(actual); + HandleFuseableSubscriber subFused = (HandleFuseableSubscriber) publisherFuseable.subscribeOrReturn(actual); + //note: unlike FluxHandle, MonoHandle doesn't have support for ConditionalSubscriber + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(publisher.handler).as("publisher.handler").isSameAs(originalHandler); + softly.assertThat(publisherFuseable.handler).as("publisherFuseable.handler").isSameAs(originalHandler); + + softly.assertThat(sub.handler) + .as("sub.handler") + .isNotSameAs(originalHandler); + softly.assertThat(subFused.handler) + .as("subFused.handler") + .isNotSameAs(originalHandler); + }); + } + } + + @Nested + class BlockingOperatorsAutoCapture { + + @Test + void monoBlock() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Mono.just("test") + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnNext(ignored -> value.set(REF1.get())) + .block(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void monoBlockOptional() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Mono.empty() + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF1.get())) + .blockOptional(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxBlockFirst() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnNext(ignored -> value.set(REF1.get())) + .blockFirst(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxBlockLast() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF1.get())) + .blockLast(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxToIterable() { + Hooks.enableAutomaticContextPropagation(); + + AtomicReference value = new AtomicReference<>(); + + REF1.set("present"); + + Iterable integers = Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF1.get())) + .toIterable(); + + assertThat(integers).hasSize(10); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF1.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + } +} diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index aeb8808411..ba33fe88ea 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -197,7 +197,6 @@ void threadLocalsRestoredAfterPollution() { } @Test - @SuppressWarnings("unchecked") void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { Hooks.enableAutomaticContextPropagation(); @@ -215,7 +214,7 @@ void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedExcep REF1.set("downstreamContext"); - Flux.just(1, 2) + Flux.just(1, 2, 3) .hide() .doOnRequest(r -> requestTlValue.set(REF1.get())) .doOnNext(i -> firstNextTlValue.set(REF1.get())) @@ -673,7 +672,7 @@ public SignalListener createListener(Publisher source, if (characteristics.withContext) { assertThat(tapSubscriber.listener).as("listener wrapped") .isNotSameAs(originalListener) - .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + .isInstanceOf(ContextPropagation.ContextRestore103SignalListener.class); } else { assertThat(tapSubscriber.listener) @@ -689,7 +688,7 @@ public SignalListener createListener(Publisher source, assertThat(tapSubscriber.listener) .as("listener wrapped") .isNotSameAs(originalListener) - .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + .isInstanceOf(ContextPropagation.ContextRestore103SignalListener.class); } else { assertThat(tapSubscriber.listener) @@ -750,7 +749,7 @@ public SignalListener createListener(Publisher source, assertThat(tapSubscriber.listener) .as("listener wrapped") .isNotSameAs(originalListener) - .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + .isInstanceOf(ContextPropagation.ContextRestore103SignalListener.class); } else { @@ -765,7 +764,7 @@ public SignalListener createListener(Publisher source, assertThat(tapSubscriber.listener) .as("listener wrapped") .isNotSameAs(originalListener) - .isInstanceOf(ContextPropagation.ContextRestoreSignalListener.class); + .isInstanceOf(ContextPropagation.ContextRestore103SignalListener.class); } else { assertThat(tapSubscriber.listener).as("listener not wrapped").isSameAs(originalListener); @@ -785,8 +784,8 @@ void threadLocalRestoredInSignalListener() throws InterruptedException { return null; }); - ContextPropagation.ContextRestoreSignalListener listener = - new ContextPropagation.ContextRestoreSignalListener<>( + ContextPropagation.ContextRestore103SignalListener listener = + new ContextPropagation.ContextRestore103SignalListener<>( tlReadingListener, context, ContextSnapshotFactory.builder() .contextRegistry(ContextRegistry.getInstance()) From a38988f14faeda4a0897560311a3eb8a27fbad5b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Jun 2023 10:01:56 +0300 Subject: [PATCH 123/312] [release] Prepare and release 3.5.7 Signed-off-by: OlegDokuka --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4ef0365bab..affe6f2cf5 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.6" - testCompile "io.projectreactor:reactor-test:3.5.6" + compile "io.projectreactor:reactor-core:3.5.7" + testCompile "io.projectreactor:reactor-test:3.5.7" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.7-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.7-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.8-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.8-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.6" + // implementation "io.projectreactor:reactor-tools:3.5.7" } ``` diff --git a/gradle.properties b/gradle.properties index d6f7f40fac..bfc390eb0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.7-SNAPSHOT -bomVersion=2022.0.7 -metricsMicrometerVersion=1.0.7-SNAPSHOT +version=3.5.7 +bomVersion=2022.0.8 +metricsMicrometerVersion=1.0.7 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 15a11b7cfc..528c7c0f58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.7" +micrometer = "1.10.8" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -26,9 +26,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.3" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.1"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.4" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.7" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From f66417042a1f440a0c26b75313a646d16462e939 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Tue, 13 Jun 2023 11:25:41 +0300 Subject: [PATCH 124/312] bumps micrometer-version to 1.10.7 for cp 1.0.2 Signed-off-by: Oleh Dokuka --- gradle/libs.versions.toml | 1 + reactor-core/build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 528c7c0f58..becff130d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ reactiveStreams = "1.0.4" jsr166backport = "io.projectreactor:jsr166:1.0.0.RELEASE" jsr305 = "com.google.code.findbugs:jsr305:3.0.1" micrometer-bom = { module = "io.micrometer:micrometer-bom", version.ref = "micrometer" } +micrometer102Compatible-bom = { module = "io.micrometer:micrometer-bom", version = "1.10.7" } micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 29a5e0f6fd..83e7fa062a 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -125,7 +125,7 @@ dependencies { withMicrometerTestImplementation libs.micrometer.contextPropagation withMicrometerTestImplementation sourceSets.test.output - withContextPropagation102TestImplementation platform(libs.micrometer.bom) + withContextPropagation102TestImplementation platform(libs.micrometer102Compatible.bom) withContextPropagation102TestImplementation libs.micrometer.commons withContextPropagation102TestImplementation libs.micrometer.core withContextPropagation102TestImplementation libs.micrometer.contextPropagation102 From 81c985238c7e2b8c5b35a31a1544ba3b405309c3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Jun 2023 11:27:54 +0300 Subject: [PATCH 125/312] [release] Prepare and release 3.5.7 From f6e77482a1c3449ba8212db117f45925108fa146 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 13 Jun 2023 19:06:36 +0300 Subject: [PATCH 126/312] removes queue-wrapping from acp (#3498) Signed-off-by: OlegDokuka --- reactor-core/src/main/java/reactor/core/publisher/Hooks.java | 1 - .../java/reactor/core/publisher/ContextPropagationTest.java | 3 +++ .../java/reactor/core/publisher/ContextPropagationTest.java | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java index 1b73aa56bd..fd909c5c0d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Hooks.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Hooks.java @@ -542,7 +542,6 @@ public static void disableContextLossTracking() { */ public static void enableAutomaticContextPropagation() { if (ContextPropagationSupport.isContextPropagationOnClasspath) { - Hooks.addQueueWrapper(CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.ContextQueue::new); Schedulers.onScheduleHook(CONTEXT_IN_THREAD_LOCALS_KEY, ContextPropagation.scopePassingOnScheduleHook()); ContextPropagationSupport.propagateContextToThreadLocals = true; diff --git a/reactor-core/src/withContextPropagation102Test/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withContextPropagation102Test/java/reactor/core/publisher/ContextPropagationTest.java index daaeab6a2d..069ec409bb 100644 --- a/reactor-core/src/withContextPropagation102Test/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withContextPropagation102Test/java/reactor/core/publisher/ContextPropagationTest.java @@ -40,6 +40,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.provider.EnumSource; @@ -164,6 +165,7 @@ void threadLocalsPresentAfterDelay() { } @Test + @Disabled("queue wrapping is removed starting from 3.5.7") void threadLocalsRestoredAfterPollution() { // this test validates Queue wrapping takes place Hooks.enableAutomaticContextPropagation(); @@ -285,6 +287,7 @@ void prefetchingShouldMaintainThreadLocals() { } @Test + @Disabled("queue wrapping is removed starting from 3.5.7") void queueWrapperWorksWithQueues() { Hooks.enableAutomaticContextPropagation(); Queue queue = Queues.small() diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index ba33fe88ea..5c5a594462 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -41,6 +41,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.provider.EnumSource; @@ -165,6 +166,7 @@ void threadLocalsPresentAfterDelay() { } @Test + @Disabled("queue wrapping is removed starting from 3.5.7") void threadLocalsRestoredAfterPollution() { // this test validates Queue wrapping takes place Hooks.enableAutomaticContextPropagation(); @@ -285,6 +287,7 @@ void prefetchingShouldMaintainThreadLocals() { } @Test + @Disabled("queue wrapping is removed starting from 3.5.7") void queueWrapperWorksWithQueues() { Hooks.enableAutomaticContextPropagation(); Queue queue = Queues.small() From 54eb705ce5194a7725fa5a9de5fe698b0c28a397 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Jun 2023 19:10:07 +0300 Subject: [PATCH 127/312] [release] Prepare and release 3.5.7 From e2b9d1c302920270f16991e217cca03b4f62e597 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Jun 2023 21:47:09 +0300 Subject: [PATCH 128/312] excludes mockito from micrometer dependencies Signed-off-by: Oleh Dokuka --- reactor-core-micrometer/build.gradle | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/reactor-core-micrometer/build.gradle b/reactor-core-micrometer/build.gradle index e23ff97aa4..595c90198b 100644 --- a/reactor-core-micrometer/build.gradle +++ b/reactor-core-micrometer/build.gradle @@ -54,12 +54,21 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" testImplementation platform(libs.micrometer.bom) - testImplementation libs.micrometer.core - testImplementation libs.micrometer.test - testImplementation libs.micrometer.observation.test - testImplementation libs.micrometer.contextPropagation + testImplementation (libs.micrometer.core) { + exclude group: "org.mockito" + } + testImplementation (libs.micrometer.test) { + exclude group: "org.mockito" + } + testImplementation(libs.micrometer.observation.test) { + exclude group: "org.mockito" + } + testImplementation (libs.micrometer.contextPropagation) { + exclude group: "org.mockito" + } testImplementation(libs.micrometer.tracing.test) { //brings in context-propagation - exclude group: "io.micrometer", module: "context-propagation" + exclude group: "io.micrometer", module: "context-propagation" + exclude group: "org.mockito" } testImplementation(project(":reactor-test")) { From 0e0cdf8224aaf85dba3f2ba5ea5ad2665a3d7ced Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 13 Jun 2023 21:50:01 +0300 Subject: [PATCH 129/312] [release] Prepare and release 3.5.7 From 026fc72656326acdc772d3a1b557be0e358f9e28 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Tue, 13 Jun 2023 22:28:42 +0300 Subject: [PATCH 130/312] [release] Next development version 3.5.8-SNAPSHOT Signed-off-by: OlegDokuka --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index bfc390eb0b..ac4dbaf4a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.7 +version=3.5.8-SNAPSHOT bomVersion=2022.0.8 -metricsMicrometerVersion=1.0.7 +metricsMicrometerVersion=1.0.8-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index becff130d8..b1f055f88b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.6" -baselinePerfCore = "3.5.6" +baseline-core-api = "3.5.7" +baselinePerfCore = "3.5.7" baselinePerfExtra = "3.5.1" # Other shared versions From 4619b9836e6bdee65b1b4e888b07c76c0a293227 Mon Sep 17 00:00:00 2001 From: tejavenkat lanka Date: Wed, 21 Jun 2023 14:25:18 +0530 Subject: [PATCH 131/312] Fix a compilation error in sample code in docs (#3511) Resolves #3502 --- docs/asciidoc/faq.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/asciidoc/faq.adoc b/docs/asciidoc/faq.adoc index 5943d371d0..16d5b08ba4 100644 --- a/docs/asciidoc/faq.adoc +++ b/docs/asciidoc/faq.adoc @@ -237,7 +237,8 @@ Consider the following example: ==== [source,java] ---- -Flux source = Sinks.many().unicast().onBackpressureBuffer().asFlux(); +Sinks.Many dataSinks = Sinks.many().unicast().onBackpressureBuffer(); +Flux source = dataSinks.asFlux(); source.publishOn(scheduler1) .map(i -> transform(i)) .publishOn(scheduler2) From 55e9027159a55f4488624bbb3f57b51ee35c85e3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 21 Jun 2023 12:47:27 +0300 Subject: [PATCH 132/312] fixes failing tests Signed-off-by: Oleh Dokuka --- .../test/java/reactor/core/publisher/BlockingIterableTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/reactor-core/src/test/java/reactor/core/publisher/BlockingIterableTest.java b/reactor-core/src/test/java/reactor/core/publisher/BlockingIterableTest.java index ae2f02bd54..12e4241c05 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/BlockingIterableTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/BlockingIterableTest.java @@ -240,6 +240,7 @@ public void scanSubscriberCancelled() { public void hasNextInterrupt() throws InterruptedException { BlockingIterable.SubscriberIterator test = new BlockingIterable.SubscriberIterator<>( Queues.one().get(), + Context.empty(), 123 ); From 95520f377e3a46a507f8d17e5de125afef4d32ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 21 Jun 2023 14:32:12 +0200 Subject: [PATCH 133/312] Removes unnecessary JSR305 import from package-info.java (#3510) --- reactor-core/src/main/java/reactor/util/package-info.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/reactor-core/src/main/java/reactor/util/package-info.java b/reactor-core/src/main/java/reactor/util/package-info.java index 23c1a2972e..1b27e5add4 100644 --- a/reactor-core/src/main/java/reactor/util/package-info.java +++ b/reactor-core/src/main/java/reactor/util/package-info.java @@ -20,5 +20,4 @@ @NonNullApi package reactor.util; -import reactor.util.annotation.NonNullApi; -import javax.annotation.Nullable; \ No newline at end of file +import reactor.util.annotation.NonNullApi; \ No newline at end of file From eb88f625237d61108368f3cef8c22f4177f7c79b Mon Sep 17 00:00:00 2001 From: tejavenkat lanka Date: Thu, 22 Jun 2023 13:18:04 +0530 Subject: [PATCH 134/312] Replaced Signal#getContext with Signal#getContextView in sample code (#3513) Resolves #3503 --- docs/asciidoc/faq.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/asciidoc/faq.adoc b/docs/asciidoc/faq.adoc index 16d5b08ba4..ecbba8d173 100644 --- a/docs/asciidoc/faq.adoc +++ b/docs/asciidoc/faq.adoc @@ -294,7 +294,7 @@ The following is an example of such a helper function around a single MDC variab public static Consumer> logOnNext(Consumer logStatement) { return signal -> { if (!signal.isOnNext()) return; <1> - Optional toPutInMdc = signal.getContext().getOrEmpty("CONTEXT_KEY"); <2> + Optional toPutInMdc = signal.getContextView().getOrEmpty("CONTEXT_KEY"); <2> toPutInMdc.ifPresentOrElse(tpim -> { try (MDC.MDCCloseable cMdc = MDC.putCloseable("MDC_KEY", tpim)) { <3> @@ -348,7 +348,7 @@ For completeness, we can also see what an error-logging helper could look like: public static Consumer> logOnError(Consumer errorLogStatement) { return signal -> { if (!signal.isOnError()) return; - Optional toPutInMdc = signal.getContext().getOrEmpty("CONTEXT_KEY"); + Optional toPutInMdc = signal.getContextView().getOrEmpty("CONTEXT_KEY"); toPutInMdc.ifPresentOrElse(tpim -> { try (MDC.MDCCloseable cMdc = MDC.putCloseable("MDC_KEY", tpim)) { From 6431bcbb972ecc266eb9e6376d629cd960bebce2 Mon Sep 17 00:00:00 2001 From: Roman Zandler Date: Thu, 29 Jun 2023 10:56:31 +0300 Subject: [PATCH 135/312] [doc] Document behavior of Mono.zip() for empty-completed sources (#3514) Improves the documentation to provide an explanation about subscription behaviour of the zip operator in case any provided source completes without producing items. Resolves #3306. --- docs/asciidoc/faq.adoc | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/docs/asciidoc/faq.adoc b/docs/asciidoc/faq.adoc index ecbba8d173..a62413ad66 100644 --- a/docs/asciidoc/faq.adoc +++ b/docs/asciidoc/faq.adoc @@ -157,6 +157,96 @@ myMethod.emptySequenceForKey("a") // this method returns empty Mono ---- ==== +[[faq.monoZipEmptyCompletion]] +== Using `zip` along with empty-completed publishers + +When using the `zip` operator along with empty-completed publishers (i.e., publishers completing without emitting an item), it is important to be aware of the following behavior. + +Consider the following test case: + +==== +[source,java] +---- + @Test + public void testZipEmptyCompletionAllSubscribed() { + AtomicInteger cnt = new AtomicInteger(); + Mono mono1 = Mono.create(sink -> { + cnt.incrementAndGet(); + sink.success(); + }); + Mono mono2 = Mono.create(sink -> { + cnt.incrementAndGet(); + sink.success(); + }); + Mono zippedMono = Mono.zip(mono1, mono2, (v1, v2) -> v1); + zippedMono.subscribe(); + assertEquals(2, cnt.get()); + } +---- +==== + +While in this case the resulting `zippedMono` subscribes to both `mono1` and `mono2`, such behaviour is not guaranteed for all cases. For instance, consider the following test case: + +==== +[source,java] +---- + @Test + public void testZipEmptyCompletionOneSubscribed() { + AtomicInteger cnt = new AtomicInteger(); + Mono mono1 = Mono.create(sink -> { + cnt.incrementAndGet(); + sink.success(); + }); + Mono mono2 = Mono.create(sink -> { + cnt.incrementAndGet(); + sink.success(); + }); + Mono mono3 = Mono.create(sink -> { + cnt.incrementAndGet(); + sink.success(); + }); + Mono zippedMono = Mono.zip(mono1, Mono.zip(mono2, mono3, (v1, v2) -> v1), (v1, v2) -> v1); + zippedMono.subscribe(); + assertEquals(1, cnt.get()); + } +---- +==== +In this case upon empty completion of `mono1`, `zippedMono` completes immediately and does not subscribe to `mono2` and `mono3`. + +Therefore, in cases where `zip` operator is used to combine empty-completed publishers, it is not guaranteed that the resulting publisher will subscribe to all the empty-completed publishers. + +If it is necessary to keep the semantics as shown in the second test case and to ensure subscription to all the publishers to be zipped, consider using `singleOptional` operator, as demonstrated in the test case below: + +==== +[source,java] +---- + +@Test +public void testZipOptionalAllSubscribed() { + AtomicInteger cnt = new AtomicInteger(); + Mono mono1 = Mono.create(sink -> { + cnt.incrementAndGet(); + sink.success(); + }); + Mono mono2 = Mono.create(sink -> { + cnt.incrementAndGet(); + sink.success(); + }); + Mono mono3 = Mono.create(sink -> { + cnt.incrementAndGet(); + sink.success(); + }); + Mono> zippedMono = + Mono.zip( + mono1.singleOptional(), + Mono.zip(mono2.singleOptional(), mono3.singleOptional(), (v1, v2) -> v1), + (v1, v2) -> v1); + zippedMono.subscribe(); + assertEquals(3, cnt.get()); +} +---- +==== + [[faq.retryWhen]] == How to Use `retryWhen` to Emulate `retry(3)`? From f99c72c8770f45760a2e7ec40dd5575736bbfe02 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Thu, 29 Jun 2023 18:01:51 +0300 Subject: [PATCH 136/312] adds CI support for 3.6.x line Signed-off-by: OlegDokuka --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f626c458e5..05ddc7d155 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,9 +2,9 @@ name: publish on: push: branches: # For branches, better to list them explicitly than regexp include + - 3.6.x - main - 3.4.x - - 3.3.x permissions: read-all jobs: # General job notes: we DON'T want to cancel any previous runs, especially in the case of a "back to snapshots" build right after a release push From 14b37496f62934fbe2ec1d23865e2ef687136858 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Thu, 29 Jun 2023 18:24:25 +0300 Subject: [PATCH 137/312] bumps to 3.6.0 Signed-off-by: OlegDokuka --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index ac4dbaf4a1..49e3d6d3cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.8-SNAPSHOT -bomVersion=2022.0.8 +version=3.6.0-SNAPSHOT +bomVersion=2023.0.0 metricsMicrometerVersion=1.0.8-SNAPSHOT From 1775de2c473d9d106313faa0acf576b82ca37c8c Mon Sep 17 00:00:00 2001 From: tejavenkat lanka Date: Sat, 1 Jul 2023 01:00:49 +0530 Subject: [PATCH 138/312] allows blocking call for `#handlePossibleCancellationInterrupt` (#3515) --- .../reactor/core/scheduler/ReactorBlockHoundIntegration.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java b/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java index 8d2987cb86..ba6b769fba 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/ReactorBlockHoundIntegration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import reactor.blockhound.BlockHound; import reactor.blockhound.integration.BlockHoundIntegration; +import java.util.concurrent.FutureTask; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; @@ -50,5 +51,6 @@ public void applyTo(BlockHound.Builder builder) { // For now, let's not add a separate integration, but rather let's define the class name manually // ContextRegistry reads files as part of the Service Loader aspect. If class is initialized in a non-blocking thread, BlockHound would complain builder.allowBlockingCallsInside("reactor.core.publisher.ContextPropagation", ""); + builder.allowBlockingCallsInside(FutureTask.class.getName(),"handlePossibleCancellationInterrupt"); } } From 573dc48a25426700f67051848c01f375f772dc91 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 3 Jul 2023 14:10:03 +0300 Subject: [PATCH 139/312] adds support for gradle 8.1.1 (#3521) --- build.gradle | 3 +- buildSrc/settings.gradle | 2 - gradle/dependencies.gradle | 2 +- gradle/libs.versions.toml | 8 +- gradle/wrapper/gradle-wrapper.properties | 2 +- reactor-core/build.gradle | 108 +++++++++++++++++------ reactor-tools/build.gradle | 55 ++++++++---- settings.gradle | 6 +- 8 files changed, 131 insertions(+), 55 deletions(-) diff --git a/build.gradle b/build.gradle index 4945a9c72c..32c31b4f43 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ plugins { alias(libs.plugins.asciidoctor.pdf) apply false alias(libs.plugins.japicmp) alias(libs.plugins.download) - alias(libs.plugins.testsets) // note: build scan plugin now must be applied in settings.gradle // plugin portal is now outdated due to bintray sunset, at least for artifactory gradle plugin alias(libs.plugins.bnd) apply false diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle index 889a868a8f..6558356262 100644 --- a/buildSrc/settings.gradle +++ b/buildSrc/settings.gradle @@ -14,8 +14,6 @@ * limitations under the License. */ -enableFeaturePreview("VERSION_CATALOGS") - //import the catalog from main project dependencyResolutionManagement { versionCatalogs { diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index d4c3e911c6..6b9fb65380 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -9,7 +9,7 @@ ext { cglibVersion = "3.3.0" javaObjectLayoutVersion = "0.17" jmhVersion = "1.36" - junitVersion = "5.9.2" + junitVersion = "5.9.3" logbackVersion = "1.2.11" mockitoVersion = "4.11.0" slf4jVersion = "1.7.36" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1f055f88b..0f46232f0d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,11 +41,11 @@ reactor-perfBaseline-extra = { module = "io.projectreactor.addons:reactor-extra" artifactory = { id = "com.jfrog.artifactory", version = "4.31.0" } asciidoctor-convert = { id = "org.asciidoctor.jvm.convert", version.ref = "asciidoctor" } asciidoctor-pdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor" } -bnd = { id = "biz.aQute.bnd.builder", version = "6.3.1" } +bnd = { id = "biz.aQute.bnd.builder", version = "6.4.0" } download = { id = "de.undercouch.download", version = "5.4.0" } japicmp = { id = "me.champeau.gradle.japicmp", version = "0.4.1" } jcstress = { id = "io.github.reyerizo.gradle.jcstress", version = "0.8.15" } -nohttp = { id = "io.spring.nohttp", version = "0.0.10" } -shadow = { id = "com.github.johnrengelman.shadow", version = "7.1.2" } +nohttp = { id = "io.spring.nohttp", version = "0.0.11" } +shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } spotless = { id = "com.diffplug.spotless", version = "6.13.0" } -testsets = { id = "org.unbroken-dome.test-sets", version = "4.0.0" } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 774fae8767..fae08049a6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 83e7fa062a..ad08afa33c 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import me.champeau.gradle.japicmp.JapicmpTask apply plugin: 'idea' // needed to avoid IDEA seeing the jmh folder as source apply plugin: 'biz.aQute.bnd.builder' -apply plugin: 'org.unbroken-dome.test-sets' +apply plugin: 'jvm-test-suite' apply plugin: 'jcstress' apply plugin: 'java-library' @@ -43,17 +43,61 @@ ext { ] } -testSets { - blockHoundTest - //TODO once can probably be removed in 3.6.0, but MUST keep the ReactorContextAccessorTest in that case - withMicrometerTest - withContextPropagation102Test - tckTest +testing { + suites { + test { + useJUnitJupiter() + } + blockHoundTest(JvmTestSuite) { + useJUnitJupiter() + sources.java.srcDirs = ['src/blockHoundTest/java'] + dependencies { + implementation project() + } + } + tckTest(JvmTestSuite) { + useTestNG() + sources.java.srcDirs = ['src/tckTest/java'] + dependencies { + implementation project() + implementation sourceSets.test.output + } + } + withMicrometerTest(JvmTestSuite) { + useJUnitJupiter() + sources.java.srcDirs = ['src/withMicrometerTest/java'] + dependencies { + implementation project() + implementation sourceSets.test.output + } + } + withContextPropagation102Test(JvmTestSuite) { + useJUnitJupiter() + sources.java.srcDirs = ['src/withContextPropagation102Test/java'] + dependencies { + implementation project() + implementation sourceSets.test.output + } + } + } } configurations { compileOnly.extendsFrom jsr166backport testCompileOnly.extendsFrom jsr166backport + + blockHoundTestApi.extendsFrom testApi + blockHoundTestImplementation.extendsFrom testImplementation + blockHoundTestCompileOnly.extendsFrom testCompileOnly + tckTestApi.extendsFrom testApi + tckTestImplementation.extendsFrom testImplementation + tckTestCompileOnly.extendsFrom testCompileOnly + withMicrometerTestApi.extendsFrom testApi + withMicrometerTestImplementation.extendsFrom testImplementation + withMicrometerTestCompileOnly.extendsFrom testCompileOnly + withContextPropagation102TestApi.extendsFrom testApi + withContextPropagation102TestImplementation.extendsFrom testImplementation + withContextPropagation102TestCompileOnly.extendsFrom testCompileOnly } dependencies { @@ -236,28 +280,33 @@ task loops(type: Test, group: 'verification') { } } -tasks.withType(Test).all { - if (it.name == "test") { - //configure tag support for the core test task - def tags = rootProject.findProperty("junit-tags") - if (tags != null) { - println "junit5 tags for core: $tags" - useJUnitPlatform() { - includeTags "$tags" - } - } - else { - useJUnitPlatform() - } +test { + def tags = rootProject.findProperty("junit-tags") + if (tags != null) { + println "junit5 tags for core: $tags" + useJUnitPlatform() { + includeTags "$tags" } - else if (it.name != "tckTest") { - //default to JunitPlatform - useJUnitPlatform() + } + else { + useJUnitPlatform() + } +} + +tasks.withType(Test).matching { !(it.name in testing.suites.names) }.configureEach { + def tags = rootProject.findProperty("junit-tags") + if (tags != null) { + println "junit5 tags for core: $tags" + useJUnitPlatform() { + includeTags "$tags" } + } + else { + useJUnitPlatform() + } } tckTest { - useTestNG() include '**/*Verification.*' doFirst { println "Additional tests from `${name}` (${includes})" @@ -305,6 +354,15 @@ jar { 'Automatic-Module-Name': 'reactor.core' } bnd(bndOptions) + + bundle { + // workaround for multi-version JARs + // see https://github.com/bndtools/bnd/issues/2227 + bnd '''\ + -fixupmessages: '^Classes found in the wrong directory: .*' + -exportcontents: io.micrometer.* + '''.stripIndent() + } } jacocoTestReport.dependsOn test diff --git a/reactor-tools/build.gradle b/reactor-tools/build.gradle index a20a126a65..86b91843a1 100644 --- a/reactor-tools/build.gradle +++ b/reactor-tools/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,22 +14,52 @@ * limitations under the License. */ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.apache.tools.ant.filters.ReplaceTokens apply plugin: 'com.github.johnrengelman.shadow' -apply plugin: 'org.unbroken-dome.test-sets' +apply plugin: 'jvm-test-suite' apply plugin: 'java-library' description = 'Reactor Tools' -testSets { - jarFileTest - javaAgentTest - buildPluginTest +testing { + suites { + test { + useJUnitJupiter() + } + jarFileTest(JvmTestSuite) { + useJUnitJupiter() + sources.java.srcDirs = ['src/jarFileTest/java'] + dependencies { + implementation project() + } + } + javaAgentTest(JvmTestSuite) { + useJUnitJupiter() + sources.java.srcDirs = ['src/javaAgentTest/java'] + dependencies { + implementation project() + } + } + buildPluginTest(JvmTestSuite) { + useJUnitJupiter() + sources.java.srcDirs = ['src/buildPluginTest/java'] + dependencies { + implementation project() + } + } + } } configurations { shaded + jarFileTestApi.extendsFrom testApi + jarFileTestImplementation.extendsFrom testImplementation + javaAgentTestApi.extendsFrom testApi + javaAgentTestImplementation.extendsFrom testImplementation + buildPluginTestApi.extendsFrom testApi + buildPluginTestImplementation.extendsFrom testImplementation } dependencies { @@ -87,7 +117,9 @@ jar { } shadowJar { - classifier = null + enableRelocation true + relocationPrefix "reactor.tools.shaded" + archiveClassifier.set('') dependsOn(project.tasks.jar) manifest { @@ -121,13 +153,6 @@ shadowJar { project.tasks.build.dependsOn(shadowJar) -task relocateShadowJar(type: com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation) { - target = tasks.shadowJar - prefix = "reactor.tools.shaded" -} - -tasks.shadowJar.dependsOn tasks.relocateShadowJar - project.tasks.jarFileTest.configure { systemProperty("jarFile", shadowJar.outputs.files.singleFile) dependsOn(shadowJar) @@ -191,7 +216,7 @@ buildPluginTest { project.tasks.buildPluginTest.dependsOn(generateMockGradle) project.tasks.check.dependsOn(buildPluginTest) -tasks.withType(Test).all { +tasks.withType(Test).matching { !(it.name in testing.suites.names) }.configureEach { useJUnitPlatform() } diff --git a/settings.gradle b/settings.gradle index 031f9fec9b..ee44636416 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,8 +19,4 @@ plugins { rootProject.name = 'reactor' -include 'benchmarks', 'reactor-core', 'reactor-test', 'reactor-tools', 'reactor-core-micrometer' - -//libs catalog is declared in ./gradle/libs.versions.toml -//TODO remove once Version Catalogs are stabilized. It is also activated in buildSrc -enableFeaturePreview("VERSION_CATALOGS") \ No newline at end of file +include 'benchmarks', 'reactor-core', 'reactor-test', 'reactor-tools', 'reactor-core-micrometer' \ No newline at end of file From e5fbb3efcbc3480d0b292e2ca2221b9f53411e99 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Mon, 3 Jul 2023 14:57:00 +0300 Subject: [PATCH 140/312] includes 3.5.x into CI Signed-off-by: OlegDokuka --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f626c458e5..149bc2b9ed 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,8 +3,8 @@ on: push: branches: # For branches, better to list them explicitly than regexp include - main + - 3.5.x - 3.4.x - - 3.3.x permissions: read-all jobs: # General job notes: we DON'T want to cancel any previous runs, especially in the case of a "back to snapshots" build right after a release push From 8e99bc058442fb2698dd7e6b2a32cbc29e6b2414 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Mon, 3 Jul 2023 15:00:42 +0300 Subject: [PATCH 141/312] Start producing 3.6.x snapshots with 3.6.0-SNAPSHOT Signed-off-by: OlegDokuka --- gradle.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index ac4dbaf4a1..9168e65d78 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.8-SNAPSHOT -bomVersion=2022.0.8 -metricsMicrometerVersion=1.0.8-SNAPSHOT +version=3.6.0-SNAPSHOT +bomVersion=2023.0.0-SNAPSHOT +metricsMicrometerVersion=1.1.0-SNAPSHOT From 4f1f4eb5c584cdde1e28b0e63e1fd0c3ac00eeac Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Mon, 3 Jul 2023 15:20:02 +0300 Subject: [PATCH 142/312] removes 3.6.x from CI build Signed-off-by: OlegDokuka --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3115959148..149bc2b9ed 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,6 @@ name: publish on: push: branches: # For branches, better to list them explicitly than regexp include - - 3.6.x - main - 3.5.x - 3.4.x From ab636678f8b13f88ecba32aec79eef5ee2f7e65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 4 Jul 2023 13:36:06 +0200 Subject: [PATCH 143/312] moves to micrometer snapshots (#3528) --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b1f055f88b..a2a0d6f1d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.8" +micrometer = "1.10.9-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -26,8 +26,8 @@ micrometer102Compatible-bom = { module = "io.micrometer:micrometer-bom", version micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.3" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.4-SNAPSHOT" +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.7" micrometer-test = { module = "io.micrometer:micrometer-test" } From c670caf267b394d055aedc136d2ced5e29e1a87f Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 4 Jul 2023 20:39:44 +0300 Subject: [PATCH 144/312] upgrades to Gradle 8.2 Signed-off-by: Oleh Dokuka --- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 4 +++- gradlew | 19 ++++++++++++------- gradlew.bat | 1 + 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 39834 zcmY(qV{|1@vn?9iwrv|7+qP{xJ5I+=$F`jv+ji1XM;+U~ea?CBp8Ne-wZ>TWb5_k- zRW+A?gMS=?Ln_OGLtrEoU?$j+Jtg0hJQDi3-TohW5u_A^b9Act5-!5t~)TlFb=zVn=`t z9)^XDzg&l+L`qLt4olX*h+!l<%~_&Vw6>AM&UIe^bzcH_^nRaxG56Ee#O9PxC z4a@!??RT zo4;dqbZam)(h|V!|2u;cvr6(c-P?g0}dxtQKZt;3GPM9 zb3C?9mvu{uNjxfbxF&U!oHPX_Mh66L6&ImBPkxp}C+u}czdQFuL*KYy=J!)$3RL`2 zqtm^$!Q|d&5A@eW6F3|jf)k<^7G_57E7(W%Z-g@%EQTXW$uLT1fc=8&rTbN1`NG#* zxS#!!9^zE}^AA5*OxN3QKC)aXWJ&(_c+cmnbAjJ}1%2gSeLqNCa|3mqqRs&md+8Mp zBgsSj5P#dVCsJ#vFU5QX9ALs^$NBl*H+{)+33-JcbyBO5p4^{~3#Q-;D8(`P%_cH> zD}cDevkaj zWb`w02`yhKPM;9tw=AI$|IsMFboCRp-Bi6@6-rq1_?#Cfp|vGDDlCs6d6dZ6dA!1P zUOtbCT&AHlgT$B10zV3zSH%b6clr3Z7^~DJ&cQM1ViJ3*l+?p-byPh-=Xfi#!`MFK zlCw?u)HzAoB^P>2Gnpe2vYf>)9|_WZg5)|X_)`HhgffSe7rX8oWNgz3@e*Oh;fSSl zCIvL>tl%0!;#qdhBR4nDK-C;_BQX0=Xg$ zbMtfdrHf$N8H?ft=h8%>;*={PQS0MC%KL*#`8bBZlChij69=7&$8*k4%Sl{L+p=1b zq1ti@O2{4=IP)E!hK%Uyh(Lm6XN)yFo)~t#_ydGo7Cl_s7okAFk8f-*P^wFPK14B* zWnF9svn&Me_y$dm4-{e58(;+S0rfC1rE(x0A-jDrc!-hh3ufR9 zLzd#Kqaf!XiR}wwVD%p_yubuuYo4fMTb?*pL>B?20bvsGVB>}tB?d&GVF`=bYRWgLuT!!j9c?umYj%eI(omP#Dd(mfF zXsr`)AOp%MTxp#z*J0DSA=~z?@{=YkqdbaDQujr?gNja^H+zXw9?dT9hlWs;a#+55 zkt%8xRaIEo&)2L9EY9eP74cjcnj%AV_+e41HH0Jac6n-mv=N`p7@Fjj@|{sh)QBql zE-YPr6eSr=L$!etl>$G9`TRJ<0WMyu1dl8rTroqF<~#+ZT>d1?f=V=$;OE$5Dypr1 zw(XXBVrtJ=Jv)?x0t4n$3GgUdyD%zkA50>QqY-Yc`EpwSGE19r5_6#-iqn*FNv%dr zyqIbbZJh#;63!5!q*JJB$&P>25-YG~{TiRL%|XOHhD4=ArIXpCwq&CKv|%D|9GqtB zS$1=t>o4M7d$t@hiH<#~zXU|hHAjdUTv zR<71yhm7y}b)n71$uBDfOzts(xyTfYnLQZvY$^s+S~EBF%f)s-mRxde5P|KPVm%C; zZCD9A7>f`v5yd!?1A*pwv!`q-a?GvRJJhR@-@ov~wchVU(`qLhp7EbDY;rHG%vhG% z+{P>zTOzG8d`odv;7*f>x=92!a}R#w9!+}_-tjS7pT>iXI15ZU6Wq#LD4|}>-w52} zfyV=Kpp?{Nn6GDu7-EjCxtsZzn5!RS6;Chg*2_yLu2M4{8zq1~+L@cpC}pyBH`@i{ z;`2uuI?b^QKqh7m&FGiSK{wbo>bcR5q(yqpCFSz(uCgWT?BdX<-zJ?-MJsBP59tr*f9oXDLU$Q{O{A9pxayg$FH&waxRb6%$Y!^6XQ?YZu_`15o z5-x{C#+_j|#jegLc{(o@b6dQZ`AbnKdBlApt77RR4`B-n@osJ-e^wn8*rtl8)t@#$ z@9&?`aaxC1zVosQTeMl`eO*#cobmBmO8M%6M3*{ghT_Z zOl0QDjdxx{oO`ztr4QaPzLsAf_l0(dB)ThiN@u(s?IH%HNy&rfSvQtSCe_ zz}+!R2O*1GNHIeoIddaxY#F7suK};8HrJeqXExUc=bVHnfkb2_;e8=}M>7W*UhSc- z8Ft~|2zxgAoY2_*4x=8i-Z6HTJbxVK^|FP)q=run-O0 z8oaSHO~wi?rJ~?J1zb^_;1on-zg=pw#mRjl*{!pl#EG$-9ZC*{T6$ntv=c_wgD}^B z#x%li0~0}kKl6Tvn61Ns|N4W_wzpwDqOcy7-3Z@q%w>r_3?th#weak;I_|haGk%#F&h| zEAxvb?ZqYZ$D$m+#F|tZG%s-+E5#Y1Et@v5Ch>?)Y9-tNv&p+>OjC%)dHr?U9_(mK zw2q=JjP&MCPIv{fdJI}dsBxL7AIzs8wepikGD4p#-q*QTkxz26{vaNZROLTrIpR3; z*Az3fcjD8lj)vUto~>!}7H53lK3+l(%c*fW#a{R2d$3<3cm~%VcWh+jqR8h0>v;V( zF4y9jCzmgw?-P`2X%&HK;?E*Nn}HAYUn!~uz8}IDzW+(ht{cx9Nzf%QR%Rhw(O2%QE#3rtsx~4V%Xnd> z`7oVbWl%nCDuck_L5CY%^lWGPW+m|o*PF`gv7{SxuIOpIR-0qu{fcqWsN(m8okFaNN=g9DgQ`8c4#Q3akjh=aXJMDnWmCheHhg+#qh$hgz%LMg7X%37AY*j5CJleB!%~_a!8mIK?3h6j_r(= ztV8qvPak21zIC7uLlg12BryEy%e`-{3dSV8n=@u`dyXqC&!d4mmV8hsait2SF z1^~hKzbVcsEr)H+HCzy&2rW0f>Bx?x{)K}$bRn){2Pa8eHtc`pcMt~JF-ekZr10N@>J^3U% zZ?5Lu>mOxi3mX7t_=3Z))A-82rs^6+g8*3w^;w+}^Am!S!c zcjkGeB+sQ5ucZt4aN$8rIH{+-KqWtHU2A&`KCT!%E@)=CqBQf`5^_KNLCk(#6~Hbj z?vTfwWpQsYc39-!g?VV8&;a^tEFN}mp(p7ZVKDejD~rvUs6FwcA9Ug>(jNnODeLnX zB09V$hNck7A3=>09Li^14a%frrt>+5MTVa5}d!8W~$r?{T^~f%YV&2oFFOdHZ+W-461bP_f zr=XH50NN@@gtQ=n>79e3$wtL*NGUKC<|S2(7%o+m>ijJIXaXVnVwfpZWH@fYUkYQJ z*P3%$4*N5xy4ahW`!Y9jH@`j}FQJ2Qw^$0yhJWA{Z&Spb(%?y(4)#+p5UTN&;j&@Y z8y*+wx`xfLXy2L7RLK~6I8^WRt&%h0dwRI60j%;!J(f`80Wl`t96JFu(~0^IRS*g-$IGS$#+8QxY?}x25E^_h!`yuuOJz9c>a3L`vc) z06t3`-)vWQI>tBkAzNtINbOsRmd2G=Ka($9B?iBJCCR$$wF)J>dY4q#l|!uI<()=8%evp ziiTDYFWO5?r_X@tBOcSN@&r|&xTDB!fF}g@NGHTM{{y8olafox=dOCu9O9u!#kenG zJgVQ3-&u}&`fvU|t-fAUzq+Tl75wtC3u3_pf7$qoouVoWN~mIUtXP?!l3ohg;LYHs zT>fB>F-lyg(ilR;OCS;9&o7SY2^ugYlWO}ai<12xzvh+R=5$2kJq@=h*IVVVZ)^$u27tLhOLV# z4nn+w3^prURshPx6UM_kXLNAh1ana69ZeS#TC$no-1Qu{ z#V0rjhzC3fh(L<6AVo^=E6Yq!c`Lre}$T!52UafPazM<+x=PO%{Q`xH9T9w7mJG6XV zscF#ORMKOf5z#a4Y`3WQ>47NKy;Sro_qS={sx3d?5H9Juy}DedhY_QOG}`P6M{855 zZp1owcyiDbOG}k-l@8!dVW?^|T(Z(8MWn+ltFu*8<=i88c`=Wq*Z@(bMC4Mr6`nV@ zkp*FSI;2+D^DD|>Sw21i7izopJO;_3sZ}u3uO_g#jIK&Y5z~H(WokolB9;3AX)|n~ zUe`jzAX4znlT#{R+7)ZyM?Q@uVO83DOXInC*fhbdd1Py~QexaxUbrIeE}rDD7u zK<;xyI9QY7*K5UYnt?e)AlCBB55cu?wSi+2Hz{$5kZ&o(5Av9`$Qb9C=Zc*|X}A*j z@nZl>XzxW`1a%Vum01W=VAu*FCNGaDqs#KLa)Xk6j@YB*57;O~6*KO>6u)-kWL%Zw z@AEm1o=j-$EGhu`41tWMH1j@{vAJot5bF#IpZu!-X=B|6ff22;3K|h-1ms*IS3Hb0 z@IAOeZp8Gf4>Qsbq=QK-uPS{9>7*jGBc;#N*L>&H*M1);i-0evQDR7(R%4rGSTD82 z{s3fpyvZxqH$vR3D5=2tIXF*MP^G!*5D`<$vMul9(GJjX|7om3f^!Wyzy*DaYj5_v z=~&Ypytt&>;CICFz=uY6oSLPPX03A(a=&*gPnddD$mA8?C)_P#_YLp;>-{^Xb6BQ^ zOtfbSrB$B+18pQ*Gw?;65qfB|rAxt2ct)1ti`>7_+Z6fh+U9zQpCb>;%AP2|9#kZK zw2K12j2*BzMzayoT%;?@7J=;CX!FSI{IF1SB}O-jZjT(0-AMe$FZgR%&Y3t+jD$Q+ zy3cGCGye@~FJOFx$03w;Q7iA-tN=%d@iUfP0?>2=Rw#(@)tTVT%1hR>=zHFQo*48- z)B&MKmZ8Nuna(;|M>h(Fu(zVYM-$4f*&)eF6OfW|9i{NSa zjIEBx$ZDstG3eRGP$H<;IAZXgRQ4W7@pg!?zl<~oqgDtap5G0%0BPlnU6eojhkPP( z&Iad8H2M2~dZPcA*lrwd(Bx9|XmkM0pV}3Am5^0MFl4fQ=7r3oEjG(kR0?NOs)O$> zglB)6Hm4n<03+Y?*hVb311}d&WGA`X3W!*>QOLRcZpT}0*Sxu(fwxEWL3p;f8SAsg zBFwY`%Twg&{Cox+DqJe8Di+e*CG??GVny0~=F)B5!N%HW(pud_`43@ye*^)MY_IWa z$Frnbs`&@zY~IuX5ph`05}S|V=TkrOq8$rL`0ahD$?LrT&_Y#Tc8azVT)l_D8M+H_ zwnRoF6PP>`+Mqv$b%Ad`GHUfIZ@ST(BUlOxEa32u%(4m}wGC|-5|W-bXR2n~cB_yG zdKsN(g38z1mDrOc#N*(sn0Em{uloQaQjI5a+dB{O62cX8ma-1$31T<;mG2&x-M1zQ zChtb`2r&k{?mjH5`}lw?O9JV!uOn?UP3M#fHUp=cxBb%PML70LPmiQKcq^FvojvtcZOCYEydgWQNAIrV0%IkxPmv)Qs^S zmLvL{F2@2dL%N^h=e6PRXa2lFh-sVtYlM1Qpp~@J7a19T>r^m-c7jZvDu*fb`U(;T zS-<-##+6Cv75X~D?Qq?ues%u!jBF(Y zIUnJIJJp~diP4wdU?54`;#zd^hZHa?76P3cnLEu#V!{F@Hpqm#X4W1HN8!VX5v&6W zKQ#Ri6w9~%aVjl6Q88)_;gH4||&p%hS9?1k@B725D5=L&$fMhxMi2%8__R)RBc0Hvur>!w7Xa6Uvni@ z-M$OMYiA1HoMqfnHs&K5H%2ezc5dj>A_TuZd4Qr!KJ5ZhljtBjT3*^sPX90A&m8*M z?Xx3`iM%6$mb>}UAvhvUS3*TGaL^sQ(hFc<_CRoL-r&;oX@N0g;K0y5*nQK=w#nvi zLnfCUUy*@0?cxGZMmRuvu}0w(AUq@uC^A4b41vdVsmKSrdL4BxqOJw8sUY)P>r+p) zw%X%tIjoew%BG{L`f^ocMtx~wQ(jAr%ZK}Vy>x7%xo_X;VkZ!ic|WNCH)WW;t4 zE~|&S+p@_f9xIx!=(f#uExcWOs`qDQKPnm;gxYBzj4iO%W+**s-`c#vqk z;hpHcBSV*Wa%DTA(u_u{isR4PgcO1>x?|AccFc^w;-Bxq_O+5jQV3$yUVaQlg4s59 zs@|ZELO22k&s6~h4q4%O)Ew;~wKkI65kC&(Ck>2G9~@ab3!5R=kIvfu>T>l!Mz3}L z*yeB){8laO${1xC@s%#F_E89?YUbqXSgp9mI3c`;=cLihTb=>+nr~i_xFq>r_+ieN zltGcpCFW2R-6j@74ChKK(ZFbs!!s=@nq2$6b z60H$h$(&CfxyO0UwlHEY^S<7wu|@6JK{)c|w_(C4-+FSF?iy8{FY1l65}9X1$Qa#( z)yNhnz5lG480H9oJsRdRHFxddQ{piIFZqGDOc0oyD6^D(CxW~fDWXKtbd3}~z2m4? zxyJ}qey{})xa{GBpPnR7{8@{vL!KF3)1$w>==~^CYQ&`SrlKA}ca_{ywJ&)(vrONU z`MZ=`jXu0zp@nH+24+c`FoWh&+$TLyJZ+(ygHExS!WXObvm6yqOsB;JVbA&ir^I>* zhim~-oI&{L^o24mh6HpUGd1d$GA)u>uQw*=J`5HhW=)yiaEx)dd2uZk$sKGbS`c$5 zI)L$3^TMIB-4r0!(uZ^oejT5P`S&a;UQ8$~+)8D^s5DGypyq4wL<;6PFm|Jy^;mz1 zhi+-pt=w^`v&IBWgK}Lo`fn~pTs3{~&ANBOzaUZz~c zM*cyzx1{QIcv_UUq9oW`FAFf#Fki3iara|&1HtpR2#wu>TutxnMh0Dh_cHiBPUfQo+v>aK09@y3!5u>0;;mKBv_oBXxPU(bBkNlj~o18?(tNrXa4g~o(#m3(ajqPU0qoaH~DjedUbfA0fcbp4M=u_@gF zNNP~e%ENNEkS4%P*L3#BYa5cw{(CeP@sY+Er(eD{Rkh@n0|uCl>|Eio-xm z2uEt#(w0yH2Wxv>6h1^3Th)^%Kctp-{mjFZ1?<#>SVoc8aUeAfG47|~>&=;=JtaOR zaBj&@I7<*`&^j!J>bH@^{Ta&l>)t-I=38&}ik2kJwn1#rw~@>3apDL0fAVFuAn1Mx z7zoG%)c^l)gWkgjH^l>!B(I#l5nTnmj2ZPt7VepToH8YL3@rC3aAUTZ7E{(vtGrn67u#c1>T4151-2olaIYPwPBA_P9^ zT)MH&vb|0#h>+^T3#**}Ven2sZdL3Myq!p+bzU$gK2Kk^jkJwh zepO$%drajHu=2bgO0y}tI#t~}5b`KJY;IQj&#lk(`Vwa z-+Lp^Np?>+Wia|z#`I!SW@sAEvijh>buf;(!)G}jWelyra1x)OM!Wgn_XTvimNQE) ztbtgCMUXPV=MA>P-2G%cFd2IK!5^8tVO!lG(qnQUa**au$Q=?*1vV$Jh7e0SFjUzu zUBRpkDW<$z4_DV9R0guKEc~Bfjx+=_srm=zVW<>Tdg>JCA5baQoWvwRmwg~bDwqCb zX=({}xx?ZQ+8$?GObN_F5=aR;r|jXBa!y7-e-F;SwB3ACQWt9+(E%P6OXa{1&5=|n zOm;d~Jktyf6=j!PQbUg{1;@4MbO*LrEJBsJ707zdY5i7{qdeEWtkxCb49bX~&x@{0 zuS6$E`tJpaCl*s}-TVm1)FFEVcPSQ77Auu1O|Yly)|~WZ-lO!0cL*4{bWW)q4JDTV ze#}fJv9pObE8eF`Bb4bgGUjZ#V5Gr;DKS1co@Qyxe!&FFH0I3`5$lUU{{kh$|uY(m+FQuf)ZS?{Hm zG(9h)3g;SwO-ZNXoU{ZXEQLqTXihvJFlW&PeTeR_$JSs-v;?7?wq*wVwE0oERWzp@ z(6CbDb_gM~XG`^xYv|#Y=lNU$ahYFXLZq1+Fqp?C|0(C7v1NgSoOl0V?-yU3?l*sw zR4`CpcdL6jfUk7J=F~FXC$HI&T_u-`H(RZ-ao9wk5~gsP}#JMbr-9IybPT zKE^{Fr6qspSUwfQ8!X6iBFRieSIT3-z$*e}$sw(l{>f4+L*4~%*-#IItJVbrxSI=^ zRn4&|Xk?{W=ZP5qRfLmU_$V;HBNK<>V%Xm>*Dc*9E)jcyO+$?IN`?VF<#{8H0N-^yEhtR5j>6ZK70+5rd6|5|0IB-&jR{Y;y-sDA@lqXvt*g zJ4lh`cLzraz-=Dj_Xb7&-ysYy1NB8^inO3K;4@#%~2xu?Xj)(s9b}a$R!s2KhpDZ|%6md^c_{(sD=32)hrm>lo=?HLmLJ z`%yhND<$<5$Bk$VQDXyxUXKFEHBES>xY_Wr$w(0DH;PiNT*W+7Ka&=(#3 zffXt$z?CQ&k?~6w3aeq9#TD!MHU41rqQ4)V0T&p>3MDzP#!|LND|RZ{jm!28xYgor zzqECq^uXX;@QZj@y*K^v#knPc6XsdK8dCl>gC(?>ay(OZx$@JoJqSsw%L?z*o0$x! zJl`lfuoEsW#ZpFBGd5!u_<$HfM5lvqK5`0NndUuZo~o-o;lu3x=^Azmo` zN3;zN)wef2A~_IFS|Qa$6+IjSuxNvS$yV4BEO8ILZ2tig<%IJN>2QD|WAc=gzu*G$ z$uF6}^rmERp&BUfDhtCX1Z_C0;}yF-4FBuF?$AfVX3}B zsCI{^qUP?}QrD{*Xpm$tjfm0sSuK(-&1jC_{@{>rfiBu>BltP*njy|0kTOgt@4-^6 zIL9_bYl)7gD`GeaCV3Qyq5CMPAFRkU(6FmMXAN$k_A(wgsvq=l6B0hKtxq zqH^ZaE+Y>&vJmdIP2=dC&S2QNkH%D`QN9!Pk35k@pR`(YxhE~vDE%AcRVa|=UtO2Oj=$*Pk-V!HiuZ1NxMF3TPe~xz;p@8VeEr;$M^aI zUtQM8+o8`!uCob zmsiMx{H41NPFS>1Xisf183g&fQG)hrwes%FEyxmg39MlU)gf|>-omm!gQU4On zJt@Pjytp;5<8Mle9(*8f($*m39Z!ty+{mQCdxc$(V|M$B zr#eh)yv#~2zhGwJ8UZ}F&pJ7t*4$iRgRx06-3!t}3qC6j6#D}m7)kqE%UO8v_?Dz; z38?6qb4N>u!792F7G?!yokb>#^NsYMc&$MgC4l^gS0Drk2-|;8IE=*50R~Qs#u$N$ zv>5Pi{y>G}F%*~3MwRW{0c)~_;V^qSmag?}c#ax5AG;k-$?p{I9qavY;eKKZ0jDV{ zdE)sMaGHstenmqaLckjCOWqRfs2OQwrxm(t>O_z5L0M~If5&qDGgn6Vl zlY4H_5AG1-u$Dk~o$_KC`(D85yqHT!n0)yQTA{&jARG^PEf8>a&YqE;M}-Wp6QThi zN| zGol9%&|!Ii`vDvQBn_pnmw5sDUq<6Wv-5FtOW0g5j?qCjHTumdX-35<+hAp~s}U5o z8A^MHK72zh$;)()ZxtQ zcqxsR(Nk)^i(0;m-eI-C8ngrA1FlVll9w4SP5Es4w#EUnr{DH(_0fWkfJ30G*jbb8=*9)gLqh+vS4@+Lu87{+2-Rc=$2HXTNNQ5 zl_RUQAs)1~Wo@>QoIxsQcIT>g)ontxy_!aw&;D{+wGNm%Z~V`*@|MXlQJ-d4yw5q; z{>OTNV}36~p|1xM5cZ==f|diNvsx?%BGl7YN%7D&M!4);aYe0 z&l%66;NGL-NBX%cy@#QWh{*|>PUTd%Ym(O4$|0Qs6BZ8VUIVTH8r-m{r96wJgp>dd z?AloIfb)6s_}};+94HCmoH~pdEfgs1c7v?!1n{Gwzp_80Abg(A9z5(I00&G+?UCeq zLr;g3KR7HU&kurul@pX(w;?IhoG_An2=$m4%TQ*ljt+C0QhK$tXR6z1+{I7U@+lr6 z3#;S21J(?NyBpFST+o9v<_+uiQQ|X!2U#^rxCOp;B(|0pT_TCutj@ID^6lxy%h74o zwwlWhHPv+nZ7vp%RT@)FfGYHtbSF4{qKcDPXfaHc=9MkYMmCgk^}UV|R8+n75d#?_ z^2G`}aKe&_O60Z(@Y`7$PW^OV{<%Oz$iZ4nuF#Gt@`cstRqFy?b4`x$5KP$Zbm*Zn z#)~b;LtZu%IEl7ZsP@bmSU1>I3n`rg+^_xVib^`ZqSehsV}^Mg0Go~YT(>a~juFW? z6N9NcFkL)Lfl}D3>U?XL*!5;4XN?CAV zBm5ldOm8_qw6%se4w?6m>#;|b5Sj}tV55zS9hVOuvKfAu&gv3J@Lo{iM4inB&jg71J1i;&WM@HS}O ze$SmM#w~dWP=cFB$`S4sX^q~tkqy2Hq4u`9z?xkCq;^7K?v}gkJO~(DX@(N!CRnvu ztdL2eg78}_lTHNXu4jo`NS3BC=h6ZFgRz7}azu4T?^I5{9zCjHUUV~?65=)4(UADPnk|!@Y=pZIpKy5}(F$HFBx`6tDy- zcO4n)uU)tJL$zi9XR7L1V@opZY;(W+M@`(OwJF{rSuNDnXaLx^aRYx4^wMY|7pyDv zMhVd+AY@V`0e|dFu@=duX(O>g9N{#PF+yB|R2FcIi}p(quk+tB%#=lSf&Dz;61-9? zYO@hNy`IvQ!Q1TaH}RUtTcnO( z38tR-%<7MyBeutubg6VDI^r9WPfGb%*;mM_eag!S9A2;4K2?!3e_bg@yi&#b?8eFI zPOH)(2KS`5h^-wJD;(-eO~7RI-m>kpv;|P&-rJ!L9KKF1mZlK5g77(gmJ`Pg0e)Em zb!bj8#@i^ozayNY!wx`w8Bxxx;lnBwIo1!IY>Oka7@!v@x29~l6q&!Lmm7xUQvxC` zv_fK;_4{tB9tpKHBgdc5JSq)0MiECOA_Pd47Ary}8DrihLeUU?Rr1+sVp6s@B9nDy zxqSzw=K#ofa9jC@cKtPlg-<~V0B|vh_^*5zh|>IHGLBR;%KLlKiHTD}RpvfqoSLb` zqh}LbOxh{O@-yzxX|SceOiEicwYNV>)(5b|7acaZkIF^e^my8Bel;Pv^kbM#TAvW?+CPF-8w%jc?1iYrdPR0M+d6Bel#l zH5d9O=N9fJNoqbh?Y#3V6<1pe-gj?W$|uU+bs9!UZSHqGXHtm|5U{pTI44G0MhCpR z%Vi%K#j`EqHCPy{JXljh>OAF@4XYyIfTNI$7f1_lQ+5mUbGgY_(yjIPfSUP`JxjOj z&d#n1)i_tHxMtfH@B>DJPAy$N5Pj%{hWh!{Gg}ha%$(o3*DU<~5W`|~~0Ahu6Kd{Oo6(Lo< z-jZ-n?Es`IPrA0FSw#bfR&7X+tR`)tlVThp<=YocC_di1<_BLyr0>l-sQuWF_d0%73{0&0z7ZH3Dkd3#MoU#^6xv$ zXJU1vZi*v4su^N807`n?Wj0W;k<(dT32}WGwmN*$!t^^oX$c8H@Q0(Nm?#LpyrSw?4}%AO%qG*7mpdDlVs-PO-ZH92;-F<9p9u#vfdMIZQ$zS}x36hydt6K5#nkHECWqmCcZr z1K}IM6v3ggF@qPpO*@~)T?M!iJ0U%ZY&CsX6kX)*gz^mU8i^?eC^P#a2=JB7P(Pk; zk0%5B>!WMOEvbQVj(00{)?fDeJ>xbf;XBG76irB^TFxM&pa|8MBR3KIs=Ps{9+Z)Z zWB6fH$9!Q)A%N|>=(8jEyrBv@ugtma(1orem3;ob0%$W&@_KAD{N+U#k8M}x$N)he z3vNZy(m92FH9wZ#$%Fd`V=&k{vH|g!g017(?A=hAG@|ULAdEnX>Q@fpUHxA=c1j0D zZXMQ5ttT8Yt4E57$+dHrG7Ad76KMUEf1Fj8?1XL^$^(k&6~BdkC00xpFF*MpnfPK| z3QFGIQFykL4B^A>XkeK?`BF|kRy6BzaCD334C zBvGQrlnqc>3-FiJL7t@v*osEMRC-sLJPyZ+jA03nQjXK$A;!M%zyqx@an%oD;xOi4 zWy4%$y;?mGvF}d-Vthx$c_aSX(<<>tj(dU5at51WLnw=th>`zM{jxwMu})!CY;cB} z?6J;}jgo}qKEAR}#!XI#OiGn-^GR!;W;IXA{09K%gSj?--Dn`xkMs(&HdPK3i9aZ- zVJIt${*+=#cJ*-@r@FP^9Mx)(+>N9OdLbMQUb-7|@g6t96$rF+oixyf*{?${!SZD8j3z-I*6c!|=$4o+ru7srWWe_qH&NZg-5jPq6QZ zdF$;6zUQ_BI$cjM2l}spQo!ijnAoPLeni(its-$FhjWOzBBwoU)?BG+kChS!Sr`^g zDMKYUVU9~G(%fZ5A!mNX4**Nw9D;ML5obF_;bm}zz^AHv3zw_aS zyf1JiifW6oiJfS7y93Vn?T-ZX=N0-yVH($bVE3>42>CdAqAwQ9?+?YW5iw7Y zeQ2j2Sm*@jqf8kl5x!Jzg#xsWJi3{j{v6-QeGEoF8sI2?$wjS*3tqjk1om6602hQkROLQ|U)0w&iMA7O>LrwZnEzSp%g$zv;uBN^6jI2LKi9(Z{d#Krqc~gEv)^bw5X@_0Q++t+mm25YE6nGMcHx+&_(^*bzIeehm(6h&srgPimn~AQ ze0pz~wmGI({WV=ct>xfG7kWZPo#h8L;XrD_o=^lBeHL!A+FkdHQ(0Yrs#b$Wyc*SP zV9Bn5iRN$I%hB(O+>RH(EdVK|`OSzU2m8D4V3sW`7l7;2r(}?crNbV?+}8t5N`z47 z2yDvlPyLvIMhygG1ix1Fai2KA>S8cUa=t;vnjl^nc!FCEL>);a(`cSNiY1Rx_d=0?a=FP{AQ?GrJia_&-UIkmb^UDTC0g7yp@m>h_d38@&Iy z(AkpzKdr6qE==pde{115P$?$1OaM8rB}t4gswVOgO>Y?0!Qx6hA{mTCU6ODL4oFdJ z8wKx-FshQ6D0Ut(i;1++lGC#6uc#Mf_n{(p6W8Bro!1Fxr-U02*wZ30nH>ooyI#b_ zfUnO3%Aos~x*&lNu=oRX^n6_&r+raSY*vk+;JJs>2PfJGq1;E|0ZbtJ> zczCsLujO86xDPxx0|SOLx)IVJ`mM#XdPaYWE6xG>6hg^Mo`5 zm+d*3Pyd?OB2OuBaL6K0n$atjx0O~cVnH=WJ=AuPTNITe6#*QVHc4CnLDQm#VDgP& zC^%IZi-Jj&%e7z2L67o^J?TPT`7>M9 zY$Nxrga-8XrtCpK5 zAlXC9dbLh*qr9mn-redGmX*V0bCm4L8ra2kwZ{MsZ@;w$w4aIiMQCZCdfPu*()Rp{ zF`<1QfG_vk_T>w&R;29dGiV@I&4@fpyY2R$^4H(a46>SwC|G}{R!hTqckS$3#SuHJ z?7}5y8EBeuwGbgy3gC9T5d1$}ol}q|K#*?R)3$Bfwl!_rw)Icjp0;h)=#Y~kuQN@Wx^1!F^hQ-6{jE4+fsz?HC;_@&X zFj^#Amuna09r>hECe#YyExG-6Nmk(vA{kz9L{>0gnWL_`OJ>Bq{0N!5WXWUCb+)T5 ze!ly`k;kxyS$%xj8PqBgQt(EWswcfad?g|T{P|4)0cH4sq9r>Xg)qhSUk=D6+$rh? zX3a?U7`{B1-zdWoi4$MJpAmaW?sGpN$2;5hhlVDKFLUtiw)?D#m=_WJ!s#rHv8LUZ zV12Wr?goD3O6!*6)_qn+^Ue@jl&nnWTtk-*e{ZkIac8h>40qrm-0J|p%&yfBqs+Ze zM<{6kv#00|=%EfVCOJ+}r#)h3NgNe+gN6ZN4lPh)_p7Q_^7z%-tqzL$MPSiHjo2&TY#FeyFikHzO-xD*ub+$Lbq_Xnplv$i zvCOLX{_TZIm?$cj*=t9`pGaU@_;6Y@tzwUEIuBdW-LMYpef9D;&5EY>nc=T=6s|h; z4+#|5myZ>SDlvHTG>Vf#{pwS^RDCDmg+`lV_IoRV(XS37pGs(e&9v6JnUhsQeEnA7 z^e^VB*e*nbTZLTTy+sMALzi$pQ5uUBo*lw&l^NihB@u8GXf%PQe?s$75LLl9X*W)^c}(6~_YVIz1+iTB(aY@@9u% zJ;A@~j<-1fJ8&3xqVR{C`#UJJ`GCP{@IRU#`m^LpsyQDOYKU#Lk*y;uKtoHMGAEX zVx5(?=AF~k^L5qmGA8iz^^Ms}^+`(dr!Xq9mC}$sOa_^LB6Xk>mH?f!la7dtBuWfR z-2tFF%+^VgOok;?XsR;;S4aEHQCV^uj+kUGIfw}>OC$acf7^b<)`xI!fKX-6LX}pt z?vT_0%a_;-(;E36cD&Qjfu^jYdCE3q*>Y+&6AMD0wRv*)cRJU!17i`^r*v8Ec-6&u zxqO1c_+E5kt|Kls5Zb#{v_NxS&P<*#<7nTZzC^OOqFFm#)@k* z-3W4ZKgp1>J)yn8t`tg_?LNHG*izhYJki2zKcV=63M1C)h^jxHd>FPK!)clpF&XqJ z18bf4D!>Zqz0#7?XTfnnKFum7k@511u{E)^?r*tb_`ihaDgqOJWzbEGxN(-j$sDjX z$@I90so^7cqDirLHhQnY=cqkI?U@yAS0Z6H+8x+BzOAbgiN@mT#xfBZV}{)vapf)defF8_wBvu2-LrMF1iZ>yz^%50llNsA$ERHjKZ5)29s zimAdF%@H2ZrIRcjQh@gQkCktbY5)|T5Qm(Jx)2ZSA(>}M(03e#tJI01Pcw+I7En)H zqAF|CK_SHN5qW!L?#=4ORaCe`R)NX&;ccQxx`b4hEG8mXE>TkU#u-pk?vp?zgW$vj zBxpd?676LN$k|Z6V&))rxHOM+6|m|JabNqR22sAE=FD-So%om9QkDhGI0E$hF`&B# z)sef^Zs8y*9H>8)FOa^7A6uZi2SCAh4uIK~V4fFug8~R{Nd|6V>~ihaMKqO*M56J; z2Mnhgp{ZRj)=s~_D{Q4|aF-I*cZwu3F43y+942vO9#A>3D{Kef%HEx()M=GJXqEdt zLHCvd+>hH5x9jorO6}h)DgkvD&sy2dI?8l*3f*<*F6H80{%{G4Xy3xTUb^?QGAZ7L)gWnx;qqS_!t0wMy7WQy!;w4J}f>^k`05Nc^MeJ;-)3E z5GL7*eJsKVOg=1eMrpOiv?q~#KrZTz&_q&Q&s-ObKKbFxkH6qB#_yY4SDg8r4oEY} z#pJu_B%+i#dFZ037=SHq>f_C>!K(gnUaf#jYt*a>Aui;{8Q2_=B3k&#uqFLfRE(8}c zqC51F)C?1-gF#6cPwIU%uZQ>?DcRW>LIKZ+Jyt!kEnAm8Sb!c$f?mz+!Pz$9mSzH2 z-?vzf=%ZXaCYC2uL`HG{+YIT$+`}Y&e_Fi440}w8_yp%2V&LPcZ`k&n?xSh*oW8gT z(>Dh9e(YC|V8n+!pHb{4azvvyBoJk|8#F#Sa){0-3cX~!SM^57?z8FnTli$=16*;ke-6`K!J8z@Pt4X%jzP_WuV$ML2<)#GH8Lst$n5kdqV< z&YK0%vV#1ZtA;wi+$_k-`d6AVOf8G7O|Dtj&9TA%8_xH(jKOz~qJ*K_`%%pD zW&Qb-&*H}Wg6!u4&54&d*2eL&>D+zOadNq3J_GOp*`@o(-iN)ZdfcIlM}SE|fs|@` zcY^(U^t2&DSl6jpSh8+t!n@eD$`^Ll zC2L@JqK-)vvhdq<6rgQgB@H@(rsh-qMSG||%@Y=SjH@?NTx*ZvWO&|16{I<&^^^W+aTWA+HW^RB=#@ZAlWN8E@E3hGal@x!9vkjGg zR*(3CqkF|;`V^7`Amg7>9L$9-+_%d~>yVp+a0xn}1E$EgTOj8!FmG(ze%NA6yF>3` z9%b#l9Z;y(J`fO#h6ITpK^w*PzOfvcU=tpg`iUUbB1~MNvDbP|>whw8zlmID=4LQM zG=Pk0Dc4NHSn{swaYk??W!w%h3GD@^A&$C<(km1a?%1`8Pb#F|G!vcptIfUM+2@c~ zuGUM_0ZIhBuuL$;i}nsm4)SH%v*B)?KTO2Hv}Q`wS^FZ5F%<$t?Tcl0#LtiMU<5;$ zQN>X!h!7f>Ov?dw#l}HmjN@8T!l+#61E`TQR3~9NQKRNkr4hJYE8@4sw6cEcdU_E? zPUNCgN-CJ+r)Y5EK`wJ}bBk;e<)SXkdW!GY!cUvdi56WCOXxASM0Z&D|xpk7scfw`2j*R3{RkQ#>p;KDNM<5;lSNMD{=(MZor)om|;vk50hnJ3WBkdVtz!W zlaOEO)=AtB&}gtEQ*@CtWPqAc@-k+s6wd9^oat)e0w_ML6dh<6-|EKt>$~Efq1h-_ zN%tS};AL%I{Mo-|kO3r5a_H17Hk!A=4~(g_d#L-+ImJ9We*}(-ROWwP+fbCy@shXXvJRY0Jt7a-uNen7;IQD$H$1?PoCVo9!Io7T$w#C}vFd+n z2ry%=vuB%`X5*zo6r>diO6<}T^_NVNqR`oC01=Dqd`p`ubfKi$aVnXI6T6u3Q`1wM z8fKhN^?n)oq~#bV5sizuXjO<292c-#=lPfHjyLe#O;fS%2I1!nvdU@|V{^Q07SDg& zjW&FzS}t+75T5!egGB7amAqrOapVe~7PlU@vWg>`IE%^^l|*$K2GW{3<{!0j*^|RS z0XuY+F!ucqgXDa&WslPS>3%s5YS3q7u=6~d683D7BTIC|RA6$t)aQpQQamE*;tlaw z@4#ASFnRV;3ygxs7>0jFJOah>MCy+v8*uQy$>?OA>69g2d2rt$(4}-;PlqO7 zX7LH{5$BHRFhyKlC^+F<2mJ;O;d*k-0amZ-QCFamE&at3ej@7oqmLq_$)OVG9;Pr| zFI21QH@~3D41UjHfWKx5`v?=nl{~_Eg*3c^R=lFP-(tvqMniu?C5$QbR-6uPn4l3q z(sha;lVms+N-6~{VwV-4{XjOJFuFe4{CtDP26EzBF)~U)5DlrDS-{x*A!|ZQ1u9k8J>Iok8UHhR^@%`AA58i1-kFepA){yqxyObN9-#=Fa!Kp6$E9$@W?T)BMZ(N7LtI z+lkK!&&ftg;_LcNj(2=m^8L(xS&-jJUhL@$0Dp3ri80(CZTcZD0}tOTA`AS|$Q_t( zECN#{_yI=JI5spuhtNz5n6EDw8Urc})cu~72{kfL)UYO0+Ou6_5^+FQC|Bi3bAQn$ z$rpO&ZkCsSY{2==1Oe~F(M@NnQw7`PWTUf5-2`4;Mgw7TV=cQ9vztPw?*TM$XBQ8kuCl^Sx(J8 zIJ7>c;D&0qq^WLR3hMUW9{;ua8lpQaC2#3%+_+GZdwHkKQQY`Iz({Q_zM`k-QKV{2 zIj-`W3Rm^Loufl+zcmjG2MLh;#o6lWTw9Ux$MJEsptbq0*>$(`j;HlFeEdqd z)Hwr>+U&AgD&&|nuhq@U(EX6{6h=CYjm`Svk}7X+3FnvO>FVf>4(*K$9`E*+mX_wG zCW!Qme`z#CYU`3vV{2+zZe2+cps3B-JJ;2kMbLCmrLnBSSy$beu(r#R@6`d4hNVp; zzE7y{R?0U1)ZofMK!uf9<;Bo)^51KV0ZFzOEr-Vz=<{ghbN*x zq>Tc3YY7jRo!Aj2zXm!a&-A1il<@hz+Ee!Xh>nD&%N)V~}I ztbDT(?0nB2%%J+p9L!*DCBWqWd$p`ObzTr4OPUEe1f_=5?E5$~+6!eRRqJ__qx_p0 z68~dD{qLbOeSj+=XP62{UBGD61tp54RnHWzbo|xas9h7EZq@S;pik0PhS5ZFi^dDk zg9t>$h=XRDzY~_$SL^Gp_^b)${IJb$ENZjw;Fw@$y~>(z$QJ~9mx`pzVzHV8?bt=a z&q!D?P{GLd-{bwjca-3_ZaYfpI+bcTq<&r-T~x|Iu=BhOQWVAxHMF;m)d)fUd& zj+)80_cT0&{IsS@Z;uAGTWRk%l}}Q?I*pGUG}kDreSqOO1@+G%t)PMa>f(#p9WKVo z-+r%XFWOa(Ih1i{Y`^-1AQ+E#C2P*uS}ki2!hmM8P<)nT0E0FB%h-NXDXoO<#8MtA z0(P-0<+@#}2vVwtJcQmNCZxYsRnsq@skl)oogppph7STBfXEbxo0)l|W^70Rh_xAn zT5$;Jegv#&%Oka{nQ3O6u6D-epRsCFYN4^S$WWJsQz^^+#m(h$bZsko+6_Wiu$26) zKdjr87bcvHfGNre&p?S@cAP!GIe2spn2r=`Df=RWYsty;_Ir{#+1+%Doj8l3_jg2k znB+`9Ze_XY&*XD5a`nf~F3uw;(fv7okwKnvGvp5OT`Ly~U-`W+Z2gfH>qkbu{5d`s z1=yL@O|6xx6=RWBB^%uNSBP%Ky$sfG)}6{bI-iPRK+fJqYVir>3HHu(i{+>0yTSp_ z;HCUGF7_PN;Owc|dz5&~Tod+|JfrCs>L?6$%=hew`@>^>#14r)Z?^8(p4_{y&p*Qm!aR>4(N>Ql@A1P3 zcLS0?fHB-fN|v&@oV2nyXciWizldm0q$^aPor)3Dq~b6jj8&sCFsOg84Teg2j0n||RN zKxf^~t;Mta=4~Wg|FpH0@yUGf(V*Nd5J0|N6Pov!Iu{Djmot4HAX#7j?l{^b?^WDG z(2Wmw9R`z${Zkz0@52x?6rfNhkWGwPD)b8D6mM~h+|k=gN6zY%<5zw6^7?_@Gi^`! z29swkO1Z*1exG;e=!fE$Ob-p23iYNAIB0pb-2kx6&`V}f)<+1t4>EViQ8chpe#Q(7 z>=FnA__pYlXxP4yemG$mJYBqEy!s9?X1mzDLq*tl0`|Vso7&4VJe*iHXGqSBNm_dw zHLOLANwc{zOx|_jyM{l#1CD1=-C%}4_rlI%ha|*_2^VgD*$~`U0|t)WPPeQ9rt#Q3 zks4=3tT?S>)$IL6fc(1-;%d{k(luKQlqtP6F{AV*TzQedl9j{dy7-gzz3sFV6m(Hb z^igjU=)>nnfFmsB=$(TcVxA*OuPSThuG2B)qd~IMWd%p*258{I-!9EKYp$ z347M&J*3M)cJSpBTac#YjSdh1FEe?I38$>#VW;Wp$#VSMSP2i`(SUl1lv5+TKw+3jr`kk7;_I5SyQs1) zy#_H8@%_MbN{DHf`Jf)sCT-@~r!)Cx+EdiMa5nwHKBrz_bKteikJD));6*jy;Muoq zre9%E4lvI3^Xr;E3QribQm*HJz4cZvITA=7;Vz)tb z?|2qPS_#vUT%dM6{#Z@*2N6aZEUjQb4G({5UWGk4KS%LuTdM-7e1U!93b7&q=qtH~ z+=dpb6Qm23(%u-YbL~eFizNGed`Zo;8ssQrpJg$Y(aTOZTZtkZfQ#uAeH}EqtHtF< z*_=PQAAj6r9j?SZPV-j52&BsGDuya6;reIO#uIwICLS6hLhYH;zhr|Gf__$4=sv*? z$e|#I$a7Xt4mkl0w)1I|+T?ue=73H7zeun*F_!^f)8lzjw#pr9)B-TUY}YJD3=z&! zlzzdiEtQtkJt%tdeghr9i02HqGJ93w_XL*rF3wP?^9Y%Ah4Am^*j(t2Kf)Hb&*-eM(eSoK&9-$9ZI96rK3#5PX3Pe(C44IM`rq#cBoz%OlJN-q(08kmAsq z2gLJop;U5`=7rh_2NuS?e&|a<dDkv2_o#}TV0{MRu`L}nq%L22QY zjWs|3h_3nL^<5V;IlaUr%&Wx{K0zL_G^yhe#qQd3k%P-J#4jsq`UXL#A*%$9u@eIRkh^v)m%TOxewvRxv1!^f4=VDK3KH|5T8gKs-8jxXXBPQIZ;3UZBmjf;N`-@ zAIZCf3vKfM@r&e}0PZHQa-3Cy)djb1rE5@E{mA53AKN$DK#zgdX6?JQE~14)_mXdb z0Zhnn{UJF5N-lt8aFLQ?!}*aPJ*i*w(yD)onp(F0L$hyxgjR4^Rmv;6KvRw|7X_UI zctD)0ylsO=Qjb!!v^QO%oZ=R3pfPJlh({Q8p3h{+_lcs*?S^l7ipxzhn}ryh5!aHn zRgt@D1Y<{5s%j}MD%46(u(FgcFQO_-E-uuvk|8tezu3gOr<+Q+xp?(VhF=ph*lp~k zs_{r(^`1vc&-lea6JL>dbdD*9Q{dSJK;xBuKu8pzQ;Rp*(@B>BrY^uA>lUlsH2ZNp z`|IfpBk6HbS~ZXFq(NRLJxc|}?J5(jux)u(+Ca~b5Hlb7w*2?RO#6coudeC^H+t{z zApuhv^8q7a5Z5~o>MnH0xi#=YCn?lYC;)xAZNx(H29xd@e6L=S`sTI`MMd!hP+9s& z1gz5Uqv{$lb5`|C1yz2>l?SgMV3nA-;5!XQSLU4bckaO|i&{-4#rs|z^{|HWvCYRS zVER-yJLiQ^*C92T>~zw*)FCSQ#Y;VEe!QRvoaN!=f(BX|=BTCi-xHg~mI*ldDm0vE z_?h;$j0wV`ffllJBQq!hmnhu^$Sv_NF|h~;RlrB>gjStxFF{$|w#CGsJCmJWo*Oq- zaSNT`=3aA)A>tN@AEuJutb?(^KxubgFgBQI+}IBB3gP&SQ`+)sanQX4N3_mzT%9h= z0+8@Z5G5Y|=-gW|{N!DT9{rGfzf)x#hEI86!$c7ZHpZgnLh~OEDD9)HYE{+~;-%(F*N^)|UyJE*5 zTYBHYspo&Wu=z@^{7L-M5n6Gi)18?(71xvExT9`Qn-Mof#&_Z16&qZN48sKfd*Fh~ zr3QWkbA}U^>f?Z1Y;SZ702b&t)y~xbst!3dorESDaYuxy=^f!O)bc{35qnjgCt+&f zLuQ#Ed1wWGJLotBLa@nkb>#Dn?M8q@yHoPY+WrHGVC0eqKOj^sRR|Zhg~n4ql?&ch zI<*bnj!$zATMd^akf4+e9zwoooOfibIUE!r!Vito%rLR96SfuypuYEUBC9ykgMAPv zFh+@t#umgQ#g@PN)@0e!hh~exSKt>k>n(P>4bS@L$bZ`O&$PXsVHfrGH8Y)`J=s;` z7STzV=6=jox|knjcL23z$OmU^+NV@06FpTt8i(t{sdE{b6LEz9{4U19{8!Jp;d>#A zBbGJffv`?rl!kZ$vY(&T0!qMayHZ%O5H}DJRkt4!<6Zp2a?TaoXCv@PLtXeYDU@G8 zbDszoKM*-RgUs^6-W6@s3ucSGlR{LmttE@nnDAJRdms*v(|H4l0IYrU^D@79|N zA|-P>2FG9k6L#d@oxT8(**fqJ=%tgJGXlm7;rusnvwjIXsk3+VGWEwjN#Y;LA29sj z5E?3b+(W$iXe7ZNR3=3H&=*c+LLgF92|ux(X1+J5${?l;ld7n3EhxFh2~*m(%TjLf zhj@wK^?ZeE|N;>%+IeK~qU(!NQe$WkBj%F@~7XFIT) zrjIlAZ<(Q_PeSAF3a$eA5EU2w$M$h8v^i9D-swD~6&;C{&0|N|HbT$EVDS^aW2RZk z)eKTqx=y~9R#(q@YL(IweZx_LHN81lr@^OM`TmEv%^y{(LTvEUokDT7 z1+#beHQJ^Ev=4+yomO+MFAB43qonW1?+tbvx^80PB2mkbP2^U_f+@#2d$K*=cLJ_& z25M9yaIU@n*H9UmJBU_jdI5x;3je%5YkXJ8lmC~OO~u{(L%q78f++KIr)yM@{2&_!QTi8G%v=7Eg1JU4s2552BMZ?s1 z=S~2Rek5s)u`HH3W1m4nA2=Fls?uCwBrN^Xo+j@|#{_lu2+U+Yi;Q%zeZN~K0)jf)BxNn?B=n;GLKXT1lgmYZ8XhAZRjuJ^xu4wcRQZ6r0+5ST3R^F~ zo-=4xdc*3p@wZ~**pB7;IJ&RF*Eb>L^+AA5h_OBs3zxb%zkf5)$P_7ab#}9f(ezS- z<{3HpKvT`%q(kdZ%LVH*iIA1$ex<;@BTbL!zH?qmTxEVN&i6jg*3dt$BF>vMT~NWA5FNkXu;*!!zB zc_^9RN;KF$y!5qIr&bBr8`GJSX=+*t)wtD`sROS5k|it!dk_a%9#R7ntz~;?5H-wK zY@OA6aGn4BTAfw9cyKrSd~i1hpx^{nuaE@RuR(1BL*~%@E4Sd?Dz`}?HFtpM5PL^u z1Mj)W2d)hc^CPF_HF7GCsI09vtsaG(O4*LyYSjn&+4n!X!Yw_eK5HCKpWpW?A_Gb7 z3?G&zkdG>zMM*a+<94xwuj5rSk^q$xp#EwFNP;=@qw#Fmi&2yS*9}YmnANV47im=L z-vLeCC<$QCL)6hx%wmV@+zWsLBq=QSO&tFYjIs8!U_U!j0dM7O<0Bug@{fhTm|Kj6 z5+c=+!#ZYD2Nk?gY?}`OYj*4#-RWyiQZZ&y&p;Du)uyIvNlmnt^M`OVDUYaPg)%b} z$)?ka5tAjah5Xw4PeRQ;K2ymP+WB<>aOZ`z#^_HE$XEG^x;M;fP1wlml8qzoJFHwEh=52pG7T+I<|Vwh_)k0psi z+{9T~0-O)R*?{wRFZ@xUs;c0mVW--86L_`s^~WpJJbeme(j~DDCY8L9<>S|H&oGY< z-tv9Chp@qn{D-jNjB>z0fuU4f$sh;4BBD37g@B5ouE-0LhHd#vCaJ?3)8c!ACZMTn7! z*Fr<|z~O_KeMgv%PTTG$psLYs;(%!1KAqMjk=Ls@Ta%E5CckvYi{GtV=b<&Kz}Q|HVqo73K=$oh zk5%ql0}A#EbAuDzh`g-{E&VO{Mex5f#yXRd1+RZ&F4_(vBwP$5dF*%)FNk416V*`n(db{&)##vcYosb3P0#}0 z=3z*#+pRbHw^hq10@zYQ^B}R*WGI#vR0S-w>Yy$}dbR10G@y!B4}giDGqCckke_5@f?N*tAnna zvvq@vuHpjZ)w|^YSOm;r?rA*^w;(*Gs2_rY=F%7_uNW?lpu07oSEkFW)ElpUV+yO>uVrIPRmXi zK8m2Eo%5zK&T#LQ*bqF*A_nF~3&YQS>Hwj}dNI!Z1A%(meLQ@f6EcyWlI-20Co+6K zX^3r`1L_`S)8{?RIeG^#CkqU(pz}IMdlf|=*a-SG&H|@<7x!;o+jImRlFkL8FCJ(5 zK8e#D-eq#HuN(kLFT41b(oWyiiI#g?J?IAs(b5gm*jTSu_$&ePEbp#I$8Kfr8^HbT z$k7`V!_L%;$EzMz+i%QPeR99~ft>sMk~fz6JN_(ziz0rzgxFsuOD87#f%txsC!wx> zg9EW%9z9X`xAQ;%y>tc-PiBDP$;ctsWswm6+*@vnTlhP|*n`Zx&C*+KO3!4h%tKHL z{Rt5Q!QE}5o?k>y!pQFj_28TuPrxgdCqGRFZ^^?-SEDv+ZAQ+_iPd)q>(1hvwq85d z^FGF_n5Va(Sx@0Zi>u$73_(12%bmN)5)E;$dzTK0)kZXg{m#PMhpf0WXEtPzFx;2f zi`Y4f%`mpGzsF`2%Nusa@}j-fnun0F^T_b?@lpmmdyRdEfymczldKpW1^~hh%u3kb zL0?XS7#;Ryi7DDT46@6?$eEDU!t3>ytk=l;I}AFVZb-{BIilsc!M@qAe-hwBc(M2Q zNz8@DWXZ~!Vg~e6s5CYnV}FaqsHMhIp}40Nth$MC-ngNiGf6rOhQgY(Ug6_f+cuqK58{ji?cA(7iwVRpc1K#m4kNTrcAWoT(Z^ zE`Do{huqzyH&f4_Q?k<`lCfi~d1RRE8xX(RCs&7oAclD3uLUif3DN)BcPylxBJ@`- zIA7ZU18;hF7@H9qvO^p|6{B&Hts3zeUTquf7|_N+iub!d(20VPumSQ>n8e(VITt=r z$ic(CYJF)}*(i51jEIWw(BEp)O4k;*qo{(3km{I>v!?|_-6!U@WM#IMGn_{%`{COe z=P;v+*ndx$l}@!l6x_pQ0V9~HBn$NfcbVmP2xJ6Knf{9bgSo6OgV^A~qF^%2es?k* z5q6>hiZM0k2A}iNWdH$l*tO~VNS`St=Pd;SKnPcuxIix6pa#G$kE!8~;UEXx$o|)n zTA+%-#98{mJyG$DfrD!l@M$(}CnwNU+k=9vMP?jvYb5+!WKB*_2KF^rEZ*x&VUo#0 zWXeVb6fjf*AZLAytOc+$tTZM5N|mBaoo_ zIu%^L01A?LwmQNA4LSo96$(?HTLsp$!S90O>d9?m)vRfOsRO@M*NaMowC7qi!7IuY4&JO;Rz6sao`rsp~!sMkbYoh|!4Jb<9haBt6_N#)0B2+jubIRhWC1iUzk@F3aK&ldQ_kXaLmsR!U#XH4XOdM7dNh27D|q zS{2DD4tKGs>!7uQ$yAI}c~}VHb6tYkMfm8DN=(S%&$g?~aIF*#WMvAQiR|)*7&z_# z-#tMiMu>Wt?Z9PBm4TB3vwTYohj>JZRfA!OfV);SN4CBop6t_bSaPLZg~nx3BT#=) zVKE4ENPs4CVu5a$0oM8&Vx;7^yf8>=6f;_EmO_dX|I!97#M-I>>iY!juLIf#HcZbZZTOmG!3wlW8-*Q<#J|ngr8>=V_&#>qJ|_ zvH+|YKY`RD8%-MNWR`l#&ZB4=oTsF#!8pg4Y+ygc#$5VBzan zh@bEuSUnaordNhf^`JOo2KHC`OP13VFo2t0u+FFZcZJZ+e5ue51#Uz!eg`|tshAfP zm&jg;FJmSod}pYvGgqVV)K^8niQS(+Ab=h^ za{6h-Dk4J;Q3w&fU4}jNqT(I_#G99b+`EgiE36+lxN*JIU5%dyDkA zY&xxfw`%grr4rTlkYsR;4a7FN9ri)?san^QPu=0WE9mD#b5& ziBR4*oXugczrK0kVQpjFBC4m@8kMe8id}E$>Nt%E$wigxKb$K;jy$!}gnIIJu-AR6 zGTQ(Rf3^DT(4Icyw{tjn()Pv`ILUY*@Z$s+=r zyiLLd5J9c6QvY6E9(`|Xm;jYa4MH3kfmP5}qW68Kk<}6;8CCVL>S4(@`_ESkjW4ms4e|j2!|IQToPO2Y@)H2Wz$UDTAGF zR~xLtHmiPuQBe)ACE`XbDK$;^{M=VqIfu0^a%<14N*Gnoh8Hch@&7ilyofEf)(-b<@)M1b z?BtF@R$Q58Y-DNj0_bYnTEJ-);{J{=b^Do@$@M{ zF1a{qWP%kP=O^}zj&sP^nz$+B0j8j+6iJ*yJu?HX&6vk4 z6<|gPxhCwe&=?m6bxbR`g>vhilGr#ZlzHWE*7`C2P6@mpPyX|^nY8bkTz`F6Of=;e zaH^VTqc)snurnMN(f^U}e&rLV@?jpT;W5Z*J9pLtqm&_9>AmKRA+y5njo2l>z#o*( zc8cJWzKrtz3kWymvX|fNYbEQXK$03}ZK)K zPR4UBa%DaB9q9~D8PF@75!SN4-xk3w>!!hnf+Lp&2C$^U6zljZX&(EEF@ue!VY*sn zw84B|!&XQ%%PCVjXrFuK|ywKb5{x;T-SkSG}v@+9-E3XkNHYhy@ijiKa%N4X*%2a z929O*0HDQ52lN&uuw#Bn@?qLzhmnUImTQ?BKH&^u)^Esz9lM?#TrzV_XJ;!bQ~24q z{}XTtO2L-`qFSjIPNc;vNaDeSg$dUqyqZY-QG!eD15}3S{QDT8OIO+-n#FL3ILu|`z zhD5c_jgW7B9>(>bq4c19y@tT7>xhsN{iV|)$sF?36OI=}%!WFT6jA2o0=~f|H?UwR z)`O8FG#q1+MTso+zn{DA|880e(2~V|2fXz)%49%3sZdStKP2y#fbE1p-dyQMCD^XN- zOZFrM3Z%2c0`F5jqjm&+?5)_F-)253dmqY=XNxc9rIPfWw|b=RdgpJ1e1+Kv3nU)s z#@7Xn1XsX5T{$|3gU)tukX#c8i4_f_x{@=|ao?Dp<23jMo%iD-quP2;m`4N(03ILw zE0up9-k2mAOX4gDe6?BG@*?HZnC?IEPLbrk@%SW4_WdXo9DCBr_WdcKT?4EE_<4Q= zM^xi7G$CUabU(yL2c|mOON`MquK8IC7s4eYC)~2&Sx5XSGn$%A!odS7kECcfzw0=l zgpsO*y~(3XylPvqX*sBu)iiMm0UFxUzs?X-9p*sZk?|mc?^t8IWhHvoMN{{ryrBDK zi!2|}I@?YyD;-eW#2v2?X`=#qFNBLM@G|Ch8`y^oj%Dq`b$J_qS!*oe8+` zCV0uRyA&+Njv(deYq0aEj_P|c$@PP0*o2iQXlA+KDqa+gt4c)OcO-)O0V@qA2Kb~| ziWg4w&iVzh$)`EF%J2)5(*vv(&Ox7I4WX9s%{)aG^m-v>E@buDDf2 z4VK)b$XAUb^!Y%!OJaKG!xjv0WwFv_In<}br-px~b0OIjQ7`EG#v{v;j9lo4>a60t zEPk2Y6e3>b^SMy@rqU~?1Fpc?1c2UP`DE}bIRmo`Y7XGEq%1$wip13Hlbes^TrL&t zjbJD^JL0o{jq2ul@cDv1ZtmV|y_5f`UT9%-2KU@9a^wz9d%!cl-!QqQoFa~uC*wxD zVEx_1Pzp83EeFtsDDD9_F~hzU^BTJc~ejR?Hv(U_+8$h6rtw&Q|tO8ODB9HmTsOqoeTB6Zn7KFao?t5*hrBN|q9RGVq|DtZ2SHdc* z*G+FeS4Ob%oRAJJgT4V0Vc~uft0Yf-wt<*!{DVjn$Sg`Yfl`+IH^!tVRAF>}QVDo~ zR`2Hhcg1eF`hupy4Zy1%zQW!3D_WxghsG`_?Zse8j`42Fg~Jyz#xauFjR%$|g`I|k zyUvTrSG!FDsBYKv9Uj&VEAyJmOH3?)LJ7#D-;Ki)h0;R9IjkFo8s2pEs4&{dSQqO) zxR8#{SuLEbhXb02izT#3J?hQ(-5*a}4~%K;S?9>2>EkrB86Z1U)#!8NQnyCUn)Lip zw*-rr8IN7b?IZ}b3qj)A%xw;mB1#~(qkGx~+WLjrzpuA0>OPPD?mj_jlT6LvIoK(hMGmNhFNjSKdQ=4nG+Oaz9eB*eeNXaixZW47FaQ9a`I!B1((f=V5@{(kj)4D9_XUut z;+1Ew57FWa&!Fe8Qu%_N1%ljcKd>YLkTAP-$aO$}Y411rJIh~MKM%aG;BV+5`COV) z`$zZNZuGSa0*#B_Y?`y2M?fy|u!iJ2C1i)n;cJTgkNBlW;Hg}CJ47BhR}s(-_f){x zF@V^!GrTb|jbXd6#byTw9Hw8i=AO^7oo?R+C34!8Up^}#B z$tbNMjHcUwOQZAj+C8d;fBS=aqDcv1=mqrB<9a0*ERazF1 zZV*WUr8}1rkPsB*8@czpf_ML!-S<52JMXFa?aZ9>Jf2rH+J4>+BwD_Y2tJ-rJT}0a z7ou!Q!NC-0^}^~)(14U)T+b=#WA?RN1|g+d~YZ?{jQ z7P-ZVCbE|#v>Is@hEKi?Q3Dw`m{Py*O-`Ad6d!t|e47vc;gV=I%#ozVe0P!GV@4YZ z8-RReS%$$=)ehfgPa%ZT zqLD$fto=K-FG8~sqluLvr|2MEU!mUR0K*1L{6i`F^%&>7DG0s&b&2A$ zH-!>fcrK?b8n4;3kh~B`VI|nnS;tVyJ~)N)q)jpPXkx-GRd6SHnrFqJ&2A8__wa;si z6=L=S+#3yJ)q&*j0E->IbqLK_n*Y@{qQcv~Gw4)HkS~l1cBLqGZPmZ2jY87gFikQG zr|$xc6E1Dq@`iXWK9oJlR0|$3rxjt5xi^l=>|bWKJR|GjJg;(I_>8dL83vm}dm35bt3qwNPRCubfxdxn1$ z5y$r=8Ddc5h8Hx$+ca+GU?MJVR)eNXez&?}J z!6IZ#ijs}qzmyCHH9$3kt#@Q-qQj#b7Uti$9T0E%BPbvNUlw~6A~&xL1a;ON#}wKz z3143J8OJ>or|$6%FG@A*L9{Vm(|Ndt zE*iEk&6U5iaN_%Xs(l52Ex=pUsHJ7y->#&%!YM3pc(KcvLBy+WZHJ|%xi0PNEy+j_V?!!K*Hcfcty+JxkX5T74~}3&{Us?>U5Oi zo+~nY-=TWg#~+`YAij7-!jxofqUt#{ThVfH4t=-UCrDpf?uOQ#!>~dhXwqw1#u?7re@nUw;VYz z?$Jd654qK|=M2f7akXo>X@^{E*pZnSIT)O~-;8d7btF$3#epG3)PiJ+ZHq!nLm$uW zT@$f!7^j-Y>X#JR8jdGt5|9lIxjVu;^|27nXDaNCk(ckaf@Ik&XNxQ<5acJJD zi`Oxo8I?P>f{>A;-iEb&hNGrL4~f%BdmM;|2D0_0bhw zP@br@!7&_nW+W!0EETb?J_q0frwzXeq(s>+&0P!L(`OLh*eKGA5j z=)%w*U6m!v9j;e+!CVn;a_%11)s0K_HRg7wd z@;__|}p%$%`Vd5fDTn)Qo952n^tstWsj}`Fbg*Z&MODbOFM$5hUg)+i!88K=bN`|i? znm(`&epRSwq72gkNjO8ps{QCctF!)n^ZNE~dcYJO8d@=5a$vyIzNFL8iDX@k z@2I-uBbBK$b54Oe$>Wm79dKpV_kyY&nDEwsE4Iej_(|N?rn&mLuiL;`z<~!E&z>7p z;Mv|V>Aiw%e1T+-vM?rM&UpAP{%k;gtWo5yBed*}JN3PyY$_bezE*T-nVujuj^m?! znV$`rx1x{df1Czj>djqkOY;vF-f4)mb0b=Ck&wyj?Oa%l?;OOA@vyR5I28PK<$G6c9J6oLdbl%9 zObJVk&w*k$b5mmzw*=Xkr+tvsrcQ(Q6MIJqF3^d+D#(Ud>O@0{?Y4_aLAJ(SkQ&89 zp>QNz=l0f=VEHEnGaY43xXX-S!Vy)SELEMA8B|6K@JFXj6}x7G;bL?=MbT*>qQe++c!J0a|pT4#JWT zVnI<4Ta%^jr6jQzLsMVxn#2uMx%qWzg&`~)sx2R^>nx=>JWEeIgjY6Bl%t$XzO#8N z_O@mbzws)|mLdOqwV##x9%Ds-8;J_{l77 z*3yKpu&G;}H2bM!W!g)0Gq%{WEV;Z=UIRYHH+4-e*IFwxczrr;)TVwZ z9>y?T<#lf+YsWlTW+g7vxW~ghjdxN`nFCoHw(VS&xaR=PdbVfmc~;{Z^oe!G9>Kc{ zSsXg!(6BN057C@}&fKj3d>a4UEIKt-z$MRN@?}=i=IA(oKfJ<6qk}8kc*({k?!PGrA&q_-oA41?%*A&rb3+%y6Tcuwh5`|={4+d$E6CC^GedmdQlx^eVK}N!Y7%v z0cr<*#u5Bfq*loU4p%L&n#1j8rvZ&V;`=w5HJbBf%`FnLeN}NkKM1%kqoSr_>}KNo z_Sqo0(|f48`b&6?-m87?9$T!K`0`~qHB~CA#0GB&|1Z1RY4cLfLwQQcy#UCz(KpTS z7;snJJ*D7BG=IHc{V6{xcJ0uLUR||DLP>r8nUL4edcj*U1?^`i`@Xt#cGYH0< z)A!(UHQM7#((f8VOptRo_0!E+S^>!^FFv5KH7Ktc1dp|jmn{bM70fy=>r!CNJllm8 z{LGG>M>~thyJaOWT~#4nP~{Y2W>3|9z_`Q_>mU6%Ytc@>MW!T4s^LAajdCP)ZL`wR z@r~*09Fgrt@Ny1#sZ}~`kAUh_<5az~EZ~SXRwtR3Z?gqT1y6fi?=dxD<2l7Q(=$8$ zMMR5g&y=#ceaGN5RG2-63<}rZ<2W_$y03pq3D?{6J5}hqWpGMh$L5R@V$J1d2_g() zsnD2Pd#NIWKs*srV0?1b_;eA7cWPuowx3)K=~``N>_4dPaY zvk=zPljQzrN6UEB@6~rhl@n9e>rw(qAFnu~tTI13pLH#6kKCp_7B9cnoT*l^y2?{l z7-fHA{@&~fB{dC#D>3+^k-qip(^^Ovd7xMsvOYWP?cE!SJz2oZ53lK!2gnf1jRet) zA@vk?LvY!I%nEhLJw$>__h7-5T(u+Rt##U9A?b)sM>TnF>70Em{dZ$mrOhjeXy#$CiQ8c@^^nB6@qN`zTB%L;%BCS?Q^Kfu zrVoW>Q-D3gYOhMHH~r9EZTODvRi*(s6Bl`+{*WZ7s)Fzp~;z+(+HEZ*%_uX(UV+MvrrqbeXDm5uRkf^5{Yr}mm$%E-xYk4#Kr4 znT{EtM>xx2!pfKkrcfk@>V55r%io9>>s~B2;U`;*u8fLO#EPbLm~6e1pzElL@Q}_a zhQDjCiTfGuMllde*3)j^h1{cC*wDM$<%KR}jiX`Jm8!>XHWOQjzb)umwdsIEKn~Yp6H_=ns811-rv_i)h z(z#b1uLg|Et6#<1qJollF>K`{@n1JSh0{@SN-)WJ2i~f~F7`r-g48hR+{@~;yxLSz zk0A>FnW)lOkR!M)zIhND(B(uO>wtBECP?xmdzc9!k@V=Pad* z9$bV|Q;KV5bfuJap1P*xyZJnhJtc*bdcGWGz^50o8uKEKCKxK@2r^AN^I+U6_?sIB zJ$GK~(`%@zk-m_}A7Jkj{LD7iKuX|FZM#0B*!+$>yE>QOMag{9j5WZQBV!qjuOr4@ zfT_Yr?hqPbJ55>4URobxxsms6Uaurq!xg{I+>^6KYh_DXcOf}QI>(7`V|ZhOWuY_d zEb|OQM*|&$0`vE3JhW$p1c3M?Gsw)!4+T6YIe$^KLV?Q3tABH~E>5!k{e^al=fW*m z6l%@S;cF=8?eU5A}beMaeECEauU9T3}Oa`W;p?? zIr0l|9G+&jA7Ee~a1VskCAcfwc{WXR%opIhF1rv7F!~OtD5iV~-pP3m=bY!c0RLCo zo(v65`V!om=Nz6s&vF5NN!j-jeB$~!9B1KTGQYJ`|BOB+3c|TSB~>blKU?yboF$O6 zK!q`V;~e91gOvAA%rE^)1Ued89@sE9F6FT$dF}+0B>Rukxv(YJG}YjalFJRhE)6<~ z{>S0Bn&6-5FUf)q0zk0re^a|8>2@i#5e3kR6}YeP-_$ONdtGwkR6chaSz^1;4Zp>` zz+rR=ZlwmoSwN{TLU70unO+>?SZ097GCyd}US`FB*Z@M-{DAf>IL!c=2N!W-b^zmw zJZQFBVa33A0J!WW|386#kuuM&5M#_Z0-sm@neTL~#27?Q0PpI>j{i;3{AYs7Ak>i- z2yrB${IgU4=8Y|1rNqE>1BSXOfhIQ!V0V@HLd7p}l3uDfiN`-Kzb^o%-WRK7?F%yS zfH$x{xc}+rbGklozKnx2QtnbzWxsQ$?KR#DNu1MifdlU^5H4~FJ{EKiH$yRAfM2Eo z`i*}X+6xEaTwqK0$6w5J?fH2WqIEj3sPWmwqA}pSmg~=${@*3w<|$T;*%#;L-4q&N zZv9t}u7bwgjB_K?2IYlhF72rLoeOxGip@NSyI+D|+8uBSj{fo--m<}TA^Pu?+GuD@ zm*8Cm|3t?j;;$mB@7;pMO_v`=Z)!z^Oz?}`3l4%_R7WxJL<8bL|$0Y}rPoM)G`0#@PTVd{3 G$^QWPgI3l6 delta 38507 zcmZ5|V|b-Ow`DrE&5mumW81cE=Y%KbiP^DjcWk?3+fFCx>6tsvz4Ohlz2CR$=c;F| zT6^#MID}aG4FRPr2LTBW`i6*=gpctJ9u&NTmn5bAFZuTe1riJl%*oY?83OEocCBOm z*CGh=8xamX7#J+C0*+bp4!wIR!7Z>`zJF3fU1o%?Ta>9+ zb-2peu)j)U%4NJxdO9RTp8zB z8G$R+K7NS&89TU8`7`jFQ5EkG2dq8m&9&TEBKB(HPwk~d$*fOb_dZ97Lji@y^}(dD zUyb!PNSw$z??0BT1su-E$$`u5gPFw6R$Y(MIf`$l9{{Wj3_kVK#v+3@AWhwGGo2p_ za@!Sp;73eSL-w1*QTY0dBn|RRztPA^X~Cl{vOM*|x+%#!Q(0bB(jBY-91ClV41hNN4ha3Wt-UvEpsqD#Hsf+03eq0Q3O(;*H@ejQEl)FD7nqQIoS&%6) zkh*@#{RSjiA5a*)pG};XG!R+F2BwKm7m(Uqg4fZ64op!kc<`~}gW zkN*73{t3K@52<72dH?l82vMBw(81X;!_|syzokGxH&DN7A(U#+-_C zAGo#FRR^*Qp<$dL^~{gkc+ZSAJA|{e*mP{-tOQV_JB;jlvg46hw=uv(W^T1^15DF} z_9^;8>JX}t6o|IL)!G#87N1NjJhNr0cAOvl75hc>7_rz$1jL&&%MMi3NapHMw(#@7 z^~Au_fJMfVkY#+t_`ShS=zl*J$IY`8p^Rz9bk7=VWL0-7O^)ky{p=Z^Q}m*spz=_QI88LhYI=X_HHz)(tDt8__Wcn}kB1%q)#nay(OszQEpEH%!Jg)OBy zBS#LwR=<=0vNY?V~PNYQ`;z)?M+&MXqaA+>MHiLD~52PO^h03(>^FjYK{ZWI2x<5(kzNH9jwU>c^lU(7sk@!VKQ z;wY{rD@xZpbz-!cWjY6Pm62GH8$y=dt#nts@x(9>tMPK>C_tqtHmRJ+2}LvHBU^Ma zx+Q(;XmLYUosOzP@yNpfP`1bw!&N1feI|r>P8F-fQmi>7w2?8pD4;S{H@-JOp3i#C z7{&Y(yaH5}!hNG_R~?#yIit_OzN*-k5|QmD=a+Fb#g&VmKT6A7@X*+Qj@LT1c#nPd zlYDS>OW2;L&F8>eH39wS`uc~XmtC!}G&FWd#>}s+{opUs1VO_jK=xIGmhS#@9S^%w ztIbLMd`cnd;2C%alY)1~wETRqC|z9Z^kdP~xVp^5jVRP|T6;Z$f;)v$4BV(C^Lt9F zz+zLHLIUUp0Y5J=%FkfK^H5-7pwx$qcVJTS)c7-S6ZS2iItYam)(i*I(~S$lBFD>O znsesGe43tTC!4bl5SG8w-R5>lT9VWk(l?A$lyMg{xG>o;L<-%IUv$j23zj#vqx!h_ zy`xghtWEf}BNt3spDi*E$~1;N?7FGq7l51-=k@&>N!1<$TV zlTV=~?OH-Xf-8mP1)UXb7k#vSj&CFe-;^ag!qO#Ep(4!)z#AoOoKi3`gy-bc&)hjY zi3Tj=Vvn5-lrE&2X)hJ8lp`IKUscf(MeO3XlcEw1#~qYkkU!91Czy`&q^YhnVx}qi z_F{aCpM-Od>|H4$q-VjQZ-A|;C$5?g=7fBtGHr;z$wgvuW}h*}xE9B_9f=)6Bic`(iG$O7?D z_GKr$n*qVfLMJm6nT9M0Z9e%poBpaeL*qk_$QrR)X0KGGdK#yVT5fYQmPbf+ai5qx zi2Zc~Ls?Bbec&CFtJwL$;l;$#n=t!bGj>0XUVR?ZTG8Y|FoQZOST7*GzND_azzaLg`5LS6a)(WQ&TQ+S=An^xE$`wk@n%r^NlWbMCx!7S6mu#*Po;V*YL6sB3niNGf zGRlSCVYA=-^tR+yCkJnShM^%VZen?zGk$OK- zzhbzo#v8T*|K^D~gz^R|jhxA!t&AgW25Np)vC~A$gaWkz?G!BcP+J(*e387crj>DV zEgQ7gYLz1~?ix!qU4=IuPgP$ijkx{Rk5locq13WrIDx^v&IiDM3BM!+r~jk+r2nt> zGeX4smsRiKffn~zn+6eofdBhM*vD%kLP>}G2H(_zk^1dlki#v603l*849gFNHjGD6JA8-cBj?gLUf&SL&6^_e?aS( zc&M!DN7-FwtjmmJu&G`vF8be`$*CNtUS587zre4rd#qpIH7PjA7o^41MG?r*O>rMh zVPANFyw?cR<&g2L@i2r3=-nA9-}gvI$>V9E6W(MQAqx=!TQXZ?60X3UY5F92!#Ik^ z8b+N-Dh&mlw73w{p>bdRWp%e?lh)Ps4<`h<9L9#2mm1b~3|~zXYqXG(+?r-n0nnmP zax>*qY>p8KN#im`wC(4lv&(r&1ulD~3X7K4f`l~mPIoD-BpEXfJiJaEk1L}3Kmkur zrr9LCmKretP7G9AlhtTa+Nz+j%7czr^ZeUWLKakS_(;Wlxavy5Y}YYXX;ZGtWXN>p zW@!jiAUroGr)H`}Oz6#VT*s(Lo>P@rx7pclMf;YVK6PB!?GOMTKZ=-rk_vn6Ph}p6-!@S zW{KrR_o;QTeXrFdCE=^8@NbW{3t1zhY%B^5r@JLu#{A@@%EA6hJ1$O0e2YN)MKo|mY6G#x49O!97`(1Wkxf?fYftm>lE*h8$dp}| zvi3EJK3)jiYK6{vm|2t5mHN7EX8`w?MON9k1G``opNwnhake9z7gShZu;LI4_+4)_ zDe~P~G@8d9Ta3x?s{!z7nYKrm|8r9R`#x5JCtd`KBUJ!2mwy-1f()j24vHol5x*s+ zz*0z*^fqa1w&Lx%&b%skMf+gtO%$h`A41uUV4E?VbzMk?Fw44}nVR{swDfZP^RU`R z0%qy55frZiVH4{C;;1dM{vIU*p;qrMf01D_rrzzF8)G|;#xy=FiN4TQ z>abs1E(rkSLjjkFqGQI*KXX@LrSpe6lEU zGJr`N7W12)M~An=xEpWLib>Hm*YTq`phBewiz|g?Vi;lkby@X;$5-H@;Zw(Bwj}VY zVS)ZDO^*qO({4FEzML`EiG`xQy5jIRHlD8lnh4-D!{XF#V!FKfR1JxMXpG2o7-xP& z^W-M{%}StQKT3Gn{A=jlV7um*6xl|b;a7v3chk%W))9blbdP4Z>e>ELqqaI}0LN@R4;=GAs3 zW*Ec<|EOPjhEyW;;|Wv7U`{3lnjuicG+iC3hvS({gg?J1re@HX zU@Xbu=UKdfB6x6deQaRa9Es?OwWgu&z8N4Um5g9523E|Dm7_5S88?&%hmCjzC)iOhm@Z;%|RFKhL>^3uLm@l-%%f#w?a!c#6d?nr&6S zl2!PboK>1?(^uUl=Uy6JwHv$(hFtQ49Rtp83r3$FNLt-nh3VP9%@bFu9dh?lQ0+Nv zEw*~g(yAz;ju{nd94lK%pA`xycG(bX&QTck`b^dU9%XAZ+zxCsZ3=2_tChArwV>aH z%wyhKVwg7C{K{9NidGDW5NSH@>Kn8Io`{o&uVE&0dVam9bEJBDpf{=WHrvw5tW^2= z2BfCsixl}cv734Y+>lBGv?Y(VA}6bkck$%5TV!iJ>kUg^k8UUL`tVB8#Zi^@!!y_c z*p^m+n^eGMpng2r;0(by{a;ketxW`hT(rSz++*DRo=vmF7|p>I8Y^*8WUo_sglnvv z;m8n^oW1tZL?P_5{rdo@?AMe7b|^}F)}fDA^;@ufc7`|KPN(aP6^tf1%RIqL>3-f= zICUdd3KXw;Q!RYXE%#dCB$^J}H3;>(8W zx78%hpH#*xOV6Hs{at{>tNtiAJ`)ei&at+@=wKQ|2k=T;tSu9s9r(q`6fG}32^d&F z8f3_wA*#I#YW^OVXWzxh1Obg;4OEwwB6%HofvaMLj#^Y&2@?+q;q+4A8S%NR*6W|a z{O0GrAVA08zH&LDQ99Elek7I2VKOw8ZW}D|A4{$*-3ncL%_s}i6v@J*iPEK>Xdl7P z-@3&PWL!p$=SQ(oEpcv{#(`(CkF2tQ*1g*DwB*=5h#V)~PXxjMjw-)I*>TJbi5w9n7?rd^Ts_HX1Ic)Ul2+&C@ZR0v-x0N@;2=nVPIaj@ z){l%pRk-4@W13phI2&78cE`lvzNCXh9?>%L@8DM11=!MBg_&KO4G`Dw;U-)se2U(5 zf8u#tep%^{5@`jsK=`is&`$Aw$dJ5*JPWIqgesoj z4LuKKi;_ z(rkEyjyzVyZ%KyCf}@k4GgpCzC_o0Zx815rU6S7O$2?IYX;3*e@s zJwh$S>+i~oKB|8uSnbu_pnS;bl>7*l?sG!{CjWCPDK^}u!O}g=%*WyhGV`jVZETt- zJK#B^DKn$O9`zB+hfgB7x4(dd)sC@3UT4}7pWUU5t@eIqACFLf(BnAMMuCd&Xn(=% z8bE&aH|U0qFs3C{X{_e{2J-EoFOr7pO4bZJDu@Y+xMc{g`DbdFD;8YBf_{l0Ues7CuyA$Oj&XDA6 zrfYO&1lI@Ie=Ig*VQ}yIVTn!0p5Zq`B7A(r2a5bZagBrxgQ@Ec20-%fDPd)l0^~on z#cEA5dukmrWZ-7e%&#C}13a@z9leSDgoe zH>jL{1_BM~uPXri@tK)-NCDsl$n+vBxx+MqXZ>-V0adN65{Z>e^tC1L92>hgV7RU@ zh^`t>_>1_g0X0-UfA9CFQ|Oy256eO`uM{(Bne}+8U?!L3ThqO@u0+U&WLh?}Yv&(cD#w zNCl0UArE`L&lw2k>N`C}_ji+sFdV4BKYvg3T`nyQ4b$umCMMYob$xVZCgE!bZJfVH zyy)8S*BUuF8&^FzXYmqY>PMw^Ut(rtS6zEKE=xR-*wTb9Hm&(W`&suZEU0q10xpy4SrMsMhH1FIB+Fd8seDYG`c~R%KOKCbwnk zsxkSjI&M~v$~2|l!B@4(^;fMi);DgcKlPJ(>7~gN%@cZzwF2Y9@|3xCTJeR$Pc7l< zXxBnjpbSpc>v8NbyW=_0w^7@R%iFq;Mho=sAHo6h$h!UAAxf9^`d z+AzE0yfC|Cw&0O>1)*--D1LV?(yso*pKSD8Lfcv?oBsGNq%plI`azcwS; z=@xqc{_8M;?oUVjn&}(DC1)EXwQ3m7^S*SP42p}cQfy45bZ`h$!vfl&DYec_cNhVk z+@%NVK1A4RN_4eyc2jF?_4!C^rIPBT%aor|k+3Zn%bu*AnRNo?pR$yxO>`NGV4c6Gc&O>GUc<@h09W%K;N~{%&9+LX^VQe=;8}0d=X1NrO^078m%v32j)k}6AKlj zP@`t3jo(ZXqzGydNWYmfPYe;ON3XIfbqC`&px{J)YLjgbEr&G?oW$BWGw$YUtL^1# zucF@!{Z8|xUf~vhA!=uuyJk!t&=#Bru#WjP?BdeBSEbBxXDl1xf1>Yg*RlMenR#d8 z0!~al<$T!jr4Ns&XoPqSSznXxYoF_=h;0XX<0SL^$m&bbbwPF57jutJ5J0F5IMYG! zt%qL)IaZw!ijG4eocTlWK{#-G|Avs0&f@?!NwMZrCV<>nqIE`ofdB($5n6QRdd+@12kM3~AEekW!Nk4v5udjvSDTcVll6@oZM}f*Wv_9NG z?N_XKl2YLo(b!2k!FH#JK>!@-NUGX(`Zq#7=HU?${@$-M5SQgl?B!*YRTRqhaak^=`_?)U@I0lQi*0}om${*5vBt=aqf(Fcbe z#1rZ>vlziB8}$%&E^3KT2&nP7ht#Xn)GADSX?-eg=+Rz0edy}eZP0sw-{SJL>))l! z;uIdlq)3sK;MVB#z#W7%xsJ>?u`%Ofdw*J+S0hAAj$9ee-&T-#CB~vxzr1coQOzQm z4DJ3*y4IQtbcy_1={%>n(=*k}CMt9N9qEgEsK1HyP53|Ak7B5|u;icYdi=+L0{^!R z4En>y2XIhYRK^_r>qW4&f`vyHnIJE|4$+8|L|P6v6M;*eWz5pAg|jl1b&c)BUw9Yi z^tkvciXJ|M69^`pa<|z!^-T_XGWj}Z!!7Wn;VQqcFAySQI5{5Dl`naWT856sLstr( zdwD%JIoc)VAj4uVhjG?boUjcSX!Lq7$7G;Z3-H}!$BQi!&1kfBTjewWc4Uzg3X}7qH6OJkZMd zaZockpFD9C-*Vn`%`ofeZE0Q9%QNjCJ+wDv)pWMOLl=GAM~yN{?&;CA-^ugjTzVetMN!{DLniV~bB=6Il*7Kh9#KBpovc zpqqV09mfeI>lCvMn-V!zx!)WB^Fzs%$th@>|3zpe6T(c(P_)Av8$LITT6u)f1&9o= zd*J9qY2E6d|4oQ=;?jRImll>|g_+Ox%lHeXunU(){zmjqAneQds0H{Smm|v%tqe7- z=)Fa3#IB!7hzwLI;Xy<}KEJDcYr(i@Jf1$13YHOyO3J~-->bz`{y!m*f6fnLf3f^3 z5m9T$79~!$;ILjJUYjW}&mzL|2A~#k2}ra=(Aj_BhjGNnjOxhmxRk zA{YhfaWMjhdU(*sD&|<|yjInHV=KnY^uy!fpg?q(^7J(2k!G4AD*Yb7usx3K&DvCk z4fC-yLKWsEs5;K6kokIer4Hxm-{&M#=weHLHXR+A#HYyme|{#OT1>Wf^CO}>^xqo4 z-NB2QFIT8E%ABoPb5@mlk5nPuBc>3Ba?|N+FFXTs(K4CD-p5<5c%LVbae8&v4~U0b zJT|z7Z9}_iW!l4kF}U?)o*Jkre6`vpQ+5X+4l4IPM)w_uL$_UoH&Qcn^>TdWkWNV$ zP;Furr|~=k%}7uw;wk+4a15MBq!usB;u@YZoc>^`PAbab9%oU;xv!qtRFsoOr2rQ* z7Uuv7YWR+(+Wp-?J#FRsauc{oM7Q9~>h4?l21~eA`nJlz43qkFy~-`i3_jwMz@GA8 z-7;EU>*r&oH8tQkprR(E3(>6KEic<))@8~Sr85T(-~SxHZkf3I4zli6a`I!+T%)t1 zbE#r)lSO`YdU|?}kyvn~Ck3PH$>{pV#SYN4UE=9lYtO=zTrgWANwRJNMK$pkA`U{kI=|Fsc+sK+Ogcl@ zbC*y<&{CXI|aJt@rC+3Qf?I2 zu#fS|OaUH6B@}d1?Bc11Y7Y_x&0J5-_&-cf zU4Onmd{PJT3YPyD~_mrJIlflb}Iso3fJB89d%?dyVC)h0gT7b5nA1(XV&eriP53Q z4L}$~=2>+wuRx1+f}_Q1R14B$Tvw|ov(tmtD{+-t0b#kl)DPaS`3C0z#x*#HlMZ?y z%O;S8Toh6N$H))tP*DL6mLNn{=2S!m<0O+qz-AeLt(J!;o`pw6*DZ`I>SzW>@Hka#njH@#l%=*o3gh?SK(jfDB^nE~B3%KpL$>-%><& zDAk-^TDWr*XHlGGR#4I^@Kj~CNylO=<)n28{TUWY0^zroP%~C(pFf~OPaquw5_@MQEtG9khAGF1NjU)*b)wM)SkVKWU zd=?CgXF`=786I_FvO;le`G+LEcj|p5_<9Z#vFJKKQTz_urhO+NxA>rV6)C>s1TfM7 z86+fauG$`6!DXp_<|uVaZi#`eD`GeSE_vjSiT^~TAEL-!U_|wV^PkefO2nlx<)5_h zhWdB0W&|+_L4%k?2ms+02v`Mlx<9JtRLyC>hozuOVaTf*pE&tO)%kHl1_Qv6~1b@WUY zg-YlhD9!VHF9rCqt}cifr=>LHB5;*D!tWQMNzUM91+Re=gVughU(%S8(`RTr_KA>H z(C5f)fYw@!d;u_Bgm)PIpxyR;xg=1Rt@C5-GjZ5(ZI;*S^6?o93Qh^8WU%v|s$U10 zNkD2YBQbE-i~Sio??uB9L~T4M4puS8UFdtT)c%}Ba0irVOECbGE|yF)&OeprC|wxZ z@QB4{fsVh;>)5q_dXcgO zp!=Z+VX*>%dJTby!rtK0-tbEMsZacx@^!V-qH{d-?p#68H7&aBABZKKOYkVN0+0h; zp?KWr8KCJ~-mmXUWRslo4?>3>@#rMK(3K>@()bn3L>IckH_*lzH%SvPIw)iJn3ku= zBK!_34uch`;}o8;pf9R@ePc%O5=M0>yG6M;^*$gS;sZ}k?fy!D)FVW7M?fw~oQ(q5 zDF)2er4a3h`M(0>=X*n7(1ao)l5$5B8qHE}q-ehl9x6zCcP5n5{)}w6`A^6iD+Fpl z{)24$KNFJezfH*OQ#3%T+K$tLGUk^eEhd6n(8dxk78*A$!Ez5?EET$f{Fr6P`rtOx zTs_m#%BH8}Uuq-&`5~CUV1H>2IvBIJzKdivpGfsRT5JD969C5bU6 zjB=fOo0^P@h9>&$$uRrMjB#X*LN*b^>JQk?g0A=8%y%nMOm_ipr3(na0b%Tk#XAlg z$udJ}nr<9AcMV~5H0qd}Vt0*I9Fx=gNl#{FGpp*MF|XW$8{RErHZ<2_ehQB#b)N|3 ztVm{vbaE`BfY|OI=qm(0>~}Iey@_UJB(zHL{L>hs+X&3x@d`$Cj}YVQ(Z?{e!>I~# zUbWowr)=2DuJ!>gmhC!Xq=^y1-Kc+jw*};GXcKA22zVRo<<@K%j(t|Ar~KFl@V#}UD>yNP6pjH(Wi<0-e`P^732&EC68cin7;lBx{D)%;1YJ@ zlcB_1W2ORYtqK~KRgRCMv&TqA*22r`)EM`VczeR1)|GEc`hlLc))mf)icx!@DDRJx zokP9ZrM?<%)>}uvAxm2n)>uq?qlA#(#93-KjhU|M+nDa#=p7W{qQf~NJfP5;J$9Sz zP@Tc0Wq*LrwZVwQeDoLmKk?!`t&IfYlMI7PB``wZcHBH=ZW@)$2mgQiWl@U+VX)D` z!0c)NIgI}oQP7~DGOz#}WBuWzFWIb2ZeQP4i}gl9WBWabi!|2O`XeUlFC{Mx4-Jpy)n%nRBEM(UAf0=4V!pcu+b@6?XWwcAcE0s%C^ECq z{2lFAx!XHC(%-T@rMFikq1A!|1R|eT)j<;?^1Bm%!v1;x%Td;4!qqTLt(aFzsZreV z<)I?8Ztu^1wLZ?}S1gIVc!R<}lt$CIm3Re~lJ6Fn9!cPRu`9*Oqwf9#xfZchW*#ZK z7=4%x=`NLcbvyv7a;l$@ImL&0)mc%pN-;Mn{sPRPwcT2ye_YT%FJA`_^7F`h^)s_MJhh+VzK_HE9I?2=3zR#uLRw)Y^qV^G84OoTPIV~ zAtGm1&3KM~bsBzOPQ|!BXHHpb_0yz($qRTNgL)s1O(Q^CiXCbao$yHd+#7PD+7hpB zT(yru&69DpK|`~AUMG-O&*y~D;M}5w>12Ygk3$(FFM{K|QFrC_NT8)%6GRoPLK2nH zV6kT`;5Y(xpy@>^Ixnq8h8^9^9CLjNKN1pUEf4Yt8J`SsX%a%`CcjfAbC1eYprEPm zSbUqokq7VyHwvO};Wgl_LYld-ucW|I$t$e5jk+n-w~Da*ws;2@Q4ymdK3RFTHK^Xw zEoAg?fMd6u9pSXWj%~4=fgj$FD!q1CvXf$2ko_h%-D*8Gm9=VaHu24aKa`c-Y)2vF zBQ|P!lVwXUgtcn5y2@y)y``bnWO#+s<6@;odjmiNTYZjbh+ciI7&frX+O)N)(LHSt}L6Ys1m{v$pv7E>HpM64I9_sRn8 zjP`(qs9vZ7X_^Ml?Yl8UaUee^Ph2W8 zxy(Pjv$d(Bx=k()(kjg!-`>fl6*8uVQvsRsunqB}n3u^kQik5MC1ZSUoh(BySyE&6 zK{Xo1iGNUa?XKGRIZ;xP0P`eepPjrW)&W2)FBtkgE0*I(8RvGu{>GKe5&9gv2;`w5mYr_1);<+JN;ot;E322g}0TQJ8qOKq}WsB&D+n^#36>Zb4r6WgEoKrbj2*H*=RbD&1s8;G?0ak6Gz zy&OyFHj<|?;W0eLbpe~q4rMb@13#SF+p#fCTsTD8@665pl$9hd|7mFQB9WQMJDsJe zKYtw-Eun>!>D>L@Q=2E3cE9?N!v-K}NuzMoZSo!#a2>zP)W2je+$nkA%n+*hgKK9R zk^95zD3ATIXK$cvTp|mSb6v9gIu?lQj3B!J$ruA1w2Z+5b7Z{&S2Zl`<-2l+)a$7M ziDGW+#M~`qn&0%ZM`c&24z|^F)hH0ngozL^wrDPSI-G~hb_c^iGSR5z=>RSrlXMA7 zRgCyc)G{kz^mM1Z{eS0VvO_J(0VRV~4d;2gERmgOG;*vEBixjAk}z47qHdYLX9r|o zD9m4LBiNCLj~zhERI0inZbs`NZUzw`ZB|R}^k0dW2Q$vVjqta}Q85CWqiuHm+Le?A zFfWml`yFaep19~q<)j9#tZ0;fZV{v423g7) z7ZStV5$GZ|S$l5P2@FKnYN|Kg_XZe`fR`!lq+P|MiE>A5Vod4uutbzG2PMeE1C?xI zy`)-ng--acsrm}u%`3}|y2B3b;To~*S{)^ou`c=0`s3&J5)9aJcmUTpRo{=@X4r5& zjS<+ZPR&~OLp|3XQf?ZlO&Tp+SCIckV)l`(m}CDHaFebL@1BT~?$0Lla3g8kq?e9% z$FJh(I2^Va4}&QVpW2Yc2pw!B0qPXH8|CR-;3lOPb)0)Wd*hb92Y7-Gul(M60jh&VcBY^UTxfAc$X9iUs%{Mz99Ko0y6FA=?J zG^RjTz=YA$iz%|{7P*&9W@qG55I~EijP?Se6AiP|S*hc_V%M%7mH`Fm5^V0-Q;}8r zOHE`M;w1+JhZ*Ok$#A2U=WFAQ!;XhU8HX8(1RAh`+BtU>&yAfm?3KN2##e)@hc05z z^b%BQ_J;m%faBW9^MMq<;nJmY*Ne19Rk6H8>a!(Mvna}!WYQ?0ztAj!>QI#7!eErw zi&v}h$|@ii5hhIORx+PmfPv`IoWxPcN_Z0r%jm?1jj(>!|1mv3W1I2`9ww;Yw@~{; zh^$D_ob^%@WSOXg%FWi~{IA3cX3gpr(BIy}C0Ha2aEY#6=pSyLr7IfeEhv5z_t4&j z)c9F>G1?`Z-O(6;YcVm0(o{f_U8dKCg}f4Cp-6M|;DUEdIV&od&KGhg>83UCUfb_G ziO~=k%Sh`%uZ!Rb>DOA3?#z(npMsUzo)Sv1?Dw^QZOoG=kthI%zJ%gBXXMyBve8x| zmTP7R==Rgwj9M;C_FYBy41+)6z~Ji4xJ?((Gw8F6b>~u3Z0&WLA{^o8yTAzfM`~GJ zOQFBTK?92$Cs+02i2ZPVXz}8*-;c(KCz;@6eqQc3#z>VEm z7G6{B?kL7eO(Tn=l&bD>-kpd5lpgDa3jcR&Jh>jKfigTBR(5~$Chj%)2LlRjilaDL zQ0dpY$e1;PDhvv$=@4EiYd*Xf1K?rPzeavTIzdN*MhByNP z<#=B)9x#idJg*K%+{1VH-Q0Gm=y65&r3GPluo}S^`fjya25dIZlgt&HR zvLWL0}8&r{mJ*@R8KW8EoWRto7;W*l{B~Z;(pdQ2@;@ z!T`qYqe-)ITX(Hwcu3zshOU#vuZ@_7uA_#aw)%3M1J9zLBnR187hxj-t|Vm;Jv=tt ziewhQ+tPLwTw@>?+==zF)5E*O{jbD28^*A6qe=Z9&+GwmA>^bm{qmHqC!BlxG zkWKWkd!@w19bYjf!R@=MJ1Bo>Nsxx@i9_{9Bv82Yfkx3Un1Q15iM9!%S7>UiplgIy zN61P_j=%e8tah0}cDkUuvXO)mQ(aekCB{`ke>(<#S*iL7=A);4Gj0G7By7W^(XU|J zSvju<(n=}Q*Zll`yg>J*>WQ^_o=N5*Rh);ev+V7Vcgg>?FT_yFlw4ce)Qhqhu^@+b zwvse$zv*RfX~C>mx8@`f8C^!L(*G_!Cddlzh<` z!_0x5cm!J@4&iQfE!qfhK-Mic@lubJUj#KePe*P%;oUq=Yn^WDE=|jKByXQi6=s3q zDNS9t5YE&Ajx(tcIc_*~r1BLA&40xEI5yd?zCFZ!D5g&f_{DjTR|^t8@Z|*(xVdJe z(LIw4Tb~~dqBsk0bg|(5Yxg7+j8$35k(@^KOYK~9$M?z(fw=>qx<{F@28zcE*tSgT zKDq4(SgA*A(VmgI`k&su+pL$ZP4beQAL?8lj8!$#W(E*mjU;5cU>uSQgygeumreY6 zrRAI+HXCx5r?XoGILz#Fcl4E8a2P5_vG06B64xExpm^ig`() zLQ^ySK)asUKRX(aCh)ct&B}vsJm}fST`&MPmu6{D2TIIoOdvz)P1=$#9i!J0`UhdezjGBY<=>jYM`=krtc@yLuAPS2 zm?Nr*iq4@YYxsROsnIZw(0&!`UEPoPS4z+hQqH?GcKFrcVenC5|K#Wk^hdZA$q?^m zINcI`12g$fau1B|o~)ubxX-s9l#^q+e`9N~9)o~tRWAA~e>!}IE2@g5qFl{GjbEAp zs7RcKBN3)Hgi{NtraCp?Mxzub^? zhEC4n^-0287m`6y>9{Wa$n>btEcg|3LubIFT=$6b3<&3r+dEeWHL>iD{{F-?Z8L^j zo6o2G?!gHu{_5weX0eKd>qFS0=-E?ZQk!br zXQCVI-3|V}3x&kF^6C(C3X6>{hH_v|cB~@beCsZM?ZP*nJq%B1F>OZ4!0r_mJ_8KoLYFxDZ*t$qj z3J$b)VCo)|5p-Gt|^Dhx;vTTD`LtBLR$jstv_+h{J| ze+$E>V_1{xzLiLf5s zZDWcjFSiU*6pF1d`sIfyp$Xt%rzpdIy}NluIkBv@tV34p;CY#^ZtKr!=3k$*KbbNA zQu;_oa8rC99LRm^Gw@0?xttpNlfQ&v6V(C^3D57>kc$&+MIz9lWMXUb`rT6i%I#LK zB1r1Koswx(n=I#Jj_eIq1;I`VP06G}d(=uFC*K*TDWM^MR%k}3zgIAOpUI>T^vU!r zNSxc9+aB9D+SHfxiFMg0GETm3H2#%+S$BVU+syBRbXI2pAUe~;pf$WZ`uwl@eG|Ms zBJ97B8ys_Th<}0KYVm&$;Gozn{0pGFb3D)=TkLDg(1Fz zn1#ww#!ky`zGz093PhJ@G9m=KPM!l!7QSBJ-Ux!&Gp2u{4dPw)M}Au!a)F>`%fn!0C-FX?o$+Hdh~?$1FX)e)g!vF;lYnft@AP z|9ag^ouHoF5=UW8f{3VETab16$pe6lINTdbe?miaaKSo8N?K4fyQZ2#%5lFsRxsyc z+5OEpUb5O!qtNX5%kzq>v%1Iw;p&2A!6`|xXQN;EhsU?kq<%Q}`Fwej#-X7>nlsOi z*kxxM(Q|j(WazrKc3G>i)6=@e>ow66skQ9W#x6Kbh=#1^+>!_Fg@pnmWjVBeZzBA6 z2XZRqVrd76z)2eLzqmTb?y#aZ4W}_1+qTWdXl&cIablZ|ZKJVm+qT`Hna;cB!_0g- zKVYA=_Ve7h_M@0*vY@_{rF9=iID~3~AOoF}Yrv|^C2{&Vw!{I<2O2I1QT;C1E7f2< zDh#x)3$rt!^Yl{N%k+%?4glg2*#+{@+8EyP?Ru{}PL>eShYbQF$FgwCIY6t@mthzG zq#UIc+q!T&I*i|R#)Q$h1onE)OmMxJ_XmCopfILK_%yw0l?F8D~?T zqokD}H7&&SyoMdwRk2!do#!!a$#tO;q=>-b4yac1A^tHgc`_%RT|P}VUUVj*YySJp zef@@tbxFc3Q<@a9g4#;lllwPBoj}e<#MMWzNb5;K~kHL z+j^=xK)~{hDakkqKAE3y9gr`1s>e5i>Hxi>1JUwqDMZFE1uLp5&TW_~Pu;@Pk_U~WYjy<>t#aB+nngZSY zzHkTA&bfEH6vz=Bvfa79%`(g>v7Rg6!_57bYSMVG;HeJVSnWmd`lhHi)c60~cFS*cm4px=AY}gzmi|A03PDFaU_%*I9qS9< zd998voS7yfuwGaS1eNi(TAf-9)hq=4H`}IlhB4wQJGV2l!da`E>Mp*QfR?{7&*ZBt zzZcTnN`Rz;N8S!8DWlHb$+gCvrx#t$FM-cbX8*!hDRB@~7QF!o7)+60$xP(NI5*?B zLMcq7hHB#QX(l?u-Ym!Q0QyL0G!ll1PM@k{C!w&MLQRN+Za)-?5(`Nyu`wPexzB2Z zo)4K2oT1|CcvKRiv>{`E{$6cqfadldB>c(r@A&IsL*%(Vp!Me19s0knwuN?uO7K4 zoW{R*OWIU&W?!ur>ag=4rOW7~zk!D`q@}By_*Ca7*C3 zv>}}&@@Al{Mln3IQ!_igZC%KaJ$*<$yHy=Q(Ei;7N@=vXz|@wc_e&X9L%2<}Oc!M! z7IKF{sukk{`mFkXiO6lP*tZp?z zadG0P&p4rtwM#dJX({88Zr4=!9ht6w+>EOa6p*`Ck10gcJHlGNKbb>34n4HX&eD6w z=$KVUW}gH~MOdj%Bs1k1fCRzH9pI1mt8qD_FU(1Q0ITq*0CuGj+J4E=Ai{Xqz`-<2 zoW2V!TCH)Ed~SBsg;}=F>{w~H1~SIJNYGI}n#fFQl5|uHban6sEPOIJ%6;PrH+eA# zE;lS)mE@~N0K#~AVO}6F>~*9uNF~ZLnopoS`sRS|IKyxE@rx1_eCu&AYLtRqRv)=) z8m&O34JB0wKz~;nLVwTtyvS>wHB|Mupc}Tk&j4Si8iy@P1^(NiHpI?eK;X@tf5|0! zn9Xi@AmJ_Pz$`5d)1yEwV0quHfpBzbnJunGCY`D~Z_yx6k(0eNeD`#&WwXi++xdBLNa^si2)5^|S1zQ{`oC>_eVRbSpJJ$OlyX;Zpb^T&^y zP90MWWmefYw3nV(L~!BUbM)9a$DnMc)UNg`eDcp9E*HYynqHf%)75M2LtOK~x34s> z8gwi+ui20^dEL!)7A5D%-HTl?mSwtEZFCmXTk+o}HkT!om3cBV!b52<>%5!6+^eqR znZ6_eZZY}FjGT1M--A4aHGNt#rqZ>f==koke>PuA;N>BDfb7peQKS-N*Dh#h>p7LptGo#Q}*!Rc$TtBX8(pY%0 zTBQ$8MPTENujAr*El@m)y&OZwMq4m*3!QJg>N&K(V) z1b|QIUfS1DQBZrf0`!6TXvrk@u`JtOZq$=IGt|UZB6Wt0*5EmcXv0mx>0WJ$0uNp% zLxOW-k~kPk2Han44nw_YB7=7{=zFX#7<@g6<*%KW;gc0JX=x$3)KuoF`T2BsihBVD zT)$U_neCTc`SiNaz0vhmDj_;>pw)p80=?&<$g8D_4ewxm6uaKu`(R+%?P`~A;Art1 zcn(~HeJU~Ec}j$}bD!H#%KCiZt@&%92rWHC?O?X%^~OEm%Zx|2t{QsH>=?9?WzaJT zueM$6xVX1ek>~FWb;t9UaP8D0@uo!jfU-!^XEE!u%IV963#9Rm2qy~^ZX+%X; zO6r?1P4_2$ZptLqy4U%MgBGj}gK=g;i8Wb$$YPv~^s|NHkCU#Wl9Ox8&pz6M(<3gJ zMdeHl+v1Fyq?5Ibv0Yh@jfun3Vf(Z}Cj)PWdW+H|`X#*cMDugq z*54)=T{uIBHe)R9Ddq~GTBkt2Dx58s%|GQ6BQ|fLpBf&eQV8ru#yBt1FpV*Sm6FyfM#E4JJUu2jCF_aCu4N7+{LgezduDy(l%RC;$^%9Z>VW!;@=f!}t|_0;5MTO=7ngg&9xU{dO(C43@3Hw$qN zDZr$dT5ZH2{xgK(T_5IxQ|X15_%q=fBDXUlo5v9dG21>Vb&t20m{{DM3@Dv zAw%}!8QM*ur|1{t+@J5h`1K=*Xs<}fP3J6nf?#U^5~&1c;jt+(d_8oiCYEN2aTfN^ zacmMy(tB)_3Q|D&=J$e!COSn6J!7dTGka128+paI^;vQ-HPo{L+=3eG43)7{(ax%; z?X&I!@>!pYBm}&5!3oTb;iwn!g*#tKeGT>+|i;fH?%_5Yry za{{Y3^1(nr{GdQU*#0M4Zti4gVw3dOn;zJ5Ru)71x{^JWwc}(P{8_G1j>7y8&m{Jd zCze-~XYgj&lh*{gk(vFt|FrGlY<%|Pkd-H+V3JGV3?6Zk%b!Q!RsD4rbzp6yDXAzM zjrZ)DyQ9bXIctZz<7Mt4*ALPGha60T8K-!!DL|mJa*#eySYp^8Dh%{tQf>lxaoB4OecL9F8-otR&0!R^%ke3bEsF_n-JxI*%J=hz@!+<#pXP6#-=QFyQa7gxq++e^eYu)*3`vsiIKqoSh!(L7}+= zns1FJ-FsfeCHxbvSaK!vLmm6p3C=~i8-$_+M(9WG=Gx@QtE>IgC&#`sPUGN_NTcqu zD`w%4uR|3@uf`AEOg+C)Qi#;?b6IpwC-q0*CBVFXdwa4+vt)6BOc_jeumdy6>U2Xc zHs-XIEV~{EBiyn1`ch)C)RU*bj$YxN@g6j0>qqN@FL>-6=ng1E^u3SMtWtFo2}WSm z&gw4h&hc_-2ek289K(pW?M5BAHil`ba=|M4i0euU*tz9M#^OJL&t3c*iqE?MbB-zivpRU?UDcRYts~5$41?&uUJy3HfInE4! z7OTT9KE4MxDoHXL#&7QlcvWih)z~3R5nG%qDN^>xtz*x#WyDO*BF?gCL;Ff+gnq;6 zfCl3m#$~$~TCc z?XxT+eJ1^G{R+Xa3=H%b*$`@UqI2-yb*hRM}70>E4H6y%^D)q7|Lx8>M_{2SGkpsmk9;c6Jy+_s6@)Q-@{MDT8kzXOC%{; zmSmUxlE~u^D=##Ee^!6i zSR%*N&UtSOtCb+X&d;^Oa1H>GAnh}22uO{UMC?@NyN zb=yhKL$34nZ~d<+XGRoYj^?i-_0k;Rar)z|hwt>W#lo+A_RC{bjL_rM@hv6IPqyc7 z-k2>QRLbxM&zkt8qSDX5lJhxSC;&Uq|6v+&*w@iV!lY_rlqGX72F zTHUi!m=b;ac(2k^@aRf-_NdR#9$H73Du)VzlBdQIatbNU zjiP6*29~Oa${tn{M)Xj$iMEP-aWvXO+eHj9KR)})$jb;&;K<*}jZG+rQ?6o8W{P8A zav$KbyW8HxZ8SJJnrAmGM0azuy|~p_?Y*-6ysc1IiffbY{pjmutP+R789He~#<4l6 zvWyW|EW>YRw^V3pfnk2%{A|BEyWK&Hwz)k$Ct6H1|Jz_u$J;L(2jFIAGU=nH!y*%hN z&ImHvOcbkYvq5z|S`@eA5&YLrk%YZpb|py)yZimX+C&Mi8&5F=%VwIG5prWl`ERe# z!km~UbnWyk+q*hqm6*Zk>&H_&(zVi?Se*X3J0bpdReABjRSKS|1nBQ>(=yEgkq?ju z^}cn&78z2h>L=M=P6eJrY|3pQ1BXIB8`U?P!m;Fu@B;EA@;<7LXG}Pq5U+5tfyVeU zCUMJvj*MTovX|QpGvw6q8QNZQLwq^n^$-uW>|SvH3N1XAYxY*a%=$a$%<1C}M1y(b z0a`6|FW>!FS+Ay+R9PD|5?&-c>3qpCJN9j?RbNr4?N)rC&5t4Y#`+#ki;0*)Tu#w~ z(B!hyy}DUKsj7JNF$SBWNy*7n{z?aWqIEyOU{*3*imqn#8ap~&oTWsfo+z6o@gfv~ z7XYp9SP&5*fl0Zv7#gmBw5TOce#~%Gj&sAQH*_YGPeh(h^dJ@H&YW1^x2%UKz-ac@ zdw5v779EfM)};W8!@|LD@5F;fxM}^%H$jm!hvT2wFcaX&Fz(Qs)08fm$<&!2XVeam zp-e!~m<82;NRbyKVtBOP)u<|o-@(k-<*jP(j#~!u$~x=*R~~xWx2{O4q@D+y{cWZ zhF*=6HWXn&EBTUTGJ#8{lPHeS5?&0b*Dhp-@|%jE)YKcop@6Gw$WAdZ6Y6NCT&tlh zMDAnfjHBHVPIR;-DAX>1&Gz)9J=85wmg_Yg9Ziue3OXyZ!};Wv&eGr14jD;JjT)n= zq9Aes_#zfwVF$+?3^J5;RRSeun{n#vT8liY19Zn}DNCK$-1$t=Kj%GYa$5lgZY~l# z(4ZjbG;&(T&iL|t3$KZ#<}=rdLl8Aj;X4A1DVOap8R7D)@?*|$ zE=JePtvUM}p08dZsf%Rc#u;p7x~;~>D}jtzj%*4kT=J8%Ks`yrNekvat8!`nCcLl&*~n8 zz0%_Rpv$PeUt#;p1Be_*yk^4wsJK(~lQ|gq(_GaeigGy?f@4>w$sF+MMT3NV#+@$r zOT1O+^f|a+-s*$i@8?13pA8w04E%*xY(L?H8|aPPcVrlxJ05m5t%ZcL=)>{LX(Gtb z#Jf5F;hiIMF=xC8Dkh+4z-X_;-*OD?+$7%NK1lO`IiL}>fSX$GGwU=a>e!P_;||n@ zQ-np_EpxFJa|p)!NOpRg$QAn6ouIIMNwoiJlArjG5pson=>yC^XbXF`7hWAfTj~&R z%KJ?CzP_1YEWe>(oxO=-c`XFv`lhLkkvIc-P2MmvO(x7iqCf$4DR-#;USF05UV0B4 z(9A+eln#y5$lk~R7rOxkuzejHOnGs;I@*X0CE-H%vk{!0K}PEj{=WjzwBNUgKwI)v zmtkUn-dYfkq%}fhHu58du#vxTB{G7p6~BZFScbp zq6eI>Q=r|K^J{<@ESR#O0wNn8Rt(2w>|j5_g{v~Bqp@A1-3y8u3^Wt{l9nSF3g=Vy z9|c;Y6%_+u5HG#YK0$>DgA=UWg#>woV-LgvD!~8@x5cgRT7Z@f_j0!BURIUZu~AnI zynAQ<)fV}*L5}URu`<*w?$S!Z4ncyF`X}F#0Xj9J7X)CUyBrfDtsEn*9Pp3CX7&dV z(^Eenyyulv7h{of@V%b*oR*PtBCj!}qBn)GBrMIvgW3bV$QCGF#U;hC_I+Bx%$^)0Tz?m3*)1s&B9JP%LTTe+C#zoXmq<{8j>5o|RE_&%Wr{QSt zP+o&SToG^#sw_pop2(`8`ptXUVPB1>ptL;(ti%V!W<-~p0xIMsb~9xhL6;M|x7F&n zUk+lbyM-5J-^)kp>9Kf$TI|UF?T5Ec#6^X%hK8XgvTLNB-_WFbZaPI;RWhy|iRJiB z0w482lRZv&W+$)Fx7=jny*x^xCPD3lr@=$-aeknk6Hf}1hJlrV`Padi05!NkNzd*_ zQd3}9)UQm4UqknOJqD4JfiH=OCui(6@&{|?V2`_pHyi?QX$&bEb`y=(T>k3#$zGCU zUR)Bn|AK*oJDq$%Xx(*#&Y(u$Kv>_2z{`T-vy*2e)SqJ2n5(FuHMvzo->7VI@Gl-+`n2zIitoIF=t>PKT)}UNa=&8)GvWoj$Bm5+#ECb4|A=T6Kip>% zvSj@V8-|BRiXj!(4Vv@#$yYUG0$*@3a~@%~lao<;iwRRu{=v>_Oq@nt{QKu#%j|AA zu~kf_|m4_HVoVyaifhEUqB`K3Q17 zLN_$8*-_Ib_1v0t*OS$+1-c2j-pZRd5@sx zT>aty8aOtHmbB6LVf=8nL^i(sh0WUrP6xm2HJjWsO6MkgH<2f{WXrlImuGa(eoX*G zQcAcwN2-Z^|H==yD|sl3g*R#s;5#hUK1F(KK~aS9&BB+AWg5<%#06jvzYW`iQgage?a#&WW)_sV#h-E@=Rlk0AV1Us@^*E#_;eu*su23Vi{;J<5XuV^#y| zHQGG0bij-cudBx5of1__YTA=j#*w-q@evoK53g#fe@NjR>}iEg)0MD#4C9ke;rM$c zj^j67oerk28^@m|XQ(B-zAtGhouO#`Oq-{$DzLLk)q<*fSJD#K&#x_jqCW+!A65swLmba1%=S%HvPn#Wb}YNAr%IBn99P8E`l1QkN zV|>JNPY@xeFG_BfI|(YCobx(QtSO%YVq+JaFmj<)X*#9hM%k&}`Ys&i{8)WN7s`M_26Cq02_@z@*V&gH}6v ziiMtE*$3^U=MPh;n*!|owH)O}E_*ogXIl1W>nuGJwPqGay&3a~VU{N_S}FNa*QE`P zTKu~m9?{EL75CHh{8hD2YAIv(nyPDfTD)3bGa^NXUFf!czxMW-Vxkg$R4r#Ge96;L&p;g!kt znoA98!V0jTc>_&^?>mw=fd@0EW^XV^f1OR{Ue1U*3|ipvBR;N4&n&=&e-T@}ka(GL zjbQVH93BtaVa`s>N+3&)8zJ%I2AyhR(e1&Vy+49E2?9{fEA6d0dO~Pz@z804`;~%4 z(9!Orya7|=Xcfw3BKa$5Ub^|5XkNtU{ukJ>%IaYrog}dG4wtZ%cJpgw>1BiX<(jEc|KBZ3_?yeYQeE@ zj_M~Wdj|B&zhFJ#UEr0{gLQAOGs9*l=Hm-uZ|lU{+Cd$CFPh~o4ibC*L0IaS?nn0L z;_PJ?iT0*7!WE)YdhmwtYVrXsi%7{t8sYi$qUJ|X!`Ve`h#dC%8;B(fQ8O{oxsSSe zp*aY%vhok{jp|h)o?nyxQ4mB5SesPS1ed!ZY7YQN9EhMh_xY*GlkFIJO{&hmRsIif z!Jl<+C~u_c!y(&D%eA9$Gt*;h&g{RoiwU)#52-lNQ}&=In@L4hT$cX0nVo9wFpR*t z=!QOC^X%9$6Sx@h?cRon5OHu{U_Xe5hGyvamF|Q{8TTq);7-p%V}|u#b#2)2o?CY z)KOe9R#lPh^oxcsJe@ZjucT2#MS^)d4Y%Xa1F*Y%#xGMKS76$MLxBFfmjA7no^AKJ zLl`V_2OmelS_BOJnuqPD?FvGf(y=0V&#z-B# zQtaZV`}{yu!seHrRuKXBldomMgrx@UXHX}a>l|d!tq4=UoR-K}a88GCF;D{3<8Or5 zhD&-DNQG=BwzAzA9TWg5xM{OJW6wK^*@H3DQiP~~17^9)d^o?|!`*dZV!ot$&m)|p`%*>b9 zG(n&8*0tiiR%o9D>LY*FuLT#xyaX(J?G#jN-BkWH{GqzIV{hi(*rBOpB#_(5dDFG? z`Tp1M=4$PW?~%#h^>u`#sehliZvf7t&QtOp*d4VH`PpxXEfg)yMIs^|i7D~t;+aTq z^dZXQWQeabILw%DlbAF%ZTxg#!lTt0`MQ7N&xIX!Z7*&5p(=}BjCY_1LQ*$J_)2}% z%7h2l_9(A?MQ@h}D{6O0ntin(xP7G{n*E6(N%*_RJ3h;Hg!>ql8STCYC*n=Q?KaUi zfI0Xc^eTu%m^>Gac-I%Ex$X!7bAAfYH_yzpgBX*!p)->$mG43iuj>YRRW0Ww)lwvGzPFlT#U3&&opkTrypi-J4-IRe1>w4Uv9UH+1VYDLYr!Y|!rB)D@sT zk#Dt^Kb7ncWOQlcAM>fWJ8L~xG*4elmgIJ!DYVNZ4dPm{l+WEqdh%&52+O?#QYfb7 z70oqVZIRaruF)0=%rLnQrZd+%M3$Ose~QRt-1Z~zVto`tqw;D^xr=pqTL>d8B4lEZ zTCL(Nnw$>%6*Lg$@?I_QqpK9Z=7JBgwZI)&%pi^$FMjBFq zN^!^08j3KvO1DH5=r$v=upGuwfz^C`P@FUtBODO;|5#pNmWe5~Kl{)CH<&7_(9`B* zJ5hG+J~la84`_3$+NtGVf$|StPy&U!hLcpUbcneJT{8!8u-)N|)UPbvBzu*x-Jy-J z-LdwP9-@7mcV&V0hT{D#=sr+8=v4M{WzB`V-me1KDG(rMHHINS;%`MDei+pd9#EqA zRqUF-wgo!Bh6L*GGeg7y2kNkXQ*S^JmSKr9D_hta41nf1A@DOWr`MkRL$2@U4hjMo z%tiaa28j1jdddDZU#Lm7jJ4!s$2)c97ZtuOabd_7XcDcKmP<|8kd_0cVPBy=v>qs| zptR@ zPHa{>so61!){1(`YI+*f`5Z>p6$i^Tg4Sbl+6@xZXY$=zc8Mv>Q)|TyD|+~nP1mXi zT8`+`+mLh{MI7@g+67nBYva9HSV6HzwlF%n+7(xrFE_CKYv~Xf)(lV8{yC4AI>K(v zh?MlCM;09_=D`4Hp*V?FB16S*7u6vQ9|-jJdjIJx#f^R|+!JN((Xnk4&lP6-Go939 z`e{>whW9uM{FoZ2T(gZon1c-Wlf++a>^bI7u2r5Bf$W&VMwT%6!A0P;@cj=BN|O2D zPz9R`ROyvJ%W}JF$+|0_S9!LEe}^Cjx9_(oE>~aVGUoxs&YQMFMhqHoz1eLB$6)TK zf&Emdq3D_Hw)~mRo_i&(reF&WM}ehb+Rkej`bZ1jWv`SVvDD(;VOQh&Xv zZlpLd^>Bf;)J(?yRG&e8nTZJ+3sZ>9zc=Phw2^q{#F|#ouvJFQQuJ(*J`x`4a}g3A_u9quFO$qCLpIk3C>Bh-VjUu-!?BBM7_9bQD% zcWlc|ZKX397PN>dxx?(BsH^?@E3jUAkQ<<4Kdq#ss08i2mQBz?Ko`nzx&H2?M<3p^ zoiA7z_&&;q#iR$Z$lESB;@QwLqTo{`xc%k^SKx9xaBWqj6Q zar<+EFoq|a$yF}Z#WzO_tvUDge!aR`d_f37AFgX?cE19UphR`ZPDeU-h8DM4BZu7< zQS7u~es2YD`1Q{V2wyPeQ;G8)oc1yIFJ%W;p|)a|&W1@uoHJjRl-_{k^b6F31{ndQ zp@STkm>Z6jT>e2M-(%Ry`-kgV36UK!6z`z<%V!Kl`M&A$MJV3MM@Kv`>B={+;U)7vb#yr&@$4 zA7Ql_2}X8=hod`o)Ed)@R`4?YU5N}(S+@-EA$TVPCx7IR8A{I(8_CBBH?0y`6efz&=_uP@f~L@_*R1 zp*xl>y6rY_%l022#XqTwwP7=mhOjb`WCa;7tuJ$LuQqlG?Y%d18H=4i_e0P8L~cfkyo&Lg&-M%u3ewR4d!b^S+A8LF0Ea$Vw;j}GWT ze=4py+b&WOgMEwU+i%AiUVQghZA@k=F2>JY+Ncd=rOuQ^rBxpIG%SIPd zl`(6zM>_hwC){<9Dh!=l#`z_V_ryM1ZM9ysn`L1JyqbFk94kh00Up=VKhcJMAS^}Y zH0ibkTq=%Pu%QR)At#r-MsdU$x;`WERcvj(O;hsyCGa&oV^wHT@P95x9mXPk=-j@M z!)OqKF?q19=c&T1W8p3WffO6I<=s5#ES4%b^fMR@HZT6@WP^k3I-Cjpn`M#oZ@KqGHREa=((jiz_Zp=|8AV}LkLyAk8b=)Xa~7XGD~GYWZLW{a!qXCAh(f*!AR>$ zz_$Tf821Sg>;L|w?OXnA%V;1V0DaPS2@Rm5y7YsRHJ#Jbb8EijY&PUu28Z=Rmy1%Q zWyX9m8@(*%!uWk+CmC4dU^=HQD2+mbt|D@RFLE^r4Mav0I8}JVzX&ANZXhn`erVp1 z&zJMgq)B4u{PNCie7~>KV#BLQn4n3Y+3wwr|MjF z3!g}t+Ql?66$ZQ$6XXh(LaE5Imf7Wdys%V)BjMk6ezh1;Su{olFfL$ zb?*{d^|y66&Ef+lJF$VdFKxVLLUez^)l0%=j(&>QCuCUN$_G7Z4oiC7j7(|A_IGZn zp0QeifDuKKS|W8_yP@n>Y6&o9UTbHw)>-bjlsXlIn=!Mk(c($3thms2EZ0b3G~8~b zbt%fVtUAF~Bf#)z^sL63*zn=Qp2Uc9bKZa=vyizTQIk;#)g^0bg8+~sAK#+4Ef^a-Oplc?aF1zO7EUxkhw6Bm%Ue` z(%&?2r(xS>{OHgr?gEgMSj=Rb)BLbfiZ25jq3pM%_S{JfXNqwj9ii(mndqn_5C zpSNYuX=oxxH_bppo>M=OvHFmL=ZqmR)AA9epCM?3qqKIqKX)LRSge~2gl_<%}gzZ$p;i#Cc;_HxbjTrd`pfYyhOU7^5eZZk!K!U^QQ< zKpl(ik+I@~N>%cwKyUc6Uj)brI=i+`{9MmFIzz)kGncoGek!ubGD%mwYi<_M*lCh2 z0gZR(GRWWvtyGOfWp;_OZO(1kzEtE|c*TkNQ9VZx^J9R`wKN6V{rSksL7DHnNw&bx z^LpWqee#%vwKkw0hA#Oq(C~MPjeM{-9rTz=diNm*r$av^ug+8Bxa)^bw( zl3L0GwmwB%^=K1s)9T?|d<@pB?#SvQEO)6jjlNhaEr3lfC;_kNf)kcpef)iAg({O)IHehaa=P9RXEfB-l8)9I9BP)U&%_lQ4Iq!wu; z^nq2e(S(ll?6!S2dogl+pq}CS4|hy0*y6?kzb|(}tmSr{nGf zSy|JJwTF`#^K&QJl=RNGFYL>EuM_D;!Hkdr9Xbq#O;oo~xE19FSGCYt6ym1+RhXk? zLu^1xI!@*ye2zxMI(@c607Gjdj5C)mbA~H&Y6PeJ!3z^1w?Rj)oZpP>u-(`&V=?g0 z2pxml1wD;OkuQ6fT@D@VDYw^l-j6wJNdBL3*pJq4F+%dQNszvQ4D6=|E)hatO*?s& zuMb?Wzbf?BT)KqRXHy_`#nY@mAcE|7aS?#-2>az%49~Wu-Hlhbpqt$d#h`A)bxi1b zUWC6SI}pfDtL^EU#LsX_w_piN*1Bnb1|*BM+i)lm8U6@6qd=&&}L_5n_E8t zgWDiJi(3&N!iDrOQxab{6p6v0xvvrCn?T+X7Tl5k$MU+akDSFxid36xYvd(Dq)nQ&>GibWCNd z)lD@R32j6_OClq0qBnP(qzo^vh>_qlb;#nzpl4mYT`_U4CWRXpZea%F`8uV7&7HG} zo)n+t&*rHp^f{myQHpvqd4}1*WWdy=#s&$d@i27pucn7fg!|@AEa^}cf|RnylUcKVn|ilT!&6uK%hbuCM;TMV`z6|o`?5vX%9j7akJVb^ z5zo4&RzV+_Yhg%W`Zs6eez0{J-LigE_3fmTo)`#vY5EA;!;Q@Q(ShekpgXq0+JLvS z>ZAX;+M46~NiowvE)D;ezz0B3>9)T`d<}#Ak_7p&)Wu=~+e&6{KD|r$ARjy{U;Jkc zI=>;Mu#YiZyt6?5t|8YvHKqy#!A~)D%Ik|n;XohjL)vd_H;vpaH9Cgb5?y6+L^_H=*IInQ*ordfi=zJh2J$ONpZzu0 z=o-5)rruDLnTwti??f&Fe;cFmVqslLlop(P zV;U1P-$6Zj}RC;=ky}QvJm4)M?;3%xvK!0Kz0^nJv=x zNjC-E{ za7&d=O)*7Gbm}?I@7dT|{BBtq25Xn0c*Gr5UALD0<}B*=B>D3*(WeNyuT{6^W2 zc=%-dW6}G>ED-j44!4YV@{lY}PY)VjZHhv_yLAdz^5*?t@qEWdvciXNlk_HXSD{rU zpaZQgMB_kboDAHwMfIkyDJ;bkySGYgMq2|M-gCQfjlsSysr9&k%90}Gy{!!9y^M40 z`RF=4Ii-lSQ3CG}J^h-#*^$g*g~c-3PDq{I&yR_$gpT1Sc;J{+mPBhh@Xd~O4ivE- zsVarjgS0}DYC6!9EL%{sW=>qMLiUs+>EZyUk{B=&GsMSJ#cK4rdc3e;H9ZK2tmfuS zZ1dEaQ-}O#yHO)(lQ@}jGF!T7r3=rk9Yy7wY&JoK8gd^)R#T`ek}{ls5BvJi9hJq% z7Q|HGMm|#ZXDEsaKQrn)nzN%xjDq9C9HS3CXDpmh1t4@I{8*Ot#MBEv$+j6lAsFA* z&;c+N1!hSvYsEb>FDw6OU$&Y8Cqhef)%Q_##jd#F8&ygl*el0Fkq!`EYYSL8m<- zATc8YMe&@wSEU6C-7ZNY0?~1BuaK5MtpTxK%+cD4DuTRyzl=Akluh2qnIz%^Cxse_ zT3QR9Y+=gz^2nLr)0Ub7>hmY3JPu?RKjc?}BEOe+gV1}{wFKJbWfHHsjC#UtMXFNH z!?z>I3$){RbggnLMEoQ2X9(Et z+^`ULCF;pFqkF>ew#WCXq=~2!>h^z0;I;fqh6C#nxv?tWV?B;X_B;ob7NS+E;E#jay;#5*)6 z?cjJ5j)GEsCP3GW6WECLd}&Q0dsLaBUKS29O{nBpWIq? zWoFOQhXdmrXx%W_=J?eNHGBnj$N;%o)4R%^M@MrL{4>hp`@cw8pc81`AJcU()#u$m zv# zZ;T`k@CJbxhS@UF!gqErfA)2W*W--e;)Q-+fF;T{JM2AiMxo+o2b*0mH57={h+?Q9 ztNv@PKg2_3CE~0OBtZ#UiYH;oy_&r0gkQy~e9DVa3GCfDhm2}m&OKh9rzdzgY{rZ7 zRFVc8ut<`w;ZVCTWWyW=I}7+>IO)Sh{E!d=X#}0ED#j&#l5P4H&j*#!CO%flHF;j8 z+?Twx@a>cXQDr(G$`Xl(7a;?HZq)O_dI+7bn&c1Up4$Sy$1BJahl=ABZOrFK=_ZtZ zKV#*RoK)8T1Yc5BL7452Z_&bYo{MP$!P4!lwumShtgx|sGBU7~wg&uMrD^MEj6(0B zEH$l(fPZj;R?a9MiFw|>Ib9X#clmEDpmpbX8ZO9hNqs9cST{IFWdfZSkM!uhu$I{T zv6L`8Pnu^JXB#w3<4IhWIbLtEPRH*mr-xtu1~qNDd6Ww%-}5nNbU7s__N<9v#D8+OYNH5x_t=rU`@rvlP-)G19oOG^_D&{D*5Z|Ekj-iN8 ziDZMAF?!J^4EIgHv3k=_sZ zy&3%YJ>Kh9uK*xn3*#2y=e_0^u)d$s1rWFU@pR-)ufbVHBG)jK(pU6g3&h>_nB#!?mz0T=z-2^7Elywxd??D{m}DKi{l_;gVHcjV zFZkv*6l;ADSH@Eu4==@l&pSFu0`=)=9IWYkIEZJX;9-5UzHLFjFQn-wbDQW~uNXDU z$3*c9wqRr)(MBc;!P{d763r$E>E;-?z{?4wp@{I(16dy{r-ZiL_3OfCzjKQUx`wy% zha4Nord9K}2*G6~$a{}^)e2yyswWL7&|p5rlFoRm6wMKO9(NEW zQue6+TmgyO(;Z2ygeuo=09vuzK6HexzwyW`g_Fx8hpsBZM3Yym?xWRzqJ?=7=XO34 z<%G-oV4VVH@hA@2Cf2>2g3lnu!df8}gl>>c-`2^y=Q_fMLq5)_cYm~+pL%7jQksee z@B!ekNG@Hyo|Hqq>hR&o-5_JWoNrr_haHXeR;Whb=X#jEq3h3kphrbiBE##WA5K-C z6~MeL>7CBq81m#8f<+;RW=m&Z?z!6iDQ83Y65I-V@IF=fq{_We9rS+EGmT!%&afmC z+L!TI@t%)z8e$-nik;HGRrdc`(k#}O1pw*NrpmJ$*b|5{`Y)lc;B*$nnYBM0ZjqMf zlHPF?y*+GiE8Z>*;)=UC!qE;8=`Ln$USUM?U%V=}_T$Q8!W?2YeU3N6*m9Ar5XPVj z^HO@rPE#qfSN~PkmB&N%MR5ibV;NyEnQViQEus;!g^|6IEnD`ogvk~rQIy?N+1HUm zlqIEvWGA#JWEo_TJxihdo~gvI`DbR%{hs^IxpVIOym#N7?>DL^Z!pz4(6~Z$`1O#? z60{aWACm8j>A0Vgm>(CbdXn@qP-v zJ*blPVxXB>V2oJSsoE;8{c}o9*nDO~U*<=9VH{7^vd;#__^ni(^g0%^VRjDpWVY5+t=W69giE925n(f}o<3FN>o5py<4!o4KOstzNhvzc1j`Evz0+V*I zN$x?TzeojE7WUzz0XI;Xj=9Mxd#P{qgia=PAOzt8ClX*VembnN zE<&A#WhhQO?KAdi!m~o5U{O5*p%?R1-?F1*eCZP%Qj>&a%4EJ~{+O9v?i{kNq0EA` z9VOJh8McLtC)lWHglf_G=@J!_X`~IB6$Q)g)g?eXIXU;l@c8NHvSQrs)Zq4Emh3@ppe_A`_k8ALwQD~yq?6j`k%)$xU@`4$8>AN)$c{Q3~pOrbZ6UXJio zw4_2YYmwB1VOm9*N7{>FaDmXz=KUAU z^PSxcDgQi$$cm_tmZC0Zu0zzE8VYyYG{*oaO6DJ1lzC z{HN=u&lg(17mTY-o-a9%!>7aXtG&=8xNiK+Cc z!A;C+8FMJ=K)cGtO#h$|nlDLsxoLu0 zbLQ6!3S(a@nwKYjeaWGg3DG2JDO@eIY?oO&(vex)?z#!8OSx{al}qV|c`jZS=FzYS zqb&E2uqBMfF*rs_T~}7g!e3-Q8_qR>)U13Z#2!$2pj>f|_F_#CySwlVb!i zJ)7(9y~egg&!*I_pEa(J$>zLtgO07cx~q}(qbEW@C{$Neb@rta0;>xZ$!(mbRD-K? z8HlPLM%ruAd08{&wD5Z0yT3%y0*ez7Y|dhkE}<5=uL^aD(|9MgY)H{U7gx$6z!$1$ zay99ETo^;?&6EmmUVlpI2h`fFyvBmfRI=EU&|Z~}RBm1xN@>>fj{kpbrL}Pnj-aEU zK!HyMgvo3fr`~hmSMjVQ?$T-SSk#@u)&rYm}FuQKF`oe^7oSqi=E#v62eEB z@W6?ziui80=b z2WPYxG(W-Lvr%}_I#wcr9c2l%IwKWoMq@I+%xsm|^{_@k9@8~&=DRlGlsw-N+NYBaN!Y5#x3eA;M0>!63};gp`lum{~<^Zk52={=`tsx)mv^kwu?#HSCH23XsA zovwsd7~y+lKiSsIyJ00x8Z7L!vuC_q61I#m zUwh_W&qv2%S-2{o@nJGC!&`~@;QV||em|YLk=w^($ zQsiCwIE-+rC|ox?}%bcb4aaTS)+cD?O3MN=fCD_6@yLPD9~F7a5m z@lKCziri%W=K$HqI%Tc{ES@mu9*mg<2_2d!g~HP5Rk8}(w%mjN6mNZLf`G-<`*fuV zq>|$C>!5CgTT$d-(I=>Kka6X?{I$cHy+rRh{rER)NoSfrO`KJjqn(V9Jl*_;N6aug z|GsbxmNvs4i!>1_5q_lCHY>a6e@?u&P(XuSq2dW4hhMIgmab#-nNKs!c1GHYA+b0j#t8>FDYHk z6)hfJ7Z8{cdCw$XQuvM1$|$}`8=-8k?SP`|$S_<$kAFMF`lb5SSeT}yQK{7ZkpoPP zE(pA`gWNJ7`VK*OA|@>J&@#z^de1iw-EV@dQ-M{2{tw@Z*}r+I^C^cvKM-|38F-n^ z)qASuq-T`d4_T^BXpQlLg4GXht@}oKZ7I&z5kfqf*MiVypJKF2@{jl`2E}S@s5bB{ z96;d5bvc`ika(j7lMTJbA>$3I&BTW#olz0^I#wf?99*9m~&;I;3u(6;)Is za>Oe%!SN4_4-Z#(E0S)oGM5Z8tc96dLN@;ov4%u|@@iH@h-qyEaFbA)Rg=jnu! zQ@Xy>Bz4Zw1}WIP?#jsT8n$9w7&2^^EV44{PrFG--p}F28Z(p>PSw~7$UN8@TY8ROtfa&OX`Q5f>!>OYSyy-lcyDB(^ zAu)J$_VS*O3~HU{zN5~E*Pj>`Z09PD5iC(jZ`ddl6FVc3Yu;?CBEyW1!lZPK$G@LS ziD!F$l2vcX=BQfU`lQ+w{kwK$rYg1cbbj3qVlfp~ni%$)s49$$H@88fMTw2}G>eg= zk#cC>IiywNTZY@6IkwQ~*S#=Ok#^bx-0L%Vc_-iaaDExn8I+tt_yuaaNbkoz@)ieP z_gJggWnQd@HZgkosP~JVGm%XAxmWR;6Z570T_GBW-T5!{bZs_tn5u0ib4|bS`IC)Oyl1Ad+C>=k z0(_Xxot!CU>XUkPfRW(anlmZ6xYiQIXz+qas?gb;kJNCvIrqT_c@JSHiEMYM8?H3o z%LzL3cHtzpo?kjW>6TE*N52Xx zy4ONA!oW{WoWF~7eZeHiK6p4%Je+iK^&#HWJ-y*^Yx|TSV$DzsmMDFpqVQ^}*(L5| z7=Gf3bfyr$MX484e|QVk>QbYH)5FkU1xc03(WiRU<+ttMb9^q&c{g_YL7t%)ueNQ1 zv4J~>nlcKDz9-1A5FaBt48_j5|8~HqnA+Cw4Luuq!9>gpSJcGC`KwG1f zI3lt7D*AD;GN!su+aoN}EgH@;vbvqb(xK^3+3Rx3D`I^SC;R!sX>Kw_u%sV*ah7W3 zN$EIG8N7p0uL@6<7qBGdTeg#& zIoK+WBXzHp`I}_%U1XGH44Le?K>Jv~L@~C{G>s*|TvX6g#x_KXP1nfRF9Os87sEt; z_Df2b+?%63zF?c5!?ZEkM%*)9JU~WO%%#0D zx0FCAA#7B?I2Nsk_`n;7kRjFI zoQofaP`^LHhS9%2sSh9A!NX|iRh3)_UU-SK16PNSgOGT7BrrS-qhtoY42zLnkn|vF z2Khw@xdJE>rGIrK4F6-MV5XQ+Z2?gpUQUu^W(@~PJ69LUKamv?(U5QSKsQky^rRm_ zLqeIrFGxUpL=-gOK*M2HfGCUtCRjN@9lc-a=pc~5^au>n%0_MqM!>h53fYkie~wKE z5oIR>20`J1KfVj7oq&rd5P;@7^ot|lH)fk{PXOU~86b|bLoD`h!2r}4uh3sEzC7gd z+#K+RO9;H-lKFE?@SPB{$xDV;@v(^gzssmdJ=P77aO4s=BwJdRe_n);MKsyzfdJP( zPP=r+|9F7!gb*zFAW0bekHcTRXbK9YT@K$xf$Yy3JF@t{xaJ=;Aw)o$9FXKV-wr7_ zvUs7@I6DL_3lPUefXs1};NKzHl977`4oLy1)OqAjPvk&_f#GqL9sQ6cR|F=vPoREOR6bvHo2xv{Ifl~qQva@a(oq>|6t(m+qh2|P|*)_c` z;aps|=NHJX%8c9&Yilwxp9fOEZ~-1)pgXeoOSuZx^EP~|!nC*G5<8$|3Q9_F7a>^1 zlDnYcZa{WD0#NZ}1N1y-0p97IN7%)AxXUft|zet6`>8d9Rf^jaE1*W@#zF4 zz%UDgG{bw9NZ{f;3^MSX+z6}tTd#z9G~`ANXg<0<67CH Date: Wed, 5 Jul 2023 14:21:33 +0300 Subject: [PATCH 145/312] adds support for multi-release-jar | mrj support for Traces (#3523) --- .github/setup.sh | 13 ++ .github/workflows/ci.yml | 82 ++++++- .github/workflows/publish.yml | 98 +++++++- .sdkmanrc | 3 + build.gradle | 3 + gradle/libs.versions.toml | 2 +- gradle/toolchains.gradle | 113 +++++++++ reactor-core/build.gradle | 46 +--- .../publisher/CallSiteSupplierFactory.java | 173 ++++++++++++++ .../java/reactor/core/publisher/Traces.java | 217 +----------------- .../java8stubs/sun/misc/JavaLangAccess.java | 11 - .../java8stubs/sun/misc/SharedSecrets.java | 9 - .../publisher/CallSiteSupplierFactory.java | 116 ++++++++++ .../java9stubs/java/lang/StackWalker.java | 30 --- .../test/java/reactor/util/LoggerTest.java | 13 +- settings.gradle | 3 +- 16 files changed, 616 insertions(+), 316 deletions(-) create mode 100755 .github/setup.sh create mode 100644 .sdkmanrc create mode 100644 gradle/toolchains.gradle create mode 100644 reactor-core/src/main/java/reactor/core/publisher/CallSiteSupplierFactory.java delete mode 100644 reactor-core/src/main/java8stubs/sun/misc/JavaLangAccess.java delete mode 100644 reactor-core/src/main/java8stubs/sun/misc/SharedSecrets.java create mode 100644 reactor-core/src/main/java9/reactor/core/publisher/CallSiteSupplierFactory.java delete mode 100644 reactor-core/src/main/java9stubs/java/lang/StackWalker.java diff --git a/.github/setup.sh b/.github/setup.sh new file mode 100755 index 0000000000..83b729f9a4 --- /dev/null +++ b/.github/setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -ex + +########################################################### +# JAVA +########################################################### + +mkdir -p /opt/openjdk +pushd /opt/openjdk > /dev/null +JDK_URL="https://github.com/AdoptOpenJDK/openjdk9-binaries/releases/download/jdk-9.0.4%2B11/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz" +mkdir java9 +pushd java9 > /dev/null +curl -L ${JDK_URL} --output OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40d1494f5e..8371512366 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,25 @@ jobs: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 with: fetch-depth: 0 #needed by spotless - - uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + - name: Download JDK 9 + if: ${{ github.base_ref == 'main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -45,7 +63,25 @@ jobs: needs: preliminary steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + - name: Download JDK 9 + if: ${{ github.base_ref == 'main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -59,7 +95,25 @@ jobs: needs: preliminary steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + - name: Download JDK 9 + if: ${{ github.base_ref == 'main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -73,11 +127,29 @@ jobs: needs: preliminary steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + - name: Download JDK 9 + if: ${{ github.base_ref == 'main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' java-version: 8 - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2 name: other tests with: - arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon + arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 149bc2b9ed..6f61c527f1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,7 +19,24 @@ jobs: fullVersion: ${{ steps.version.outputs.fullVersion }} steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - name: setup java + - name: Download JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' @@ -42,7 +59,24 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - name: setup java + - name: Download JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' @@ -62,7 +96,25 @@ jobs: environment: snapshots steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + - name: Download JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -82,7 +134,25 @@ jobs: environment: releases steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + - name: Download JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -104,7 +174,25 @@ jobs: environment: releases steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + - name: Download JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 with: distribution: 'temurin' java-version: 8 diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 0000000000..efa0e43ec4 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1,3 @@ +# Enable auto-env through the sdkman_auto_env config +# Add key=value pairs of SDKs to use below +java=8.0.372-librca diff --git a/build.gradle b/build.gradle index 32c31b4f43..3d64366c8e 100644 --- a/build.gradle +++ b/build.gradle @@ -38,12 +38,14 @@ plugins { alias(libs.plugins.nohttp) alias(libs.plugins.jcstress) apply false alias(libs.plugins.spotless) + alias(libs.plugins.mrjar) apply false } apply plugin: "io.reactor.gradle.detect-ci" apply from: "gradle/asciidoc.gradle" // asciidoc (which is generated from root dir) apply from: "gradle/releaser.gradle" apply from: "gradle/dependencies.gradle" +apply from: "gradle/toolchains.gradle" repositories { //needed at root for asciidoctor and nohttp-checkstyle mavenCentral() @@ -141,6 +143,7 @@ configure(subprojects) { p -> apply plugin: 'java' apply plugin: 'jacoco' apply from: "${rootDir}/gradle/setup.gradle" + apply from: "${rootDir}/gradle/toolchains.gradle" description = 'Non-Blocking Reactive Foundation for the JVM' group = 'io.projectreactor' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5cc0545789..949f349994 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,4 +48,4 @@ jcstress = { id = "io.github.reyerizo.gradle.jcstress", version = "0.8.15" } nohttp = { id = "io.spring.nohttp", version = "0.0.11" } shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } spotless = { id = "com.diffplug.spotless", version = "6.13.0" } - +mrjar = { id = "me.champeau.mrjar", version = "0.1.1" } diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle new file mode 100644 index 0000000000..c3a0f57905 --- /dev/null +++ b/gradle/toolchains.gradle @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2011-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Copied from https://github.com/spring-projects/spring-framework/blob/main/gradle/toolchains.gradle + */ + +/** + * Apply the JVM Toolchain conventions + * See https://docs.gradle.org/current/userguide/toolchains.html + * + * One can choose the toolchain to use for compiling and running the TEST sources. + * These options apply to Java, Kotlin and Groovy test sources when available. + * {@code "./gradlew check -PtestToolchain=22"} will use a JDK22 + * toolchain for compiling and running the test SourceSet. + * + * By default, the main build will fall back to using the a JDK 17 + * toolchain (and 17 language level) for all main sourceSets. + * See {@link io.reactor.gradle.JavaConventions}. + * + * Gradle will automatically detect JDK distributions in well-known locations. + * The following command will list the detected JDKs on the host. + * {@code + * $ ./gradlew -q javaToolchains + * } + * + * We can also configure ENV variables and let Gradle know about them: + * {@code + * $ echo JDK17 + * /opt/openjdk/java17 + * $ echo JDK22 + * /opt/openjdk/java22 + * $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK22 check + * } + * + * @author Brian Clozel + * @author Sam Brannen + */ + +def testToolchainConfigured() { + return project.hasProperty('testToolchain') && project.testToolchain +} + +def testToolchainLanguageVersion() { + if (testToolchainConfigured()) { + return JavaLanguageVersion.of(project.testToolchain.toString()) + } + return JavaLanguageVersion.of(8) +} + +plugins.withType(JavaPlugin).configureEach { + // Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined + if (testToolchainConfigured()) { + def testLanguageVersion = testToolchainLanguageVersion() + tasks.withType(JavaCompile).matching { it.name.contains("Test") }.configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = testLanguageVersion + } + } + tasks.withType(Test).configureEach{ + javaLauncher = javaToolchains.launcherFor { + languageVersion = testLanguageVersion + } + } + } +} + +// Configure the JMH plugin to use the toolchain for generating and running JMH bytecode +pluginManager.withPlugin("me.champeau.jmh") { + if (testToolchainConfigured()) { + tasks.matching { it.name.contains('jmh') && it.hasProperty('javaLauncher') }.configureEach { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(testToolchainLanguageVersion()) + }) + } + tasks.withType(JavaCompile).matching { it.name.contains("Jmh") }.configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = testToolchainLanguageVersion() + } + } + } +} + +// Store resolved Toolchain JVM information as custom values in the build scan. +rootProject.ext { + resolvedMainToolchain = false + resolvedTestToolchain = false +} +gradle.taskGraph.afterTask { Task task, TaskState state -> + if (!resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) { + def metadata = task.javaCompiler.get().metadata + task.project.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") + resolvedMainToolchain = true + } + if (testToolchainConfigured() && !resolvedTestToolchain && task instanceof Test && task.javaLauncher.isPresent()) { + def metadata = task.javaLauncher.get().metadata + task.project.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") + resolvedTestToolchain = true + } +} diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index ad08afa33c..926c4e3a81 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -21,6 +21,7 @@ apply plugin: 'biz.aQute.bnd.builder' apply plugin: 'jvm-test-suite' apply plugin: 'jcstress' apply plugin: 'java-library' +apply plugin: 'me.champeau.mrjar' ext { bndOptions = [ @@ -43,6 +44,10 @@ ext { ] } +multiRelease { + targetVersions 8, 9, 21 +} + testing { suites { test { @@ -347,6 +352,12 @@ if (!JavaVersion.current().isJava9Compatible()) { } } +if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) { + blockHoundTest { + jvmArgs = ["-XX:+AllowRedefinitionToAddDeleteMethods"] + } +} + jar { manifest { attributes 'Implementation-Title': 'reactor-core', @@ -369,39 +380,4 @@ jacocoTestReport.dependsOn test check.dependsOn jacocoTestReport check.dependsOn japicmp - -if (JavaVersion.current().java9Compatible) { - // SharedSecretsCallSiteSupplierFactory is a Java 8 specific optimization. - // It uses sun.misc.SharedSecrets which is unavailable on Java 9+, and the compilation would fail with JDK 9. - // This workaround allows compiling the main sourceset on JDK 9+ while still referring to the Java 8 classes. - - sourceSets { - java8stubs.java.srcDirs = ['src/main/java8stubs'] - } - - tasks.withType(JavaCompile).all { - sourceCompatibility = targetCompatibility = 8 - } - - tasks.withType(Javadoc).all { - excludes = ["reactor/core/publisher/Traces.java"] - } - - dependencies { - compileOnly sourceSets.java8stubs.output - } -} -else { - // reactor.core.publisher.Traces contains a strategy that only works with Java 9+. - // While compiling on Java 8, we can't access Java 9+ APIs. - // To workaround this, we "link" the main sourceset to the stubs of Java 9 APIs without having to use the Java 9 target. - sourceSets { - java9stubs.java.srcDirs = ['src/main/java9stubs'] - } - - dependencies { - compileOnly sourceSets.java9stubs.output - } -} - // docs.zip is added in afterEvaluate block in setup.gradle diff --git a/reactor-core/src/main/java/reactor/core/publisher/CallSiteSupplierFactory.java b/reactor-core/src/main/java/reactor/core/publisher/CallSiteSupplierFactory.java new file mode 100644 index 0000000000..dba396097a --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/CallSiteSupplierFactory.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import sun.misc.JavaLangAccess; +import sun.misc.SharedSecrets; + +import static reactor.core.publisher.Traces.full; +import static reactor.core.publisher.Traces.isUserCode; +import static reactor.core.publisher.Traces.shouldSanitize; + +/** + * Utility class for the call-site extracting on Java 8. + */ +class CallSiteSupplierFactory implements Supplier> { + + static final Supplier> supplier; + + static { + String[] strategyClasses = { + CallSiteSupplierFactory.class.getName() + "$SharedSecretsCallSiteSupplierFactory", + CallSiteSupplierFactory.class.getName() + "$ExceptionCallSiteSupplierFactory", + }; + // tries to use the stacktrace traversing approach via the + // sun.misc.JavaLangAccess.getStackTrace* or falls back to the default way of + // stacktrace retrieval via the java.lang.Throwable.getStackTrace method + supplier = Stream + .of(strategyClasses) + .flatMap(className -> { + try { + Class clazz = Class.forName(className); + @SuppressWarnings("unchecked") + Supplier> function = (Supplier) clazz.getDeclaredConstructor() + .newInstance(); + return Stream.of(function); + } + // explicitly catch LinkageError to support static code analysis + // tools detect the attempt at finding out jdk environment + catch (LinkageError e) { + return Stream.empty(); + } + catch (Throwable e) { + return Stream.empty(); + } + }) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Valid strategy not found")); + } + + + + @Override + public Supplier get() { + return supplier.get(); + } + + @SuppressWarnings("unused") + static class SharedSecretsCallSiteSupplierFactory implements Supplier> { + + static { + SharedSecrets.getJavaLangAccess(); + } + + @Override + public Supplier get() { + return new TracingException(); + } + + static class TracingException extends Throwable implements Supplier { + + static final JavaLangAccess javaLangAccess = SharedSecrets.getJavaLangAccess(); + + @Override + public String get() { + int stackTraceDepth = javaLangAccess.getStackTraceDepth(this); + + StackTraceElement previousElement = null; + // Skip get() + for (int i = 4; i < stackTraceDepth; i++) { + StackTraceElement e = javaLangAccess.getStackTraceElement(this, i); + + String className = e.getClassName(); + if (isUserCode(className)) { + StringBuilder sb = new StringBuilder(); + + if (previousElement != null) { + sb.append("\t").append(previousElement.toString()).append("\n"); + } + sb.append("\t").append(e.toString()).append("\n"); + return sb.toString(); + } + else { + if (!full && e.getLineNumber() <= 1) { + continue; + } + + String classAndMethod = className + "." + e.getMethodName(); + if (!full && shouldSanitize(classAndMethod)) { + continue; + } + previousElement = e; + } + } + + return ""; + } + } + } + + @SuppressWarnings("unused") + static class ExceptionCallSiteSupplierFactory implements Supplier> { + + @Override + public Supplier get() { + return new TracingException(); + } + + static class TracingException extends Throwable implements Supplier { + + @Override + public String get() { + StackTraceElement previousElement = null; + StackTraceElement[] stackTrace = getStackTrace(); + // Skip get() + for (int i = 4; i < stackTrace.length; i++) { + StackTraceElement e = stackTrace[i]; + + String className = e.getClassName(); + if (isUserCode(className)) { + StringBuilder sb = new StringBuilder(); + + if (previousElement != null) { + sb.append("\t").append(previousElement.toString()).append("\n"); + } + sb.append("\t").append(e.toString()).append("\n"); + return sb.toString(); + } + else { + if (!full && e.getLineNumber() <= 1) { + continue; + } + + String classAndMethod = className + "." + e.getMethodName(); + if (!full && shouldSanitize(classAndMethod)) { + continue; + } + previousElement = e; + } + } + + return ""; + } + } + } + +} \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/core/publisher/Traces.java b/reactor-core/src/main/java/reactor/core/publisher/Traces.java index 7d219562c5..bac53fc126 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Traces.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Traces.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,11 @@ package reactor.core.publisher; -import java.util.Iterator; import java.util.List; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; -import sun.misc.JavaLangAccess; -import sun.misc.SharedSecrets; /** * Utilities around manipulating stack traces and displaying assembly traces. @@ -48,217 +45,7 @@ final class Traces { * each element being prepended with a tabulation and appended with a * newline. */ - static Supplier> callSiteSupplierFactory; - - static { - String[] strategyClasses = { - Traces.class.getName() + "$StackWalkerCallSiteSupplierFactory", - Traces.class.getName() + "$SharedSecretsCallSiteSupplierFactory", - Traces.class.getName() + "$ExceptionCallSiteSupplierFactory", - }; - // find one available call-site supplier w.r.t. the jdk version to provide - // linkage-compatibility between jdk 8 and 9+ - callSiteSupplierFactory = Stream - .of(strategyClasses) - .flatMap(className -> { - try { - Class clazz = Class.forName(className); - @SuppressWarnings("unchecked") - Supplier> function = (Supplier) clazz.getDeclaredConstructor() - .newInstance(); - return Stream.of(function); - } - // explicitly catch LinkageError to support static code analysis - // tools detect the attempt at finding out jdk environment - catch (LinkageError e) { - return Stream.empty(); - } - catch (Throwable e) { - return Stream.empty(); - } - }) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Valid strategy not found")); - } - - /** - * Utility class for the call-site extracting on Java 9+. - * - */ - @SuppressWarnings("unused") - static final class StackWalkerCallSiteSupplierFactory implements Supplier> { - - static { - // Trigger eager StackWalker class loading. - StackWalker.getInstance(); - } - - /** - * Transform the current stack trace into a {@link String} representation, - * each element being prepended with a tabulation and appended with a - * newline. - * - * @return the string version of the stacktrace. - */ - @Override - public Supplier get() { - StackWalker.StackFrame[] stack = StackWalker.getInstance().walk(s -> { - StackWalker.StackFrame[] result = new StackWalker.StackFrame[10]; - Iterator iterator = s.iterator(); - iterator.next(); // .get - - int i = 0; - while (iterator.hasNext()) { - StackWalker.StackFrame frame = iterator.next(); - - if (i >= result.length) { - return new StackWalker.StackFrame[0]; - } - - result[i++] = frame; - - if (isUserCode(frame.getClassName())) { - break; - } - } - StackWalker.StackFrame[] copy = new StackWalker.StackFrame[i]; - System.arraycopy(result, 0, copy, 0, i); - return copy; - }); - - if (stack.length == 0) { - return () -> ""; - } - - if (stack.length == 1) { - return () -> "\t" + stack[0].toString() + "\n"; - } - - return () -> { - StringBuilder sb = new StringBuilder(); - - for (int j = stack.length - 2; j > 0; j--) { - StackWalker.StackFrame previous = stack[j]; - - if (!full) { - if (previous.isNativeMethod()) { - continue; - } - - String previousRow = previous.getClassName() + "." + previous.getMethodName(); - if (shouldSanitize(previousRow)) { - continue; - } - } - sb.append("\t") - .append(previous.toString()) - .append("\n"); - break; - } - - sb.append("\t") - .append(stack[stack.length - 1].toString()) - .append("\n"); - - return sb.toString(); - }; - } - } - - @SuppressWarnings("unused") - static class SharedSecretsCallSiteSupplierFactory implements Supplier> { - - @Override - public Supplier get() { - return new TracingException(); - } - - static class TracingException extends Throwable implements Supplier { - - static final JavaLangAccess javaLangAccess = SharedSecrets.getJavaLangAccess(); - - @Override - public String get() { - int stackTraceDepth = javaLangAccess.getStackTraceDepth(this); - - StackTraceElement previousElement = null; - // Skip get() - for (int i = 2; i < stackTraceDepth; i++) { - StackTraceElement e = javaLangAccess.getStackTraceElement(this, i); - - String className = e.getClassName(); - if (isUserCode(className)) { - StringBuilder sb = new StringBuilder(); - - if (previousElement != null) { - sb.append("\t").append(previousElement.toString()).append("\n"); - } - sb.append("\t").append(e.toString()).append("\n"); - return sb.toString(); - } - else { - if (!full && e.getLineNumber() <= 1) { - continue; - } - - String classAndMethod = className + "." + e.getMethodName(); - if (!full && shouldSanitize(classAndMethod)) { - continue; - } - previousElement = e; - } - } - - return ""; - } - } - } - - @SuppressWarnings("unused") - static class ExceptionCallSiteSupplierFactory implements Supplier> { - - @Override - public Supplier get() { - return new TracingException(); - } - - static class TracingException extends Throwable implements Supplier { - - @Override - public String get() { - StackTraceElement previousElement = null; - StackTraceElement[] stackTrace = getStackTrace(); - // Skip get() - for (int i = 2; i < stackTrace.length; i++) { - StackTraceElement e = stackTrace[i]; - - String className = e.getClassName(); - if (isUserCode(className)) { - StringBuilder sb = new StringBuilder(); - - if (previousElement != null) { - sb.append("\t").append(previousElement.toString()).append("\n"); - } - sb.append("\t").append(e.toString()).append("\n"); - return sb.toString(); - } - else { - if (!full && e.getLineNumber() <= 1) { - continue; - } - - String classAndMethod = className + "." + e.getMethodName(); - if (!full && shouldSanitize(classAndMethod)) { - continue; - } - previousElement = e; - } - } - - return ""; - } - } - } + static final Supplier> callSiteSupplierFactory = new CallSiteSupplierFactory(); /** * Return true for strings (usually from a stack trace element) that should be diff --git a/reactor-core/src/main/java8stubs/sun/misc/JavaLangAccess.java b/reactor-core/src/main/java8stubs/sun/misc/JavaLangAccess.java deleted file mode 100644 index c5e5362b95..0000000000 --- a/reactor-core/src/main/java8stubs/sun/misc/JavaLangAccess.java +++ /dev/null @@ -1,11 +0,0 @@ -package sun.misc; - -/** - * Stub for the Java 8 compatibility when compiled with JDK 9+. - */ -public interface JavaLangAccess { - - int getStackTraceDepth(Throwable e); - - StackTraceElement getStackTraceElement(Throwable e, int depth); -} diff --git a/reactor-core/src/main/java8stubs/sun/misc/SharedSecrets.java b/reactor-core/src/main/java8stubs/sun/misc/SharedSecrets.java deleted file mode 100644 index a49bc02a4d..0000000000 --- a/reactor-core/src/main/java8stubs/sun/misc/SharedSecrets.java +++ /dev/null @@ -1,9 +0,0 @@ -package sun.misc; - -/** - * Stub for the Java 8 compatibility when compiled with JDK 9+. - */ -public class SharedSecrets { - - public static native JavaLangAccess getJavaLangAccess(); -} diff --git a/reactor-core/src/main/java9/reactor/core/publisher/CallSiteSupplierFactory.java b/reactor-core/src/main/java9/reactor/core/publisher/CallSiteSupplierFactory.java new file mode 100644 index 0000000000..d248cda6dc --- /dev/null +++ b/reactor-core/src/main/java9/reactor/core/publisher/CallSiteSupplierFactory.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Iterator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static reactor.core.publisher.Traces.full; +import static reactor.core.publisher.Traces.isUserCode; +import static reactor.core.publisher.Traces.shouldSanitize; + +/** + * Utility class for the call-site extracting on Java 9+. + */ +final class CallSiteSupplierFactory implements Supplier>, Function, StackWalker.StackFrame[]> { + + static { + // Trigger eager StackWalker class loading. + StackWalker.getInstance(); + } + + @Override + public StackWalker.StackFrame[] apply(Stream s) { + StackWalker.StackFrame[] result = + new StackWalker.StackFrame[10]; + Iterator iterator = s.iterator(); + iterator.next(); // .get + + int i = 0; + while (iterator.hasNext()) { + StackWalker.StackFrame frame = iterator.next(); + + if (i >= result.length) { + return new StackWalker.StackFrame[0]; + } + + result[i++] = frame; + + if (isUserCode(frame.getClassName())) { + break; + } + } + StackWalker.StackFrame[] copy = + new StackWalker.StackFrame[i]; + System.arraycopy(result, 0, copy, 0, i); + return copy; + } + + /** + * Transform the current stack trace into a {@link String} representation, + * each element being prepended with a tabulation and appended with a + * newline. + * + * @return the string version of the stacktrace. + */ + @Override + public Supplier get() { + StackWalker.StackFrame[] stack = + StackWalker.getInstance() + .walk(this); + + if (stack.length == 0) { + return () -> ""; + } + + if (stack.length == 1) { + return () -> "\t" + stack[0].toString() + "\n"; + } + + return () -> { + StringBuilder sb = new StringBuilder(); + + for (int j = stack.length - 2; j > 0; j--) { + StackWalker.StackFrame previous = stack[j]; + + if (!full) { + if (previous.isNativeMethod()) { + continue; + } + + String previousRow = + previous.getClassName() + "." + previous.getMethodName(); + if (shouldSanitize(previousRow)) { + continue; + } + } + sb.append("\t") + .append(previous.toString()) + .append("\n"); + break; + } + + sb.append("\t") + .append(stack[stack.length - 1].toString()) + .append("\n"); + + return sb.toString(); + }; + } +} \ No newline at end of file diff --git a/reactor-core/src/main/java9stubs/java/lang/StackWalker.java b/reactor-core/src/main/java9stubs/java/lang/StackWalker.java deleted file mode 100644 index 4f2264eceb..0000000000 --- a/reactor-core/src/main/java9stubs/java/lang/StackWalker.java +++ /dev/null @@ -1,30 +0,0 @@ -package java.lang; - -import java.util.function.Function; -import java.util.stream.Stream; - -import sun.reflect.CallerSensitive; - -/** - * Stub for the Java 9 compatibility when compiled with JDK 8. - */ -public class StackWalker { - - public static StackWalker getInstance() { - return null; - } - - @CallerSensitive - public T walk(Function, ? extends T> function) { - return null; - } - - public interface StackFrame { - String getClassName(); - - String getMethodName(); - - boolean isNativeMethod(); - } - -} diff --git a/reactor-core/src/test/java/reactor/util/LoggerTest.java b/reactor-core/src/test/java/reactor/util/LoggerTest.java index c6762b6a77..b4527ff7c4 100644 --- a/reactor-core/src/test/java/reactor/util/LoggerTest.java +++ b/reactor-core/src/test/java/reactor/util/LoggerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2021-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,13 @@ */ class LoggerTest { + // This class is needed to overcome "Cannot call abstract real method on java + // object!" problem for interfaces with default methods. + // For more details see: https://github.com/mockito/mockito/issues/2587 + static abstract class InnerLogger implements Logger { } + private Logger mockVerbose() { - Logger mockLogger = Mockito.mock(Logger.class); + Logger mockLogger = Mockito.mock(InnerLogger.class); when(mockLogger.isInfoEnabled()).thenReturn(true); when(mockLogger.isWarnEnabled()).thenReturn(true); @@ -46,7 +51,7 @@ private Logger mockVerbose() { } private Logger mockTerse() { - Logger mockLogger = Mockito.mock(Logger.class); + Logger mockLogger = Mockito.mock(InnerLogger.class); when(mockLogger.isInfoEnabled()).thenReturn(true); when(mockLogger.isWarnEnabled()).thenReturn(true); @@ -64,7 +69,7 @@ private Logger mockTerse() { } private Logger mockSilent() { - Logger mockLogger = Mockito.mock(Logger.class); + Logger mockLogger = Mockito.mock(InnerLogger.class); when(mockLogger.isInfoEnabled()).thenReturn(false); when(mockLogger.isWarnEnabled()).thenReturn(false); diff --git a/settings.gradle b/settings.gradle index ee44636416..4f68238ef8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ plugins { id "com.gradle.enterprise" version "3.12.4" + id "org.gradle.toolchains.foojay-resolver-convention" version "0.5.0" } rootProject.name = 'reactor' From 7d2778897730c0c9f6a990aacb4af5ece6440bac Mon Sep 17 00:00:00 2001 From: Nicolas Vervelle <77850427+nvervelle@users.noreply.github.com> Date: Wed, 5 Jul 2023 13:32:33 +0200 Subject: [PATCH 146/312] Observation customization with Micrometer.observation (#3456) This change introduces a variant of Micrometer.observation method, that accepts a Function. It can be used to create a customized Observation instead of the default one that would be created on behalf of the user. Resolves #3455 --- docs/asciidoc/metrics-details.adoc | 4 + docs/asciidoc/metrics.adoc | 31 +++++- .../observability/micrometer/Micrometer.java | 20 +++- .../MicrometerObservationListener.java | 39 +++++-- .../MicrometerObservationListenerFactory.java | 15 ++- .../MicrometerObservationListenerTest.java | 104 +++++++++++++++++- 6 files changed, 199 insertions(+), 14 deletions(-) diff --git a/docs/asciidoc/metrics-details.adoc b/docs/asciidoc/metrics-details.adoc index 529c4a6ef6..5cde5febe3 100644 --- a/docs/asciidoc/metrics-details.adoc +++ b/docs/asciidoc/metrics-details.adoc @@ -30,4 +30,8 @@ Below is the list of meters used by the observation tap listener feature, as exp This is the ANONYMOUS observation, but you can create a similar Observation with a custom name by using the `name(String)` operator. +NOTE: You can also fully customize Micrometer's Observation via +`Micrometer.observation(ObservationRegistry registry, Function observationSupplier)` +with your own Observation supplier, allowing to configure its attributes (name, contextual name, low and high cardinality keys, ...). + include::{root-target}observation_metrics.adoc[leveloffset=4] \ No newline at end of file diff --git a/docs/asciidoc/metrics.adoc b/docs/asciidoc/metrics.adoc index 4bb292560a..083117c4d6 100644 --- a/docs/asciidoc/metrics.adoc +++ b/docs/asciidoc/metrics.adoc @@ -141,4 +141,33 @@ listenToEvents() <3> A registry must be provided into which to publish the observation results. Note this is an `ObservationRegistry`. ==== -The detail of the observation and its tags is provided in <>. \ No newline at end of file +The detail of the observation and its tags is provided in <>. + +You can also fully customize Micrometer's Observation via +`Micrometer.observation(ObservationRegistry registry, Function observationSupplier)` +with your own Observation supplier, as follows: +==== +[source,java] +---- +listenToEvents() + .name("events") // <1> + .tap(Micrometer.observation( // <2> + applicationDefinedRegistry, // <3> + registry -> Observation.createNotStarted( // <4> + myConvention, // <5> + myContextSupplier, // <6> + registry))) + .doOnNext(event -> log.info("Received {}", event)) + .delayUntil(this::processEvent) + .retry() + .subscribe(); +---- +<1> The `Observation` for this pipeline will be identified with the "events" prefix. +<2> We use the `tap` operator with the `observation` utility. +<3> A registry must be provided into which to publish the observation results. Note this is an `ObservationRegistry`. +<4> We provide our own function to create the Observation +<5> with a custom `ObservationConvention` +<6> and a custom `Supplier`. +==== + + diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java index 4a7e06aac1..8e37244c45 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/Micrometer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package reactor.core.observability.micrometer; +import java.util.function.Function; + import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Clock; @@ -93,6 +95,22 @@ public final class Micrometer { return new MicrometerObservationListenerFactory<>(registry); } + /** + * Similar to {@link #observation(ObservationRegistry)} but enables providing + * a function creating the Micrometer {@link Observation} + * representing the runtime of the publisher to the provided {@link ObservationRegistry}. + * If this function returns {@code null}, the behavior will be identical to + * {@link #observation(ObservationRegistry)} with a default {@link Observation}. + * + * @param the type of onNext in the target publisher + * @return a {@link SignalListenerFactory} to record observations + * @see MicrometerObservationListenerDocumentation + */ + public static SignalListenerFactory observation(ObservationRegistry registry, + Function observationSupplier) { + return new MicrometerObservationListenerFactory<>(registry, observationSupplier); + } + /** * Wrap a {@link Scheduler} in an instance that gathers various task-related metrics using * the provided {@link MeterRegistry} and naming meters using the provided {@code metricsPrefix}. diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java index 42f16215f7..29f05e7935 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package reactor.core.observability.micrometer; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; import reactor.core.observability.SignalListener; import reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags; @@ -27,6 +28,8 @@ import reactor.util.context.Context; import reactor.util.context.ContextView; +import java.util.function.Function; + import static reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags.STATUS; import static reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags.TAG_STATUS_CANCELLED; import static reactor.core.observability.micrometer.MicrometerObservationListenerDocumentation.ObservationTags.TAG_STATUS_COMPLETED; @@ -70,11 +73,20 @@ final class MicrometerObservationListener implements SignalListener { boolean valued; MicrometerObservationListener(ContextView subscriberContext, MicrometerObservationListenerConfiguration configuration) { - this(subscriberContext, configuration, TAG_STATUS_COMPLETED); + this(subscriberContext, configuration, null); + } + + MicrometerObservationListener(ContextView subscriberContext, + MicrometerObservationListenerConfiguration configuration, + @Nullable Function observationSupplier) { + this(subscriberContext, configuration, TAG_STATUS_COMPLETED, observationSupplier); } //for test purposes, we can pass in a value for the status tag, to be used when a Mono completes from onNext - MicrometerObservationListener(ContextView subscriberContext, MicrometerObservationListenerConfiguration configuration, String completedOnNextStatus) { + MicrometerObservationListener(ContextView subscriberContext, + MicrometerObservationListenerConfiguration configuration, + String completedOnNextStatus, + @Nullable Function observationSupplier) { this.configuration = configuration; this.originalContext = subscriberContext; this.completedOnNextStatus = completedOnNextStatus; @@ -85,12 +97,21 @@ final class MicrometerObservationListener implements SignalListener { //while doOnSubscription matches the moment where the Publisher acknowledges said subscription //NOTE: we don't use the `DocumentedObservation` features to create the Observation, even for the ANONYMOUS case, //because the discovered tags could be more than the documented defaults - tapObservation = Observation.createNotStarted( - configuration.sequenceName, - configuration.registry - ) - .contextualName(configuration.sequenceName) - .lowCardinalityKeyValues(configuration.commonKeyValues); + tapObservation = defaultObservation(configuration, observationSupplier) + .contextualName(configuration.sequenceName) + .lowCardinalityKeyValues(configuration.commonKeyValues); + } + + private static Observation defaultObservation( + MicrometerObservationListenerConfiguration configuration, + @Nullable Function observationSupplier) { + if (observationSupplier != null) { + final Observation observation = observationSupplier.apply(configuration.registry); + if (observation != null) { + return observation; + } + } + return Observation.createNotStarted(configuration.sequenceName, configuration.registry); } @Override diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactory.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactory.java index 06213da981..32671475f4 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactory.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListenerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package reactor.core.observability.micrometer; +import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import org.reactivestreams.Publisher; @@ -23,8 +24,11 @@ import reactor.core.observability.SignalListenerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; import reactor.util.context.ContextView; +import java.util.function.Function; + /** * A {@link SignalListenerFactory} for {@link MicrometerObservationListener}. * @@ -33,9 +37,16 @@ class MicrometerObservationListenerFactory implements SignalListenerFactory { final ObservationRegistry registry; + @Nullable final Function observationSupplier; public MicrometerObservationListenerFactory(ObservationRegistry registry) { + this(registry, null); + } + + public MicrometerObservationListenerFactory(ObservationRegistry registry, + @Nullable Function observationSupplier) { this.registry = registry; + this.observationSupplier = observationSupplier; } @Override @@ -54,6 +65,6 @@ else if (source instanceof Flux) { @Override public SignalListener createListener(Publisher source, ContextView listenerContext, MicrometerObservationListenerConfiguration publisherContext) { - return new MicrometerObservationListener<>(listenerContext, publisherContext); + return new MicrometerObservationListener<>(listenerContext, publisherContext, observationSupplier); } } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java index ee86e1c245..a0cf2ed107 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java @@ -23,6 +23,7 @@ import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Clock; import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import io.micrometer.observation.tck.TestObservationRegistry; import org.junit.jupiter.api.BeforeEach; @@ -37,6 +38,7 @@ import static io.micrometer.observation.tck.TestObservationRegistryAssert.assertThat; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Simon Baslé @@ -233,6 +235,106 @@ void tapFromMonoWithTags(boolean automatic) { .hasKeyValuesCount(4); } + private static class CustomConvention implements ObservationConvention { + + @Override + public KeyValues getLowCardinalityKeyValues(Observation.Context context) { + return KeyValues.of("testTag1", "testTagValue1"); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return true; + } + + @Override + public String getName() { + return "myName"; + } + + @Override + public String getContextualName(Observation.Context context) { + return "myContextualName"; + } + } + + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void tapFromMonoWithCustomConvention(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } + Mono mono = Mono.just(1) + .name("testMono") + .tap(Micrometer.observation( + registry, + observationRegistry -> Observation.createNotStarted(new CustomConvention(), observationRegistry))); + + assertThat(registry).as("before subscription").doesNotHaveAnyObservation(); + + mono.block(); + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("myName") + .hasContextualNameEqualTo("myContextualName") + .as("subscribeToTerminalObservation") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("reactor.type", "Mono") + .hasLowCardinalityKeyValue("reactor.status", "completed") + .hasKeyValuesCount(3); + } + + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void tapFromMonoWithObservationSupplierReturningNull(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } + Mono mono = Mono.just(1) + .name("testMono") + .tap(Micrometer.observation( + registry, + observationRegistry -> null)); + + assertThat(registry).as("before subscription").doesNotHaveAnyObservation(); + + mono.block(); + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("testMono") + .hasContextualNameEqualTo("testMono") + .as("subscribeToTerminalObservation") + .hasBeenStarted() + .hasBeenStopped() + .hasLowCardinalityKeyValue("reactor.type", "Mono") + .hasLowCardinalityKeyValue("reactor.status", "completed") + .hasKeyValuesCount(2); + } + + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void tapFromMonoWithObservationSupplierThrowingException(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } + Mono mono = Mono.just(1) + .name("testMono") + .tap(Micrometer.observation( + registry, + observationRegistry -> { + throw new IllegalStateException("Exception should be handled"); + })); + + assertThat(registry).as("before subscription").doesNotHaveAnyObservation(); + + assertThatThrownBy(mono::block) + .isInstanceOf(IllegalStateException.class); + } + @ParameterizedTestWithName @ValueSource(booleans = {true, false}) void observationStoppedByCancellation(boolean automatic) { @@ -336,7 +438,7 @@ void observationMonoStoppedByOnNext(boolean automatic) { final String expectedStatus = "completedOnNext"; //we use a test-oriented constructor to force the onNext completion case to have a different tag value - MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration, expectedStatus); + MicrometerObservationListener listener = new MicrometerObservationListener<>(subscriberContext, configuration, expectedStatus, null); listener.doFirst(); // forces observation start listener.doOnNext(1); // emulates onNext, should stop observation From acd52381ca14f25be1823da174054d5b9bd8fe12 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:02:34 +0300 Subject: [PATCH 147/312] introduces VirtualThreads support for BoundedElasticScheduler (#3524) --- .github/workflows/ci.yml | 33 ++++++++++++ reactor-core/build.gradle | 8 +++ .../BoundedElasticSchedulerSupplier.java | 51 ++++++++++++++++++ .../reactor/core/scheduler/Schedulers.java | 20 +++++-- .../BoundedElasticSchedulerSupplier.java | 52 +++++++++++++++++++ .../core/publisher/ParallelFluxTest.java | 4 +- 6 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java create mode 100644 reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8371512366..93b64f0039 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,39 @@ jobs: name: gradle with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=!slow + java-21-core-fast: + if: ${{ github.base_ref == 'main' }} + name: Java 21 core fast tests + runs-on: ubuntu-latest + needs: preliminary + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 + - name: Download JDK 9 + if: ${{ github.base_ref == 'main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 21-ea + - name: Setup JDK 8 + uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # tag=v3 + with: + distribution: 'temurin' + java-version: 8 + - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2 + name: gradle + with: + arguments: :reactor-core:java21Test --no-daemon -Pjunit-tags=!slow core-slow: name: core slower tests runs-on: ubuntu-latest diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 926c4e3a81..a33680e7e6 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -298,6 +298,14 @@ test { } } +java21Test { + println("Available number of processors is " + Runtime.getRuntime().availableProcessors()) + if (Runtime.getRuntime().availableProcessors() <= 2) { + systemProperty "jdk.virtualThreadScheduler.parallelism", "6" + } + systemProperty "reactor.schedulers.defaultBoundedElasticOnVirtualThreads", "true" +} + tasks.withType(Test).matching { !(it.name in testing.suites.names) }.configureEach { def tags = rootProject.findProperty("junit-tags") if (tags != null) { diff --git a/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java new file mode 100644 index 0000000000..9f59e421c5 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import java.util.function.Supplier; + +import reactor.util.Logger; +import reactor.util.Loggers; + +import static reactor.core.scheduler.Schedulers.BOUNDED_ELASTIC; +import static reactor.core.scheduler.Schedulers.DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS; +import static reactor.core.scheduler.Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE; +import static reactor.core.scheduler.Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE; +import static reactor.core.scheduler.Schedulers.newBoundedElastic; + +/** + * JDK 8 Specific implementation of BoundedElasticScheduler supplier which warns when + * one enables virtual thread support + */ +class BoundedElasticSchedulerSupplier implements Supplier { + + static final Logger logger = Loggers.getLogger(BoundedElasticSchedulerSupplier.class); + + @Override + public Scheduler get() { + if (DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS) { + logger.warn( + "Virtual Threads support is not available on the given JVM. Falling back to default BoundedElastic setup"); + } + + return newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, + DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + BOUNDED_ELASTIC, + BoundedElasticScheduler.DEFAULT_TTL_SECONDS, + true); + } +} diff --git a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java index 1e9e39253a..632f78bc6c 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,6 +104,19 @@ public abstract class Schedulers { .map(Integer::parseInt) .orElse(100000); + /** + * Default execution of enqueued tasks on {@link Thread#ofVirtual} for the global + * {@link #boundedElastic()} {@link Scheduler}, + * initialized by system property {@code reactor.schedulers.defaultBoundedElasticOnVirtualThreads} + * and falls back to false . + * + * @see #boundedElastic() + */ + public static final boolean DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS = + Optional.ofNullable(System.getProperty("reactor.schedulers.defaultBoundedElasticOnVirtualThreads")) + .map(Boolean::parseBoolean) + .orElse(false); + /** * Create a {@link Scheduler} which uses a backing {@link Executor} to schedule * Runnables for async operators. @@ -1060,6 +1073,7 @@ public void dispose() { // Internals static final String BOUNDED_ELASTIC = "boundedElastic"; // Blocking stuff with scale to zero + static final String LOOM_BOUNDED_ELASTIC = "loomBoundedElastic"; // Loom stuff static final String PARALLEL = "parallel"; //scale up common tasks static final String SINGLE = "single"; //non blocking tasks static final String IMMEDIATE = "immediate"; @@ -1072,9 +1086,7 @@ public void dispose() { static AtomicReference CACHED_PARALLEL = new AtomicReference<>(); static AtomicReference CACHED_SINGLE = new AtomicReference<>(); - static final Supplier BOUNDED_ELASTIC_SUPPLIER = - () -> newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, - BOUNDED_ELASTIC, BoundedElasticScheduler.DEFAULT_TTL_SECONDS, true); + static final Supplier BOUNDED_ELASTIC_SUPPLIER = new BoundedElasticSchedulerSupplier(); static final Supplier PARALLEL_SUPPLIER = () -> newParallel(PARALLEL, DEFAULT_POOL_SIZE, true); diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java new file mode 100644 index 0000000000..e3d621e39d --- /dev/null +++ b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import java.util.function.Supplier; + +import static reactor.core.scheduler.Schedulers.BOUNDED_ELASTIC; +import static reactor.core.scheduler.Schedulers.DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS; +import static reactor.core.scheduler.Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE; +import static reactor.core.scheduler.Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE; +import static reactor.core.scheduler.Schedulers.LOOM_BOUNDED_ELASTIC; +import static reactor.core.scheduler.Schedulers.newBoundedElastic; + +/** + * JDK 8 Specific implementation of BoundedElasticScheduler supplier which uses + * {@link java.lang.ThreadBuilders.VirtualThreadFactory} instead of the default + * {@link ReactorThreadFactory} when one enables virtual thread support + */ +class BoundedElasticSchedulerSupplier implements Supplier { + + @Override + public Scheduler get() { + if (DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS) { + return newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, + DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + Thread.ofVirtual() + .name(LOOM_BOUNDED_ELASTIC + "-", 1) + .uncaughtExceptionHandler(Schedulers::defaultUncaughtException) + .factory(), + BoundedElasticScheduler.DEFAULT_TTL_SECONDS); + } + return newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, + DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + BOUNDED_ELASTIC, + BoundedElasticScheduler.DEFAULT_TTL_SECONDS, + true); + } +} diff --git a/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxTest.java b/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxTest.java index 6e4b2a01cb..6883101514 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/ParallelFluxTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -530,7 +530,7 @@ public void fromFuseableUsesThreadBarrier() { .startsWith("single-"); assertThat(processing.keySet()) - .allSatisfy(k -> assertThat(k).startsWith("boundedElastic-")); + .allSatisfy(k -> assertThat(k).containsIgnoringCase("boundedElastic-")); } @Test From 997158f8bd0fc2479f1ba660343e8372c573e63e Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 6 Jul 2023 19:45:45 +0300 Subject: [PATCH 148/312] provides minimal troubleshooting for mrj (#3532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dariusz Jędrzejczyk --- README.md | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index affe6f2cf5..5e2d62882c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ dependencies { // testCompile "io.projectreactor:reactor-test:3.5.8-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.7" + // implementation "io.projectreactor:reactor-tools:3.5.8-SNAPSHOT" } ``` @@ -51,21 +51,43 @@ However it should work fine with Android SDK 21 (Android 5.0) and above. See the [complete note](https://projectreactor.io/docs/core/release/reference/docs/index.html#prerequisites) in the reference guide. -## Trouble importing the project in IDE? -Since the introduction of Java 9 stubs in order to optimize the performance of debug tracebacks, one can sometimes -encounter cryptic messages from the build system when importing or re-importing the project in their IDE. +## Trouble building the project? +Since the introduction of [Java Multi-Release JAR File](https://openjdk.org/jeps/238) +support one needs to have JDK 8, 9, and 21 available on the classpath. All the JDKs should +be automatically [detected](https://docs.gradle.org/current/userguide/toolchains.html#sec:auto_detection) +or [provisioned](https://docs.gradle.org/current/userguide/toolchains.html#sec:provisioning) +by Gradle Toolchain. -For example: +However, if you see error message such as `No matching toolchains found for requested +specification: {languageVersion=X, vendor=any, implementation=vendor-specific}` (where +`X` can be 8, 9 or 21), it means that you need to install the missing JDK: - - `package StackWalker does not exist`: probably building under JDK8 but `java9stubs` was not added to sources - - `cannot find symbol @CallerSensitive`: probably building with JDK11+ and importing using JDK8 +### Installing JDKs with [SDKMAN!](https://sdkman.io/) -When encountering these issues, one need to ensure that: +In the project root folder run [SDKMAN env initialization](https://sdkman.io/usage#env): - - Gradle JVM matches the JDK used by the IDE for the modules (in IntelliJ, `Modules Settings` JDK). Preferably, 1.8. - - The IDE is configured to delegate build to Gradle (in IntelliJ: `Build Tools > Gradle > Runner` and project setting uses that default) - -Then rebuild the project and the errors should disappear. +```shell +sdk env install +``` + +then (if needed) install JDK 9: + +```shell +sdk install java $(sdk list java | grep -Eo -m1 '9\b\.[ea|0-9]{1,2}\.[0-9]{1,2}-open') +``` + +then (if needed) install JDK 21: + +```shell +sdk install java $(sdk list java | grep -Eo -m1 '21\b\.[ea|0-9]{1,2}\.[0-9]{1,2}-open') +``` + +When the installations succeed, try to refresh the project and see that it builds. + +### Installing JDKs manually + +The manual Operation-system specific JDK installation +is well explained in the [official docs](https://docs.oracle.com/en/java/javase/20/install/overview-jdk-installation.html) ## Getting Started From 5358e4dae479402064ce46aeb278a9ae2dd012e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 7 Jul 2023 12:40:12 +0200 Subject: [PATCH 149/312] Add tests for context values with scope semantics (#3516) Automatic context propagation is now tested in scenarios that involve scope semantics using test classes from the context-propagation project. --- .../java/io/micrometer/scopedvalue/Scope.java | 67 +++++ .../micrometer/scopedvalue/ScopeHolder.java | 49 ++++ .../micrometer/scopedvalue/ScopedValue.java | 66 +++++ .../ScopedValueThreadLocalAccessor.java | 81 ++++++ .../ContextPropagationWithScopesTest.java | 268 ++++++++++++++++++ 5 files changed, 531 insertions(+) create mode 100644 reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/Scope.java create mode 100644 reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopeHolder.java create mode 100644 reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopedValue.java create mode 100644 reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopedValueThreadLocalAccessor.java create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationWithScopesTest.java diff --git a/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/Scope.java b/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/Scope.java new file mode 100644 index 0000000000..6f3eba78fa --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/Scope.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.micrometer.scopedvalue; + +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; + +// NOTE: This is a copy from the context-propagation library. Any changes should be +// considered in the upstream first. Please keep in sync. + +/** + * Represents a scope in which a {@link ScopedValue} is set for a particular Thread and + * maintains a hierarchy between this instance and the parent. + */ +public class Scope implements AutoCloseable { + + private static final Logger log = Logger.getLogger(Scope.class.getName()); + + final ScopedValue scopedValue; + + final Scope parentScope; + + private Scope(ScopedValue scopedValue, Scope parentScope) { + log.log(INFO, () -> String.format("%s: open scope[%s]", scopedValue.get(), hashCode())); + this.scopedValue = scopedValue; + this.parentScope = parentScope; + } + + /** + * Create a new scope and set the value for this Thread. + * @return newly created {@link Scope} + */ + public static Scope open(ScopedValue value) { + Scope scope = new Scope(value, ScopeHolder.get()); + ScopeHolder.set(scope); + return scope; + } + + @Override + public void close() { + if (parentScope == null) { + log.log(INFO, () -> String.format("%s: remove scope[%s]", scopedValue.get(), hashCode())); + ScopeHolder.remove(); + } + else { + log.log(INFO, () -> String.format("%s: close scope[%s] -> restore %s scope[%s]", scopedValue.get(), + hashCode(), parentScope.scopedValue.get(), parentScope.hashCode())); + ScopeHolder.set(parentScope); + } + } + +} diff --git a/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopeHolder.java b/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopeHolder.java new file mode 100644 index 0000000000..ab6179204d --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopeHolder.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.micrometer.scopedvalue; + +import org.assertj.core.util.VisibleForTesting; + +// NOTE: This is a copy from the context-propagation library. Any changes should be +// considered in the upstream first. Please keep in sync. + +/** + * Thread-local storage for the current value in scope for the current Thread. + */ +public class ScopeHolder { + + private static final ThreadLocal SCOPE = new ThreadLocal<>(); + + public static ScopedValue currentValue() { + Scope scope = SCOPE.get(); + return scope == null ? null : scope.scopedValue; + } + + public static Scope get() { + return SCOPE.get(); + } + + static void set(Scope scope) { + SCOPE.set(scope); + } + + @VisibleForTesting + public static void remove() { + SCOPE.remove(); + } + +} diff --git a/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopedValue.java b/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopedValue.java new file mode 100644 index 0000000000..7b534ff734 --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopedValue.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.micrometer.scopedvalue; + +import java.util.Objects; + +// NOTE: This is a copy from the context-propagation library. Any changes should be +// considered in the upstream first. Please keep in sync. + +/** + * Serves as an abstraction of a value which can be in the current Thread-local + * {@link Scope scope} that maintains the hierarchy between the parent a new scope with + * potentially a different value. + * + * @author Dariusz Jędrzejczyk + */ +public class ScopedValue { + + private final String value; + + private ScopedValue(String value) { + this.value = value; + } + + /** + * Creates a new instance, which can be set in scope via {@link Scope#open()}. + * @param value {@code String} value associated with created {@link ScopedValue} + * @return new instance + */ + public static ScopedValue create(String value) { + Objects.requireNonNull(value, "value can't be null"); + return new ScopedValue(value); + } + + /** + * Creates a dummy instance used for nested scopes, in which the value should be + * virtually absent, but allows reverting to the previous value in scope. + * @return new instance representing an empty scope + */ + public static ScopedValue nullValue() { + return new ScopedValue(null); + } + + /** + * {@code String} value associated with this instance. + * @return associated value + */ + public String get() { + return value; + } + +} diff --git a/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopedValueThreadLocalAccessor.java b/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopedValueThreadLocalAccessor.java new file mode 100644 index 0000000000..f541bdb44c --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/io/micrometer/scopedvalue/ScopedValueThreadLocalAccessor.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.micrometer.scopedvalue; + +import io.micrometer.context.ThreadLocalAccessor; + +// NOTE: This is a copy from the context-propagation library. Any changes should be +// considered in the upstream first. Please keep in sync. + +/** + * Accessor for {@link ScopedValue}. + * + * @author Dariusz Jędrzejczyk + */ +public class ScopedValueThreadLocalAccessor implements ThreadLocalAccessor { + + /** + * The key used for registrations in {@link io.micrometer.context.ContextRegistry}. + */ + public static final String KEY = "svtla"; + + @Override + public Object key() { + return KEY; + } + + @Override + public ScopedValue getValue() { + return ScopeHolder.currentValue(); + } + + @Override + public void setValue(ScopedValue value) { + Scope.open(value); + } + + @Override + public void setValue() { + Scope.open(ScopedValue.nullValue()); + } + + @Override + public void restore(ScopedValue previousValue) { + Scope currentScope = ScopeHolder.get(); + if (currentScope != null) { + if (currentScope.parentScope == null || currentScope.parentScope.scopedValue != previousValue) { + throw new RuntimeException("Restoring to a different previous scope than expected!"); + } + currentScope.close(); + } + else { + throw new RuntimeException("Restoring to previous scope, but current is missing."); + } + } + + @Override + public void restore() { + Scope currentScope = ScopeHolder.get(); + if (currentScope != null) { + currentScope.close(); + } + else { + throw new RuntimeException("Restoring to previous scope, but current is missing."); + } + } + +} diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationWithScopesTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationWithScopesTest.java new file mode 100644 index 0000000000..1c96a95927 --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationWithScopesTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicReference; + +import io.micrometer.context.ContextRegistry; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.scheduler.Schedulers; +import reactor.util.context.Context; +import io.micrometer.scopedvalue.Scope; +import io.micrometer.scopedvalue.ScopeHolder; +import io.micrometer.scopedvalue.ScopedValue; +import io.micrometer.scopedvalue.ScopedValueThreadLocalAccessor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dariusz Jędrzejczyk + */ +class ContextPropagationWithScopesTest { + + @BeforeAll + static void initializeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + globalRegistry.registerThreadLocalAccessor(new ScopedValueThreadLocalAccessor()); + } + + @BeforeEach + void enableHook() { + Hooks.enableAutomaticContextPropagation(); + } + + @AfterEach + void cleanupThreadLocals() { + ScopeHolder.remove(); + Hooks.disableAutomaticContextPropagation(); + } + + @AfterAll + static void removeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + globalRegistry.removeThreadLocalAccessor(ScopedValueThreadLocalAccessor.KEY); + } + + @Test + void basicMonoWorks() { + ScopedValue scopedValue = ScopedValue.create("hello"); + + Mono.just("item") + .doOnNext(item -> assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue)) + .contextWrite(Context.of(ScopedValueThreadLocalAccessor.KEY, scopedValue)) + .block(); + + assertThat(ScopeHolder.currentValue()).isNull(); + } + + @Test + void basicFluxWorks() { + ScopedValue scopedValue = ScopedValue.create("hello"); + + Flux.just("item") + .doOnNext(item -> assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue)) + .contextWrite(Context.of(ScopedValueThreadLocalAccessor.KEY, scopedValue)) + .blockLast(); + + assertThat(ScopeHolder.currentValue()).isNull(); + } + + @Test + void emptyContextWorksInMono() { + ScopedValue scopedValue = ScopedValue.create("hello"); + try (Scope scope = Scope.open(scopedValue)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); + + Mono.just("item") + .doOnNext(item -> assertThat(ScopeHolder.currentValue().get()).isNull()) + .contextWrite(ctx -> Context.empty()) + .block(); + + assertThat(ScopeHolder.currentValue()).isEqualTo(scopedValue); + } + + assertThat(ScopeHolder.currentValue()).isNull(); + } + + @Test + void subscribeMonoElsewhere() { + AtomicReference valueInNewThread = new AtomicReference<>(); + + ScopedValue externalValue = ScopedValue.create("outside"); + ScopedValue internalValue = ScopedValue.create("inside"); + + try (Scope scope = Scope.open(externalValue)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(externalValue); + + Mono.just(1) + .subscribeOn(Schedulers.single()) + .doOnNext(i -> { + valueInNewThread.set(ScopeHolder.currentValue()); + }) + .contextWrite(Context.of(ScopedValueThreadLocalAccessor.KEY, internalValue)) + .block(); + + assertThat(valueInNewThread.get()).isEqualTo(internalValue); + assertThat(ScopeHolder.currentValue()).isEqualTo(externalValue); + } + + assertThat(ScopeHolder.currentValue()).isEqualTo(null); + } + + @Test + void subscribeFluxElsewhere() { + AtomicReference valueInNewThread = new AtomicReference<>(); + + ScopedValue externalValue = ScopedValue.create("outside"); + ScopedValue internalValue = ScopedValue.create("inside"); + + try (Scope scope = Scope.open(externalValue)) { + assertThat(ScopeHolder.currentValue()).isEqualTo(externalValue); + + Flux.just(1) + .subscribeOn(Schedulers.single()) + .doOnNext(i -> { + valueInNewThread.set(ScopeHolder.currentValue()); + }) + .contextWrite(Context.of(ScopedValueThreadLocalAccessor.KEY, internalValue)) + .blockLast(); + + assertThat(valueInNewThread.get()).isEqualTo(internalValue); + assertThat(ScopeHolder.currentValue()).isEqualTo(externalValue); + } + + assertThat(ScopeHolder.currentValue()).isEqualTo(null); + } + + @Test + void multiLevelScopesWithDifferentValuesAndFlux() { + ScopedValue v1 = ScopedValue.create("val1"); + ScopedValue v2 = ScopedValue.create("val2"); + + AtomicReference valueInsideFlatMap = new AtomicReference<>(); + + try (Scope v1scope1 = Scope.open(v1)) { + try (Scope v1scope2 = Scope.open(v1)) { + try (Scope v2scope1 = Scope.open(v2)) { + try (Scope v2scope2 = Scope.open(v2)) { + try (Scope v1scope3 = Scope.open(v1)) { + try (Scope nullScope = Scope.open(ScopedValue.nullValue())) { + assertThat(ScopeHolder.currentValue().get()).isNull(); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope3); + + Flux.just(1) + .flatMap(i -> + Flux.just(i) + .publishOn(Schedulers.boundedElastic()) + .doOnNext(item -> valueInsideFlatMap.set(ScopeHolder.currentValue()))) + .blockLast(); + + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope3); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(v2scope2); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(v2scope1); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope2); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope1); + } + + assertThat(ScopeHolder.currentValue()).isNull(); + + assertThat(valueInsideFlatMap.get()).isEqualTo(v1); + } + + @Test + void multiLevelScopesWithDifferentValuesAndMono() { + ScopedValue v1 = ScopedValue.create("val1"); + ScopedValue v2 = ScopedValue.create("val2"); + + AtomicReference valueInsideFlatMap = new AtomicReference<>(); + + try (Scope v1scope1 = Scope.open(v1)) { + try (Scope v1scope2 = Scope.open(v1)) { + try (Scope v2scope1 = Scope.open(v2)) { + try (Scope v2scope2 = Scope.open(v2)) { + try (Scope v1scope3 = Scope.open(v1)) { + try (Scope nullScope = Scope.open(ScopedValue.nullValue())) { + assertThat(ScopeHolder.currentValue().get()).isNull(); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope3); + + Mono.just(1) + .flatMap(i -> + Mono.just(i) + .publishOn(Schedulers.boundedElastic()) + .doOnNext(item -> valueInsideFlatMap.set(ScopeHolder.currentValue()))) + .block(); + + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope3); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(v2scope2); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v2); + assertThat(ScopeHolder.get()).isEqualTo(v2scope1); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope2); + } + assertThat(ScopeHolder.currentValue()).isEqualTo(v1); + assertThat(ScopeHolder.get()).isEqualTo(v1scope1); + } + + assertThat(ScopeHolder.currentValue()).isNull(); + + assertThat(valueInsideFlatMap.get()).isEqualTo(v1); + } + + @Test + void parallelFlatMapFlux() { + ScopedValue outerValue = ScopedValue.create("val1"); + + Queue innerValues = new ArrayBlockingQueue<>(3); + + Flux.just(0, 1, 2).hide() + .flatMap(i -> Flux.just(i).subscribeOn(Schedulers.parallel()) + .doOnNext(item -> { + innerValues.offer(ScopeHolder.currentValue().get()); + }) + .contextWrite(Context.of(ScopedValueThreadLocalAccessor.KEY, ScopedValue.create("item_" + i))), + 3, 1) + .contextWrite(Context.of(ScopedValueThreadLocalAccessor.KEY, outerValue)) + .blockLast(); + + assertThat(innerValues).containsExactlyInAnyOrder("item_0", "item_1", "item_2"); + assertThat(ScopeHolder.get()).isNull(); + } +} From de89eb835debae22cd048b66c6e293f2a59ee9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 10 Jul 2023 13:32:25 +0200 Subject: [PATCH 150/312] extracts automatic context propagation tests into a separate class (#3534) * Extracted automatic context propagation tests to another class * Added tests for other doOn operators --- .../AutomaticContextPropagationTest.java | 595 ++++++++++++++++++ .../publisher/ContextPropagationTest.java | 578 ----------------- 2 files changed, 595 insertions(+), 578 deletions(-) create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java new file mode 100644 index 0000000000..00736448ee --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java @@ -0,0 +1,595 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.micrometer.context.ContextRegistry; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.scheduler.Schedulers; +import reactor.test.publisher.TestPublisher; +import reactor.test.subscriber.TestSubscriber; +import reactor.util.concurrent.Queues; +import reactor.util.context.Context; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AutomaticContextPropagationTest { + + private static final String KEY = "ContextPropagationTest.key"; + private static final ThreadLocal REF = ThreadLocal.withInitial(() -> "ref_init"); + + @BeforeAll + static void initializeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + globalRegistry.registerThreadLocalAccessor(KEY, REF); + } + + @BeforeEach + void enableAutomaticContextPropagation() { + Hooks.enableAutomaticContextPropagation(); + // Disabling is done by ReactorTestExecutionListener + } + + @AfterEach + void cleanupThreadLocals() { + REF.remove(); + } + + @AfterAll + static void removeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + globalRegistry.removeThreadLocalAccessor(KEY); + } + + @Test + void threadLocalsPresentAfterSubscribeOn() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doOnNext(i -> tlValue.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentAfterPublishOn() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .publishOn(Schedulers.boundedElastic()) + .doOnNext(i -> tlValue.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInFlatMap() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .flatMap(i -> Mono.just(i) + .doOnNext(j -> tlValue.set(REF.get()))) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentAfterDelay() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .delayElements(Duration.ofMillis(1)) + .doOnNext(i -> tlValue.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInDoOnSubscribe() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doOnSubscribe(s -> tlValue.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInDoOnEach() { + ArrayBlockingQueue threadLocals = new ArrayBlockingQueue<>(4); + Flux.just(1, 2, 3) + .doOnEach(s -> threadLocals.add(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(threadLocals).containsOnly("present", "present", "present", "present"); + } + + @Test + void threadLocalsPresentInDoOnRequest() { + AtomicReference tlValue1 = new AtomicReference<>(); + AtomicReference tlValue2 = new AtomicReference<>(); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doOnRequest(s -> tlValue1.set(REF.get())) + .publishOn(Schedulers.single()) + .doOnRequest(s -> tlValue2.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue1.get()).isEqualTo("present"); + assertThat(tlValue2.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInDoAfterTerminate() throws InterruptedException { + AtomicReference tlValue = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doAfterTerminate(() -> { + tlValue.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + // Need to synchronize, as the doAfterTerminate operator can race with the + // assertion. First, blockLast receives the completion signal, and only then, + // the callback is triggered. + latch.await(10, TimeUnit.MILLISECONDS); + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { + AtomicReference requestTlValue = new AtomicReference<>(); + AtomicReference subscribeTlValue = new AtomicReference<>(); + AtomicReference firstNextTlValue = new AtomicReference<>(); + AtomicReference secondNextTlValue = new AtomicReference<>(); + AtomicReference cancelTlValue = new AtomicReference<>(); + + CountDownLatch itemDelivered = new CountDownLatch(1); + CountDownLatch cancelled = new CountDownLatch(1); + + TestSubscriber subscriber = + TestSubscriber.builder().initialRequest(1).build(); + + REF.set("downstreamContext"); + + Flux.just(1, 2, 3) + .hide() + .doOnRequest(r -> requestTlValue.set(REF.get())) + .doOnNext(i -> firstNextTlValue.set(REF.get())) + .doOnSubscribe(s -> subscribeTlValue.set(REF.get())) + .doOnCancel(() -> { + cancelTlValue.set(REF.get()); + cancelled.countDown(); + }) + .delayElements(Duration.ofMillis(1)) + .contextWrite(Context.of(KEY, "upstreamContext")) + // disabling prefetching to observe cancellation + .publishOn(Schedulers.parallel(), 1) + .doOnNext(i -> { + System.out.println(REF.get()); + secondNextTlValue.set(REF.get()); + itemDelivered.countDown(); + }) + .subscribeOn(Schedulers.boundedElastic()) + .contextCapture() + .subscribe(subscriber); + + itemDelivered.await(); + + subscriber.cancel(); + + cancelled.await(); + + assertThat(requestTlValue.get()).isEqualTo("upstreamContext"); + assertThat(subscribeTlValue.get()).isEqualTo("upstreamContext"); + assertThat(firstNextTlValue.get()).isEqualTo("upstreamContext"); + assertThat(cancelTlValue.get()).isEqualTo("upstreamContext"); + assertThat(secondNextTlValue.get()).isEqualTo("downstreamContext"); + } + + @Test + void prefetchingShouldMaintainThreadLocals() { + // We validate streams of items above default prefetch size + // (max concurrency of flatMap == Queues.SMALL_BUFFER_SIZE == 256) + // are able to maintain the context propagation to ThreadLocals + // in the presence of prefetching + int size = Queues.SMALL_BUFFER_SIZE * 10; + + Flux source = Flux.create(s -> { + for (int i = 0; i < size; i++) { + s.next(i); + } + s.complete(); + }); + + assertThat(REF.get()).isEqualTo("ref_init"); + + ArrayBlockingQueue innerThreadLocals = new ArrayBlockingQueue<>(size); + ArrayBlockingQueue outerThreadLocals = new ArrayBlockingQueue<>(size); + + source.publishOn(Schedulers.boundedElastic()) + .flatMap(i -> Mono.just(i) + .delayElement(Duration.ofMillis(1)) + .doOnNext(j -> innerThreadLocals.add(REF.get()))) + .contextWrite(ctx -> ctx.put(KEY, "present")) + .publishOn(Schedulers.parallel()) + .doOnNext(i -> outerThreadLocals.add(REF.get())) + .blockLast(); + + assertThat(innerThreadLocals).containsOnly("present").hasSize(size); + assertThat(outerThreadLocals).containsOnly("ref_init").hasSize(size); + } + + @Test + void fluxApiUsesContextPropagationConstantFunction() { + Flux source = Flux.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(FluxContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("flux's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Test + void monoApiUsesContextPropagationConstantFunction() { + Mono source = Mono.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(MonoContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("mono's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE)); + } + + @Nested + class NonReactorSources { + @Test + void fluxFromPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Flux.from(nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.from(nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromPublisherIgnoringContract() + throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.fromDirect(nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromCompletionStage() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(); + + // we need to delay delivery to ensure the completion signal is delivered + // on a Thread from executorService + CompletionStage completionStage = CompletableFuture.supplyAsync(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + return "test"; + }, executorService); + + TestSubscriber testSubscriber = TestSubscriber.create(); + + Mono.fromCompletionStage(completionStage) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(testSubscriber); + + latch.countDown(); + testSubscriber.block(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromFuture() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(); + + // we need to delay delivery to ensure the completion signal is delivered + // on a Thread from executorService + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + return "test"; + }, executorService); + + TestSubscriber testSubscriber = TestSubscriber.create(); + + Mono.fromFuture(future) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(testSubscriber); + + latch.countDown(); + testSubscriber.block(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + } + + @Nested + class BlockingOperatorsAutoCapture { + + @Test + void monoBlock() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Mono.just("test") + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnNext(ignored -> value.set(REF.get())) + .block(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void monoBlockOptional() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Mono.empty() + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF.get())) + .blockOptional(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxBlockFirst() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnNext(ignored -> value.set(REF.get())) + .blockFirst(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxBlockLast() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF.get())) + .blockLast(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxToIterable() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Iterable integers = Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF.get())) + .toIterable(); + + assertThat(integers).hasSize(10); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + } +} diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index 5c5a594462..be06b14ad9 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -16,22 +16,10 @@ package reactor.core.publisher; -import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; @@ -41,7 +29,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.provider.EnumSource; @@ -57,17 +44,13 @@ import reactor.core.publisher.FluxHandle.HandleSubscriber; import reactor.core.publisher.FluxHandleFuseable.HandleFuseableConditionalSubscriber; import reactor.core.publisher.FluxHandleFuseable.HandleFuseableSubscriber; -import reactor.core.scheduler.Schedulers; import reactor.test.ParameterizedTestWithName; -import reactor.test.publisher.TestPublisher; import reactor.test.subscriber.TestSubscriber; import reactor.test.subscriber.TestSubscriberBuilder; -import reactor.util.concurrent.Queues; import reactor.util.context.Context; import reactor.util.context.ContextView; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Simon Baslé @@ -105,226 +88,6 @@ static void removeThreadLocalAccessors() { } - @Test - void threadLocalsPresentAfterSubscribeOn() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference tlValue = new AtomicReference<>(); - - Flux.just(1) - .subscribeOn(Schedulers.boundedElastic()) - .doOnNext(i -> tlValue.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(tlValue.get()).isEqualTo("present"); - } - - @Test - void threadLocalsPresentAfterPublishOn() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference tlValue = new AtomicReference<>(); - - Flux.just(1) - .publishOn(Schedulers.boundedElastic()) - .doOnNext(i -> tlValue.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(tlValue.get()).isEqualTo("present"); - } - - @Test - void threadLocalsPresentInFlatMap() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference tlValue = new AtomicReference<>(); - - Flux.just(1) - .flatMap(i -> Mono.just(i) - .doOnNext(j -> tlValue.set(REF1.get()))) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(tlValue.get()).isEqualTo("present"); - } - - @Test - void threadLocalsPresentAfterDelay() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference tlValue = new AtomicReference<>(); - - Flux.just(1) - .delayElements(Duration.ofMillis(1)) - .doOnNext(i -> tlValue.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(tlValue.get()).isEqualTo("present"); - } - - @Test - @Disabled("queue wrapping is removed starting from 3.5.7") - void threadLocalsRestoredAfterPollution() { - // this test validates Queue wrapping takes place - Hooks.enableAutomaticContextPropagation(); - ArrayBlockingQueue modifiedThreadLocals = new ArrayBlockingQueue<>(10); - ArrayBlockingQueue restoredThreadLocals = new ArrayBlockingQueue<>(10); - - Flux.range(0, 10) - .doOnNext(i -> { - REF1.set("i: " + i); - }) - .publishOn(Schedulers.parallel()) - // the validation below shows that modifications to TLs are propagated - // across thread boundaries via queue wrapping, so explicit control - // is required from users to clean up after such modifications - .doOnNext(i -> modifiedThreadLocals.add(REF1.get())) - .contextWrite(Function.identity()) - // the contextWrite above creates a barrier that ensures the downstream - // operator sees TLs from the subscriber context - .doOnNext(i -> restoredThreadLocals.add(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(modifiedThreadLocals).containsExactly( - "i: 0", "i: 1", "i: 2", "i: 3", "i: 4", - "i: 5", "i: 6", "i: 7", "i: 8", "i: 9" - ); - assertThat(restoredThreadLocals).containsExactly( - Collections.nCopies(10, "present").toArray(new String[] {}) - ); - } - - @Test - void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference requestTlValue = new AtomicReference<>(); - AtomicReference subscribeTlValue = new AtomicReference<>(); - AtomicReference firstNextTlValue = new AtomicReference<>(); - AtomicReference secondNextTlValue = new AtomicReference<>(); - AtomicReference cancelTlValue = new AtomicReference<>(); - - CountDownLatch itemDelivered = new CountDownLatch(1); - CountDownLatch cancelled = new CountDownLatch(1); - - TestSubscriber subscriber = - TestSubscriber.builder().initialRequest(1).build(); - - REF1.set("downstreamContext"); - - Flux.just(1, 2, 3) - .hide() - .doOnRequest(r -> requestTlValue.set(REF1.get())) - .doOnNext(i -> firstNextTlValue.set(REF1.get())) - .doOnSubscribe(s -> subscribeTlValue.set(REF1.get())) - .doOnCancel(() -> { - cancelTlValue.set(REF1.get()); - cancelled.countDown(); - }) - .delayElements(Duration.ofMillis(1)) - .contextWrite(Context.of(KEY1, "upstreamContext")) - // disabling prefetching to observe cancellation - .publishOn(Schedulers.parallel(), 1) - .doOnNext(i -> { - System.out.println(REF1.get()); - secondNextTlValue.set(REF1.get()); - itemDelivered.countDown(); - }) - .subscribeOn(Schedulers.boundedElastic()) - .contextCapture() - .subscribe(subscriber); - - itemDelivered.await(); - - subscriber.cancel(); - - cancelled.await(); - - assertThat(requestTlValue.get()).isEqualTo("upstreamContext"); - assertThat(subscribeTlValue.get()).isEqualTo("upstreamContext"); - assertThat(firstNextTlValue.get()).isEqualTo("upstreamContext"); - assertThat(cancelTlValue.get()).isEqualTo("upstreamContext"); - assertThat(secondNextTlValue.get()).isEqualTo("downstreamContext"); - } - - @Test - void prefetchingShouldMaintainThreadLocals() { - Hooks.enableAutomaticContextPropagation(); - - // We validate streams of items above default prefetch size - // (max concurrency of flatMap == Queues.SMALL_BUFFER_SIZE == 256) - // are able to maintain the context propagation to ThreadLocals - // in the presence of prefetching - int size = Queues.SMALL_BUFFER_SIZE * 10; - - Flux source = Flux.create(s -> { - for (int i = 0; i < size; i++) { - s.next(i); - } - s.complete(); - }); - - assertThat(REF1.get()).isEqualTo("ref1_init"); - - ArrayBlockingQueue innerThreadLocals = new ArrayBlockingQueue<>(size); - ArrayBlockingQueue outerThreadLocals = new ArrayBlockingQueue<>(size); - - source.publishOn(Schedulers.boundedElastic()) - .flatMap(i -> Mono.just(i) - .delayElement(Duration.ofMillis(1)) - .doOnNext(j -> innerThreadLocals.add(REF1.get()))) - .contextWrite(ctx -> ctx.put(KEY1, "present")) - .publishOn(Schedulers.parallel()) - .doOnNext(i -> outerThreadLocals.add(REF1.get())) - .blockLast(); - - assertThat(innerThreadLocals).containsOnly("present").hasSize(size); - assertThat(outerThreadLocals).containsOnly("ref1_init").hasSize(size); - } - - @Test - @Disabled("queue wrapping is removed starting from 3.5.7") - void queueWrapperWorksWithQueues() { - Hooks.enableAutomaticContextPropagation(); - Queue queue = Queues.small() - .get(); - - assertThat(queue.offer("1")).isTrue(); - assertThat(queue.poll()).isSameAs("1"); - assertThat(queue.add("2")).isTrue(); - assertThat(queue.remove()).isSameAs("2"); - assertThat(queue.isEmpty()).isTrue(); - assertThat(queue.addAll(Arrays.asList("3", "4", "5"))).isTrue(); - assertThat(queue.peek()).isSameAs("3"); - assertThat(queue.isEmpty()).isFalse(); - assertThat(queue.element()).isSameAs("3"); - assertThat(queue.size()).isEqualTo(3); - queue.clear(); - assertThat(queue.offer("0")).isTrue(); - assertThat(queue.size()).isEqualTo(1); - - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(queue::iterator); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.contains("0")); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(queue::toArray); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.toArray(new Object[] {})); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.remove("0")); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.containsAll(Collections.singletonList("0"))); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.retainAll(Collections.singletonList("5"))); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.removeAll(Collections.singletonList("0"))); - } - @Test void isContextPropagationAvailable() { assertThat(ContextPropagationSupport.isContextPropagationAvailable()).isTrue(); @@ -359,29 +122,6 @@ void monoApiUsesContextPropagationConstantFunction() { ); } - @Test - void fluxApiUsesContextPropagationConstantFunctionWhenAutomaticPropagationEnabled() { - Hooks.enableAutomaticContextPropagation(); - Flux source = Flux.empty(); - assertThat(source.contextCapture()) - .isInstanceOfSatisfying(FluxContextWriteRestoringThreadLocals.class, - fcw -> assertThat(fcw.doOnContext) - .as("flux's capture function") - .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) - ); - } - - @Test - void monoApiUsesContextPropagationConstantFunctionWhenAutomaticPropagationEnabled() { - Hooks.enableAutomaticContextPropagation(); - Mono source = Mono.empty(); - assertThat(source.contextCapture()) - .isInstanceOfSatisfying(MonoContextWriteRestoringThreadLocals.class, - fcw -> assertThat(fcw.doOnContext) - .as("mono's capture function") - .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE)); - } - @Nested class ContextCaptureFunctionTest { @@ -403,203 +143,6 @@ void contextCaptureFunctionWithoutFiltering() { } } - @Nested - class NonReactorSources { - @Test - void fluxFromPublisher() throws InterruptedException, ExecutionException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Hooks.enableAutomaticContextPropagation(); - AtomicReference value = new AtomicReference<>(); - - TestPublisher testPublisher = TestPublisher.create(); - Publisher nonReactorPublisher = testPublisher; - - Flux.from(nonReactorPublisher) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(); - - executorService - .submit(() -> testPublisher.emit("test").complete()) - .get(); - - testPublisher.assertWasSubscribed(); - testPublisher.assertWasNotCancelled(); - testPublisher.assertWasRequested(); - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - - @Test - void monoFromPublisher() throws InterruptedException, ExecutionException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Hooks.enableAutomaticContextPropagation(); - AtomicReference value = new AtomicReference<>(); - - TestPublisher testPublisher = TestPublisher.create(); - Publisher nonReactorPublisher = testPublisher; - - Mono.from(nonReactorPublisher) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(); - - executorService - .submit(() -> testPublisher.emit("test").complete()) - .get(); - - testPublisher.assertWasSubscribed(); - testPublisher.assertCancelled(); - testPublisher.assertWasRequested(); - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - - @Test - void monoFromPublisherIgnoringContract() - throws InterruptedException, ExecutionException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Hooks.enableAutomaticContextPropagation(); - AtomicReference value = new AtomicReference<>(); - - TestPublisher testPublisher = TestPublisher.create(); - Publisher nonReactorPublisher = testPublisher; - - Mono.fromDirect(nonReactorPublisher) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(); - - executorService - .submit(() -> testPublisher.emit("test").complete()) - .get(); - - testPublisher.assertWasSubscribed(); - testPublisher.assertWasNotCancelled(); - testPublisher.assertWasRequested(); - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - - @Test - void monoFromCompletionStage() throws ExecutionException, InterruptedException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - - Hooks.enableAutomaticContextPropagation(); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference value = new AtomicReference<>(); - - // we need to delay delivery to ensure the completion signal is delivered - // on a Thread from executorService - CompletionStage completionStage = CompletableFuture.supplyAsync(() -> { - try { - latch.await(); - } - catch (InterruptedException e) { - // ignore - } - return "test"; - }, executorService); - - TestSubscriber testSubscriber = TestSubscriber.create(); - - Mono.fromCompletionStage(completionStage) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(testSubscriber); - - latch.countDown(); - testSubscriber.block(); - - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - - @Test - void monoFromFuture() throws ExecutionException, InterruptedException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - - Hooks.enableAutomaticContextPropagation(); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference value = new AtomicReference<>(); - - // we need to delay delivery to ensure the completion signal is delivered - // on a Thread from executorService - CompletableFuture future = CompletableFuture.supplyAsync(() -> { - try { - latch.await(); - } - catch (InterruptedException e) { - // ignore - } - return "test"; - }, executorService); - - TestSubscriber testSubscriber = TestSubscriber.create(); - - Mono.fromFuture(future) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(testSubscriber); - - latch.countDown(); - testSubscriber.block(); - - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - } - // TAP AND HANDLE OPERATOR TESTS static private enum Cases { @@ -955,125 +498,4 @@ void monoHandleVariantsCallTheWrapper() { }); } } - - @Nested - class BlockingOperatorsAutoCapture { - - @Test - void monoBlock() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Mono.just("test") - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnNext(ignored -> value.set(REF1.get())) - .block(); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - - @Test - void monoBlockOptional() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Mono.empty() - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnTerminate(() -> value.set(REF1.get())) - .blockOptional(); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - - @Test - void fluxBlockFirst() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Flux.range(0, 10) - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnNext(ignored -> value.set(REF1.get())) - .blockFirst(); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - - @Test - void fluxBlockLast() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Flux.range(0, 10) - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnTerminate(() -> value.set(REF1.get())) - .blockLast(); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - - @Test - void fluxToIterable() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Iterable integers = Flux.range(0, 10) - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnTerminate(() -> value.set(REF1.get())) - .toIterable(); - - assertThat(integers).hasSize(10); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - } } From 97054e5816fd8c4d5e5d549bbf80ea6b3c390313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 10 Jul 2023 13:32:25 +0200 Subject: [PATCH 151/312] extracts automatic context propagation tests into a separate class (#3534) * Extracted automatic context propagation tests to another class * Added tests for other doOn operators --- .../AutomaticContextPropagationTest.java | 595 ++++++++++++++++++ .../publisher/ContextPropagationTest.java | 578 ----------------- 2 files changed, 595 insertions(+), 578 deletions(-) create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java new file mode 100644 index 0000000000..00736448ee --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java @@ -0,0 +1,595 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.micrometer.context.ContextRegistry; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.scheduler.Schedulers; +import reactor.test.publisher.TestPublisher; +import reactor.test.subscriber.TestSubscriber; +import reactor.util.concurrent.Queues; +import reactor.util.context.Context; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AutomaticContextPropagationTest { + + private static final String KEY = "ContextPropagationTest.key"; + private static final ThreadLocal REF = ThreadLocal.withInitial(() -> "ref_init"); + + @BeforeAll + static void initializeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + globalRegistry.registerThreadLocalAccessor(KEY, REF); + } + + @BeforeEach + void enableAutomaticContextPropagation() { + Hooks.enableAutomaticContextPropagation(); + // Disabling is done by ReactorTestExecutionListener + } + + @AfterEach + void cleanupThreadLocals() { + REF.remove(); + } + + @AfterAll + static void removeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + globalRegistry.removeThreadLocalAccessor(KEY); + } + + @Test + void threadLocalsPresentAfterSubscribeOn() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doOnNext(i -> tlValue.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentAfterPublishOn() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .publishOn(Schedulers.boundedElastic()) + .doOnNext(i -> tlValue.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInFlatMap() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .flatMap(i -> Mono.just(i) + .doOnNext(j -> tlValue.set(REF.get()))) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentAfterDelay() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .delayElements(Duration.ofMillis(1)) + .doOnNext(i -> tlValue.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInDoOnSubscribe() { + AtomicReference tlValue = new AtomicReference<>(); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doOnSubscribe(s -> tlValue.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInDoOnEach() { + ArrayBlockingQueue threadLocals = new ArrayBlockingQueue<>(4); + Flux.just(1, 2, 3) + .doOnEach(s -> threadLocals.add(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(threadLocals).containsOnly("present", "present", "present", "present"); + } + + @Test + void threadLocalsPresentInDoOnRequest() { + AtomicReference tlValue1 = new AtomicReference<>(); + AtomicReference tlValue2 = new AtomicReference<>(); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doOnRequest(s -> tlValue1.set(REF.get())) + .publishOn(Schedulers.single()) + .doOnRequest(s -> tlValue2.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(tlValue1.get()).isEqualTo("present"); + assertThat(tlValue2.get()).isEqualTo("present"); + } + + @Test + void threadLocalsPresentInDoAfterTerminate() throws InterruptedException { + AtomicReference tlValue = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Flux.just(1) + .subscribeOn(Schedulers.boundedElastic()) + .doAfterTerminate(() -> { + tlValue.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + // Need to synchronize, as the doAfterTerminate operator can race with the + // assertion. First, blockLast receives the completion signal, and only then, + // the callback is triggered. + latch.await(10, TimeUnit.MILLISECONDS); + assertThat(tlValue.get()).isEqualTo("present"); + } + + @Test + void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { + AtomicReference requestTlValue = new AtomicReference<>(); + AtomicReference subscribeTlValue = new AtomicReference<>(); + AtomicReference firstNextTlValue = new AtomicReference<>(); + AtomicReference secondNextTlValue = new AtomicReference<>(); + AtomicReference cancelTlValue = new AtomicReference<>(); + + CountDownLatch itemDelivered = new CountDownLatch(1); + CountDownLatch cancelled = new CountDownLatch(1); + + TestSubscriber subscriber = + TestSubscriber.builder().initialRequest(1).build(); + + REF.set("downstreamContext"); + + Flux.just(1, 2, 3) + .hide() + .doOnRequest(r -> requestTlValue.set(REF.get())) + .doOnNext(i -> firstNextTlValue.set(REF.get())) + .doOnSubscribe(s -> subscribeTlValue.set(REF.get())) + .doOnCancel(() -> { + cancelTlValue.set(REF.get()); + cancelled.countDown(); + }) + .delayElements(Duration.ofMillis(1)) + .contextWrite(Context.of(KEY, "upstreamContext")) + // disabling prefetching to observe cancellation + .publishOn(Schedulers.parallel(), 1) + .doOnNext(i -> { + System.out.println(REF.get()); + secondNextTlValue.set(REF.get()); + itemDelivered.countDown(); + }) + .subscribeOn(Schedulers.boundedElastic()) + .contextCapture() + .subscribe(subscriber); + + itemDelivered.await(); + + subscriber.cancel(); + + cancelled.await(); + + assertThat(requestTlValue.get()).isEqualTo("upstreamContext"); + assertThat(subscribeTlValue.get()).isEqualTo("upstreamContext"); + assertThat(firstNextTlValue.get()).isEqualTo("upstreamContext"); + assertThat(cancelTlValue.get()).isEqualTo("upstreamContext"); + assertThat(secondNextTlValue.get()).isEqualTo("downstreamContext"); + } + + @Test + void prefetchingShouldMaintainThreadLocals() { + // We validate streams of items above default prefetch size + // (max concurrency of flatMap == Queues.SMALL_BUFFER_SIZE == 256) + // are able to maintain the context propagation to ThreadLocals + // in the presence of prefetching + int size = Queues.SMALL_BUFFER_SIZE * 10; + + Flux source = Flux.create(s -> { + for (int i = 0; i < size; i++) { + s.next(i); + } + s.complete(); + }); + + assertThat(REF.get()).isEqualTo("ref_init"); + + ArrayBlockingQueue innerThreadLocals = new ArrayBlockingQueue<>(size); + ArrayBlockingQueue outerThreadLocals = new ArrayBlockingQueue<>(size); + + source.publishOn(Schedulers.boundedElastic()) + .flatMap(i -> Mono.just(i) + .delayElement(Duration.ofMillis(1)) + .doOnNext(j -> innerThreadLocals.add(REF.get()))) + .contextWrite(ctx -> ctx.put(KEY, "present")) + .publishOn(Schedulers.parallel()) + .doOnNext(i -> outerThreadLocals.add(REF.get())) + .blockLast(); + + assertThat(innerThreadLocals).containsOnly("present").hasSize(size); + assertThat(outerThreadLocals).containsOnly("ref_init").hasSize(size); + } + + @Test + void fluxApiUsesContextPropagationConstantFunction() { + Flux source = Flux.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(FluxContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("flux's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Test + void monoApiUsesContextPropagationConstantFunction() { + Mono source = Mono.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(MonoContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("mono's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE)); + } + + @Nested + class NonReactorSources { + @Test + void fluxFromPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Flux.from(nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.from(nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromPublisherIgnoringContract() + throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.fromDirect(nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromCompletionStage() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(); + + // we need to delay delivery to ensure the completion signal is delivered + // on a Thread from executorService + CompletionStage completionStage = CompletableFuture.supplyAsync(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + return "test"; + }, executorService); + + TestSubscriber testSubscriber = TestSubscriber.create(); + + Mono.fromCompletionStage(completionStage) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(testSubscriber); + + latch.countDown(); + testSubscriber.block(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void monoFromFuture() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(); + + // we need to delay delivery to ensure the completion signal is delivered + // on a Thread from executorService + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + return "test"; + }, executorService); + + TestSubscriber testSubscriber = TestSubscriber.create(); + + Mono.fromFuture(future) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(testSubscriber); + + latch.countDown(); + testSubscriber.block(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + } + + @Nested + class BlockingOperatorsAutoCapture { + + @Test + void monoBlock() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Mono.just("test") + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnNext(ignored -> value.set(REF.get())) + .block(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void monoBlockOptional() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Mono.empty() + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF.get())) + .blockOptional(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxBlockFirst() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnNext(ignored -> value.set(REF.get())) + .blockFirst(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxBlockLast() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF.get())) + .blockLast(); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void fluxToIterable() { + AtomicReference value = new AtomicReference<>(); + + REF.set("present"); + + Iterable integers = Flux.range(0, 10) + // Introduce an artificial barrier to clear ThreadLocals if no Context + // is defined in the downstream chain. If block does the job well, + // it should have captured the existing ThreadLocal into the Context. + .contextWrite(Context.empty()) + .doOnTerminate(() -> value.set(REF.get())) + .toIterable(); + + assertThat(integers).hasSize(10); + + // First, assert the existing ThreadLocal was not cleared. + assertThat(REF.get()).isEqualTo("present"); + + // Now let's find out that it was automatically transferred. + assertThat(value.get()).isEqualTo("present"); + } + } +} diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java index 5c5a594462..be06b14ad9 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ContextPropagationTest.java @@ -16,22 +16,10 @@ package reactor.core.publisher; -import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; @@ -41,7 +29,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.provider.EnumSource; @@ -57,17 +44,13 @@ import reactor.core.publisher.FluxHandle.HandleSubscriber; import reactor.core.publisher.FluxHandleFuseable.HandleFuseableConditionalSubscriber; import reactor.core.publisher.FluxHandleFuseable.HandleFuseableSubscriber; -import reactor.core.scheduler.Schedulers; import reactor.test.ParameterizedTestWithName; -import reactor.test.publisher.TestPublisher; import reactor.test.subscriber.TestSubscriber; import reactor.test.subscriber.TestSubscriberBuilder; -import reactor.util.concurrent.Queues; import reactor.util.context.Context; import reactor.util.context.ContextView; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Simon Baslé @@ -105,226 +88,6 @@ static void removeThreadLocalAccessors() { } - @Test - void threadLocalsPresentAfterSubscribeOn() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference tlValue = new AtomicReference<>(); - - Flux.just(1) - .subscribeOn(Schedulers.boundedElastic()) - .doOnNext(i -> tlValue.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(tlValue.get()).isEqualTo("present"); - } - - @Test - void threadLocalsPresentAfterPublishOn() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference tlValue = new AtomicReference<>(); - - Flux.just(1) - .publishOn(Schedulers.boundedElastic()) - .doOnNext(i -> tlValue.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(tlValue.get()).isEqualTo("present"); - } - - @Test - void threadLocalsPresentInFlatMap() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference tlValue = new AtomicReference<>(); - - Flux.just(1) - .flatMap(i -> Mono.just(i) - .doOnNext(j -> tlValue.set(REF1.get()))) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(tlValue.get()).isEqualTo("present"); - } - - @Test - void threadLocalsPresentAfterDelay() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference tlValue = new AtomicReference<>(); - - Flux.just(1) - .delayElements(Duration.ofMillis(1)) - .doOnNext(i -> tlValue.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(tlValue.get()).isEqualTo("present"); - } - - @Test - @Disabled("queue wrapping is removed starting from 3.5.7") - void threadLocalsRestoredAfterPollution() { - // this test validates Queue wrapping takes place - Hooks.enableAutomaticContextPropagation(); - ArrayBlockingQueue modifiedThreadLocals = new ArrayBlockingQueue<>(10); - ArrayBlockingQueue restoredThreadLocals = new ArrayBlockingQueue<>(10); - - Flux.range(0, 10) - .doOnNext(i -> { - REF1.set("i: " + i); - }) - .publishOn(Schedulers.parallel()) - // the validation below shows that modifications to TLs are propagated - // across thread boundaries via queue wrapping, so explicit control - // is required from users to clean up after such modifications - .doOnNext(i -> modifiedThreadLocals.add(REF1.get())) - .contextWrite(Function.identity()) - // the contextWrite above creates a barrier that ensures the downstream - // operator sees TLs from the subscriber context - .doOnNext(i -> restoredThreadLocals.add(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .blockLast(); - - assertThat(modifiedThreadLocals).containsExactly( - "i: 0", "i: 1", "i: 2", "i: 3", "i: 4", - "i: 5", "i: 6", "i: 7", "i: 8", "i: 9" - ); - assertThat(restoredThreadLocals).containsExactly( - Collections.nCopies(10, "present").toArray(new String[] {}) - ); - } - - @Test - void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference requestTlValue = new AtomicReference<>(); - AtomicReference subscribeTlValue = new AtomicReference<>(); - AtomicReference firstNextTlValue = new AtomicReference<>(); - AtomicReference secondNextTlValue = new AtomicReference<>(); - AtomicReference cancelTlValue = new AtomicReference<>(); - - CountDownLatch itemDelivered = new CountDownLatch(1); - CountDownLatch cancelled = new CountDownLatch(1); - - TestSubscriber subscriber = - TestSubscriber.builder().initialRequest(1).build(); - - REF1.set("downstreamContext"); - - Flux.just(1, 2, 3) - .hide() - .doOnRequest(r -> requestTlValue.set(REF1.get())) - .doOnNext(i -> firstNextTlValue.set(REF1.get())) - .doOnSubscribe(s -> subscribeTlValue.set(REF1.get())) - .doOnCancel(() -> { - cancelTlValue.set(REF1.get()); - cancelled.countDown(); - }) - .delayElements(Duration.ofMillis(1)) - .contextWrite(Context.of(KEY1, "upstreamContext")) - // disabling prefetching to observe cancellation - .publishOn(Schedulers.parallel(), 1) - .doOnNext(i -> { - System.out.println(REF1.get()); - secondNextTlValue.set(REF1.get()); - itemDelivered.countDown(); - }) - .subscribeOn(Schedulers.boundedElastic()) - .contextCapture() - .subscribe(subscriber); - - itemDelivered.await(); - - subscriber.cancel(); - - cancelled.await(); - - assertThat(requestTlValue.get()).isEqualTo("upstreamContext"); - assertThat(subscribeTlValue.get()).isEqualTo("upstreamContext"); - assertThat(firstNextTlValue.get()).isEqualTo("upstreamContext"); - assertThat(cancelTlValue.get()).isEqualTo("upstreamContext"); - assertThat(secondNextTlValue.get()).isEqualTo("downstreamContext"); - } - - @Test - void prefetchingShouldMaintainThreadLocals() { - Hooks.enableAutomaticContextPropagation(); - - // We validate streams of items above default prefetch size - // (max concurrency of flatMap == Queues.SMALL_BUFFER_SIZE == 256) - // are able to maintain the context propagation to ThreadLocals - // in the presence of prefetching - int size = Queues.SMALL_BUFFER_SIZE * 10; - - Flux source = Flux.create(s -> { - for (int i = 0; i < size; i++) { - s.next(i); - } - s.complete(); - }); - - assertThat(REF1.get()).isEqualTo("ref1_init"); - - ArrayBlockingQueue innerThreadLocals = new ArrayBlockingQueue<>(size); - ArrayBlockingQueue outerThreadLocals = new ArrayBlockingQueue<>(size); - - source.publishOn(Schedulers.boundedElastic()) - .flatMap(i -> Mono.just(i) - .delayElement(Duration.ofMillis(1)) - .doOnNext(j -> innerThreadLocals.add(REF1.get()))) - .contextWrite(ctx -> ctx.put(KEY1, "present")) - .publishOn(Schedulers.parallel()) - .doOnNext(i -> outerThreadLocals.add(REF1.get())) - .blockLast(); - - assertThat(innerThreadLocals).containsOnly("present").hasSize(size); - assertThat(outerThreadLocals).containsOnly("ref1_init").hasSize(size); - } - - @Test - @Disabled("queue wrapping is removed starting from 3.5.7") - void queueWrapperWorksWithQueues() { - Hooks.enableAutomaticContextPropagation(); - Queue queue = Queues.small() - .get(); - - assertThat(queue.offer("1")).isTrue(); - assertThat(queue.poll()).isSameAs("1"); - assertThat(queue.add("2")).isTrue(); - assertThat(queue.remove()).isSameAs("2"); - assertThat(queue.isEmpty()).isTrue(); - assertThat(queue.addAll(Arrays.asList("3", "4", "5"))).isTrue(); - assertThat(queue.peek()).isSameAs("3"); - assertThat(queue.isEmpty()).isFalse(); - assertThat(queue.element()).isSameAs("3"); - assertThat(queue.size()).isEqualTo(3); - queue.clear(); - assertThat(queue.offer("0")).isTrue(); - assertThat(queue.size()).isEqualTo(1); - - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(queue::iterator); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.contains("0")); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(queue::toArray); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.toArray(new Object[] {})); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.remove("0")); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.containsAll(Collections.singletonList("0"))); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.retainAll(Collections.singletonList("5"))); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> queue.removeAll(Collections.singletonList("0"))); - } - @Test void isContextPropagationAvailable() { assertThat(ContextPropagationSupport.isContextPropagationAvailable()).isTrue(); @@ -359,29 +122,6 @@ void monoApiUsesContextPropagationConstantFunction() { ); } - @Test - void fluxApiUsesContextPropagationConstantFunctionWhenAutomaticPropagationEnabled() { - Hooks.enableAutomaticContextPropagation(); - Flux source = Flux.empty(); - assertThat(source.contextCapture()) - .isInstanceOfSatisfying(FluxContextWriteRestoringThreadLocals.class, - fcw -> assertThat(fcw.doOnContext) - .as("flux's capture function") - .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) - ); - } - - @Test - void monoApiUsesContextPropagationConstantFunctionWhenAutomaticPropagationEnabled() { - Hooks.enableAutomaticContextPropagation(); - Mono source = Mono.empty(); - assertThat(source.contextCapture()) - .isInstanceOfSatisfying(MonoContextWriteRestoringThreadLocals.class, - fcw -> assertThat(fcw.doOnContext) - .as("mono's capture function") - .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE)); - } - @Nested class ContextCaptureFunctionTest { @@ -403,203 +143,6 @@ void contextCaptureFunctionWithoutFiltering() { } } - @Nested - class NonReactorSources { - @Test - void fluxFromPublisher() throws InterruptedException, ExecutionException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Hooks.enableAutomaticContextPropagation(); - AtomicReference value = new AtomicReference<>(); - - TestPublisher testPublisher = TestPublisher.create(); - Publisher nonReactorPublisher = testPublisher; - - Flux.from(nonReactorPublisher) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(); - - executorService - .submit(() -> testPublisher.emit("test").complete()) - .get(); - - testPublisher.assertWasSubscribed(); - testPublisher.assertWasNotCancelled(); - testPublisher.assertWasRequested(); - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - - @Test - void monoFromPublisher() throws InterruptedException, ExecutionException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Hooks.enableAutomaticContextPropagation(); - AtomicReference value = new AtomicReference<>(); - - TestPublisher testPublisher = TestPublisher.create(); - Publisher nonReactorPublisher = testPublisher; - - Mono.from(nonReactorPublisher) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(); - - executorService - .submit(() -> testPublisher.emit("test").complete()) - .get(); - - testPublisher.assertWasSubscribed(); - testPublisher.assertCancelled(); - testPublisher.assertWasRequested(); - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - - @Test - void monoFromPublisherIgnoringContract() - throws InterruptedException, ExecutionException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Hooks.enableAutomaticContextPropagation(); - AtomicReference value = new AtomicReference<>(); - - TestPublisher testPublisher = TestPublisher.create(); - Publisher nonReactorPublisher = testPublisher; - - Mono.fromDirect(nonReactorPublisher) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(); - - executorService - .submit(() -> testPublisher.emit("test").complete()) - .get(); - - testPublisher.assertWasSubscribed(); - testPublisher.assertWasNotCancelled(); - testPublisher.assertWasRequested(); - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - - @Test - void monoFromCompletionStage() throws ExecutionException, InterruptedException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - - Hooks.enableAutomaticContextPropagation(); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference value = new AtomicReference<>(); - - // we need to delay delivery to ensure the completion signal is delivered - // on a Thread from executorService - CompletionStage completionStage = CompletableFuture.supplyAsync(() -> { - try { - latch.await(); - } - catch (InterruptedException e) { - // ignore - } - return "test"; - }, executorService); - - TestSubscriber testSubscriber = TestSubscriber.create(); - - Mono.fromCompletionStage(completionStage) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(testSubscriber); - - latch.countDown(); - testSubscriber.block(); - - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - - @Test - void monoFromFuture() throws ExecutionException, InterruptedException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - - Hooks.enableAutomaticContextPropagation(); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference value = new AtomicReference<>(); - - // we need to delay delivery to ensure the completion signal is delivered - // on a Thread from executorService - CompletableFuture future = CompletableFuture.supplyAsync(() -> { - try { - latch.await(); - } - catch (InterruptedException e) { - // ignore - } - return "test"; - }, executorService); - - TestSubscriber testSubscriber = TestSubscriber.create(); - - Mono.fromFuture(future) - .doOnNext(s -> value.set(REF1.get())) - .contextWrite(Context.of(KEY1, "present")) - .subscribe(testSubscriber); - - latch.countDown(); - testSubscriber.block(); - - assertThat(value.get()).isEqualTo("present"); - - // validate there are no leftovers for other tasks to be attributed to - // previous values - executorService.submit(() -> value.set(REF1.get())).get(); - - assertThat(value.get()).isEqualTo("ref1_init"); - - // validate the current Thread does not have the value set either - assertThat(REF1.get()).isEqualTo("ref1_init"); - - executorService.shutdownNow(); - } - } - // TAP AND HANDLE OPERATOR TESTS static private enum Cases { @@ -955,125 +498,4 @@ void monoHandleVariantsCallTheWrapper() { }); } } - - @Nested - class BlockingOperatorsAutoCapture { - - @Test - void monoBlock() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Mono.just("test") - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnNext(ignored -> value.set(REF1.get())) - .block(); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - - @Test - void monoBlockOptional() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Mono.empty() - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnTerminate(() -> value.set(REF1.get())) - .blockOptional(); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - - @Test - void fluxBlockFirst() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Flux.range(0, 10) - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnNext(ignored -> value.set(REF1.get())) - .blockFirst(); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - - @Test - void fluxBlockLast() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Flux.range(0, 10) - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnTerminate(() -> value.set(REF1.get())) - .blockLast(); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - - @Test - void fluxToIterable() { - Hooks.enableAutomaticContextPropagation(); - - AtomicReference value = new AtomicReference<>(); - - REF1.set("present"); - - Iterable integers = Flux.range(0, 10) - // Introduce an artificial barrier to clear ThreadLocals if no Context - // is defined in the downstream chain. If block does the job well, - // it should have captured the existing ThreadLocal into the Context. - .contextWrite(Context.empty()) - .doOnTerminate(() -> value.set(REF1.get())) - .toIterable(); - - assertThat(integers).hasSize(10); - - // First, assert the existing ThreadLocal was not cleared. - assertThat(REF1.get()).isEqualTo("present"); - - // Now let's find out that it was automatically transferred. - assertThat(value.get()).isEqualTo("present"); - } - } } From 322d707c6f37e7583697e1343baf704717b51ab3 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Tue, 11 Jul 2023 12:50:42 +0300 Subject: [PATCH 152/312] [release] Prepare and release 3.5.8 Signed-off-by: OlegDokuka --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index affe6f2cf5..3fab106cad 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.7" - testCompile "io.projectreactor:reactor-test:3.5.7" + compile "io.projectreactor:reactor-core:3.5.8" + testCompile "io.projectreactor:reactor-test:3.5.8" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.8-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.8-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.9-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.9-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.7" + // implementation "io.projectreactor:reactor-tools:3.5.8" } ``` diff --git a/gradle.properties b/gradle.properties index ac4dbaf4a1..3e007ca438 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.8-SNAPSHOT -bomVersion=2022.0.8 -metricsMicrometerVersion=1.0.8-SNAPSHOT +version=3.5.8 +bomVersion=2022.0.9 +metricsMicrometerVersion=1.0.8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a2a0d6f1d5..c5685b2f86 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.9-SNAPSHOT" +micrometer = "1.10.9" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -26,8 +26,8 @@ micrometer102Compatible-bom = { module = "io.micrometer:micrometer-bom", version micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.4-SNAPSHOT" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.4" +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.7" micrometer-test = { module = "io.micrometer:micrometer-test" } From 40e9df001cd8a0f8f432bdfda99b2c86c91353a0 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Tue, 11 Jul 2023 12:51:44 +0300 Subject: [PATCH 153/312] [release] Next development version 3.5.9-SNAPSHOT Signed-off-by: OlegDokuka --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3e007ca438..8cc34e84b9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.8 +version=3.5.9-SNAPSHOT bomVersion=2022.0.9 -metricsMicrometerVersion=1.0.8 +metricsMicrometerVersion=1.0.9-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c5685b2f86..7c7137a90f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.7" -baselinePerfCore = "3.5.7" +baseline-core-api = "3.5.8" +baselinePerfCore = "3.5.8" baselinePerfExtra = "3.5.1" # Other shared versions From 1df750e20d392dc72344871e8ad220a6ae2ebf7c Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 11 Jul 2023 15:46:21 +0200 Subject: [PATCH 154/312] [release] Prepare and release 3.5.8 --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8cc34e84b9..3e007ca438 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.9-SNAPSHOT +version=3.5.8 bomVersion=2022.0.9 -metricsMicrometerVersion=1.0.9-SNAPSHOT +metricsMicrometerVersion=1.0.8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c7137a90f..75c0d7bb5c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.8" -baselinePerfCore = "3.5.8" +baseline-core-api = "3.5.7" +baselinePerfCore = "3.5.7" baselinePerfExtra = "3.5.1" # Other shared versions @@ -27,7 +27,7 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.4" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.7" micrometer-test = { module = "io.micrometer:micrometer-test" } From 5f5435fe8029ed61a7bc15dd3836ef99f9d18416 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 11 Jul 2023 16:31:12 +0200 Subject: [PATCH 155/312] [release] Next development version 3.5.9-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3e007ca438..8cc34e84b9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.8 +version=3.5.9-SNAPSHOT bomVersion=2022.0.9 -metricsMicrometerVersion=1.0.8 +metricsMicrometerVersion=1.0.9-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75c0d7bb5c..0f6eb4ee6f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,14 +7,14 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.7" -baselinePerfCore = "3.5.7" +baseline-core-api = "3.5.8" +baselinePerfCore = "3.5.8" baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.9" +micrometer = "1.10.10-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,7 +27,7 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.4" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.7" micrometer-test = { module = "io.micrometer:micrometer-test" } From e9aa94b355b9ee2e9578cdf79b35c21263560aa3 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 11 Jul 2023 19:27:34 +0200 Subject: [PATCH 156/312] [release] Prepare and release 3.6.0-M1 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 40ddd2769f..c369f06ba2 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.8" - testCompile "io.projectreactor:reactor-test:3.5.8" + compile "io.projectreactor:reactor-core:3.6.0-M1" + testCompile "io.projectreactor:reactor-test:3.6.0-M1" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.9-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.9-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.8" + // implementation "io.projectreactor:reactor-tools:3.6.0-M1" } ``` diff --git a/gradle.properties b/gradle.properties index 9168e65d78..f111f30a9d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-SNAPSHOT -bomVersion=2023.0.0-SNAPSHOT -metricsMicrometerVersion=1.1.0-SNAPSHOT +version=3.6.0-M1 +bomVersion=2023.0.0-M1 +metricsMicrometerVersion=1.1.0-M1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7bcabd641..47b35cf22c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.10-SNAPSHOT" +micrometer = "1.10.9" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,7 +27,7 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.4" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.7" micrometer-test = { module = "io.micrometer:micrometer-test" } From 28d288a97a39e6804cb270360c8e9b329dbedffc Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 11 Jul 2023 21:54:10 +0200 Subject: [PATCH 157/312] [release] Next development version 3.6.0-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index f111f30a9d..cdaefca474 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-M1 +version=3.6.0-SNAPSHOT bomVersion=2023.0.0-M1 -metricsMicrometerVersion=1.1.0-M1 +metricsMicrometerVersion=1.1.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 47b35cf22c..b7bcabd641 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.9" +micrometer = "1.10.10-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,7 +27,7 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.4" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.7" micrometer-test = { module = "io.micrometer:micrometer-test" } From 853c95667a80b482e35b73c5e51d010ac6f3bbed Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:51:57 +0300 Subject: [PATCH 158/312] fixes MonoDelayElemnt to properly handle race between run and onNext (#3546) Signed-off-by: OlegDokuka --- .../publisher/MonoDelayElementStressTest.java | 75 +++++++++++++++++++ .../core/publisher/MonoDelayElement.java | 70 ++++++++++++----- 2 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 reactor-core/src/jcstress/java/reactor/core/publisher/MonoDelayElementStressTest.java diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/MonoDelayElementStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/MonoDelayElementStressTest.java new file mode 100644 index 0000000000..1f582afa28 --- /dev/null +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/MonoDelayElementStressTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.III_Result; +import reactor.test.scheduler.VirtualTimeScheduler; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +public abstract class MonoDelayElementStressTest { + + @JCStressTest + @Outcome(id = {"1, 0, 1"}, expect = ACCEPTABLE) + @State + public static class OnNextAndRunStressTest { + + /* + Implementation notes: in this test we use the VirtualTimeScheduler to better coordinate + the triggering of the `run` method + */ + + final StressSubscriber subscriber = new StressSubscriber<>(0L); + final VirtualTimeScheduler virtualTimeScheduler; + final MonoDelayElement monoDelay; + + MonoDelayElement.DelayElementSubscriber subscription; + + { + virtualTimeScheduler = VirtualTimeScheduler.create(); + monoDelay = new MonoDelayElement<>(Mono.never(), 0L, + TimeUnit.MILLISECONDS, + virtualTimeScheduler); + monoDelay.doOnSubscribe(s -> subscription = ((MonoDelayElement.DelayElementSubscriber) s)).subscribe(subscriber); + } + + @Actor + public void delayTrigger() { + subscription.onNext(1); + } + + @Actor + public void request() { + virtualTimeScheduler.advanceTime(); + } + + @Arbiter + public void arbiter(III_Result r) { + virtualTimeScheduler.advanceTime(); + r.r1 = subscriber.onNextCalls.get(); + r.r2 = subscriber.onErrorCalls.get(); + r.r3 = subscriber.onCompleteCalls.get(); + } + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoDelayElement.java b/reactor-core/src/main/java/reactor/core/publisher/MonoDelayElement.java index 155321a17a..1fe1115ea4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoDelayElement.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoDelayElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,6 @@ * * @see Reactive-Streams-Commons * @author Simon Baslé - * TODO : Review impl */ final class MonoDelayElement extends InternalMonoOperator { @@ -141,12 +140,33 @@ public void onNext(T t) { this.value = t; try { - final Disposable currentTask = this.task; - final Disposable nextTask = scheduler.schedule(this, delay, unit); - if (currentTask != null || !TASK.compareAndSet(this, null, nextTask)) { + Disposable currentTask = this.task; + if (currentTask == CANCELLED) { this.value = null; - nextTask.dispose(); Operators.onDiscard(t, actual.currentContext()); + return; + } + + final Disposable nextTask = scheduler.schedule(this, delay, unit); + + for (;;) { + currentTask = this.task; + + if (currentTask == CANCELLED) { + nextTask.dispose(); + Operators.onDiscard(t, actual.currentContext()); + return; + } + + if (currentTask == TERMINATED) { + // scheduled task completion happened before this + // just return and do nothing + return; + } + + if (TASK.compareAndSet(this, null, nextTask)) { + return; + } } } catch (RejectedExecutionException ree) { @@ -158,10 +178,17 @@ public void onNext(T t) { @Override public void run() { - final Disposable currentTask = this.task; + for (;;) { + final Disposable currentTask = this.task; - if (currentTask == CANCELLED || !TASK.compareAndSet(this, currentTask, TERMINATED)) { - return; + if (currentTask == CANCELLED) { + return; + } + + if (TASK.compareAndSet(this, currentTask, TERMINATED)) { + break; + } + // we may to repeat since this may race with CAS in the onNext method } final T value = this.value; @@ -173,20 +200,23 @@ public void run() { @Override public void cancel() { - final Disposable task = this.task; - if (task == CANCELLED || task == TERMINATED) { - return; - } + for (;;) { + final Disposable task = this.task; + if (task == CANCELLED || task == TERMINATED) { + return; + } - if (TASK.compareAndSet(this, task, CANCELLED)) { - if (task != null) { - task.dispose(); + if (TASK.compareAndSet(this, task, CANCELLED)) { + if (task != null) { + task.dispose(); - final T value = this.value; - this.value = null; + final T value = this.value; + this.value = null; - Operators.onDiscard(value, actual.currentContext()); - return; + Operators.onDiscard(value, actual.currentContext()); + return; + } + break; } } From 33afc884fd1bd9a0070bc1217084727d0b4354f6 Mon Sep 17 00:00:00 2001 From: AramMessdaghi9001 <67048354+AramMessdaghi9001@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:55:13 +0200 Subject: [PATCH 159/312] ensures `takeUntil` Predicate test before happens before emission (#3544) --- .../main/java/reactor/core/publisher/Flux.java | 3 +++ .../reactor/core/publisher/FluxTakeUntil.java | 7 ++++--- .../publisher/FluxTakeUntilPredicateTest.java | 18 +++++++++++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 74c42ea42f..89afdf55b3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -9206,6 +9206,9 @@ public final Flux takeLast(int n) { /** * Relay values from this {@link Flux} until the given {@link Predicate} matches. * This includes the matching data (unlike {@link #takeWhile}). + * The predicate is tested before the element is emitted, + * so if the element is modified by the consumer, this won't affect the predicate. + * In case of an error during the predicate test, the current element is emitted before the error. * *

    * diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTakeUntil.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTakeUntil.java index d215c9f248..04d3ee566e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTakeUntil.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTakeUntil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,18 +81,19 @@ public void onNext(T t) { return; } - actual.onNext(t); - boolean b; try { b = predicate.test(t); } catch (Throwable e) { + actual.onNext(t); onError(Operators.onOperatorError(s, e, t, actual.currentContext())); return; } + actual.onNext(t); + if (b) { s.cancel(); diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxTakeUntilPredicateTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxTakeUntilPredicateTest.java index 2567d18cac..cd3690ad04 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxTakeUntilPredicateTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxTakeUntilPredicateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import reactor.core.Scannable; import reactor.test.subscriber.AssertSubscriber; +import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -94,6 +95,21 @@ public void takeSome() { .assertNoError(); } + @Test + public void takeSomeWithChangedValue() { + AssertSubscriber ts = AssertSubscriber.create(); + + Flux.range(1, 5) + .map(AtomicInteger::new) + .takeUntil(v -> v.get() == 3) + .map(v -> v.getAndSet(0)) + .subscribe(ts); + + ts.assertValues(1, 2, 3) + .assertComplete() + .assertNoError(); + } + @Test public void takeSomeBackpressured() { AssertSubscriber ts = AssertSubscriber.create(0); From 4399fc5525dda31ed67332947cd735db257cc7fb Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 9 Aug 2023 13:18:53 +0300 Subject: [PATCH 160/312] reworks FluxPublish internals to relay on predictable state machine (#3538) Signed-off-by: Oleh Dokuka --- .../core/publisher/FluxPublishBenchmark.java | 106 +++++ .../core/publisher/FluxPublishStressTest.java | 48 +++ .../reactor/core/publisher/FluxPublish.java | 402 +++++++++++++++--- .../publisher/OnDiscardShouldNotLeakTest.java | 7 + 4 files changed, 496 insertions(+), 67 deletions(-) create mode 100644 benchmarks/src/main/java/reactor/core/publisher/FluxPublishBenchmark.java diff --git a/benchmarks/src/main/java/reactor/core/publisher/FluxPublishBenchmark.java b/benchmarks/src/main/java/reactor/core/publisher/FluxPublishBenchmark.java new file mode 100644 index 0000000000..b40af13ed0 --- /dev/null +++ b/benchmarks/src/main/java/reactor/core/publisher/FluxPublishBenchmark.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +@BenchmarkMode({Mode.AverageTime}) +@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class FluxPublishBenchmark { + @Param({"0", "10", "1000", "100000"}) + int rangeSize; + + Flux source; + + @Setup(Level.Invocation) + public void setup() { + source = Flux.range(0, rangeSize) + .hide() + .publish() + .autoConnect(Runtime.getRuntime() + .availableProcessors()); + } + + + @State(Scope.Thread) + public static class JmhSubscriber extends CountDownLatch implements CoreSubscriber { + + Blackhole blackhole; + + Subscription s; + + public JmhSubscriber() { + super(1); + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + blackhole.consume(t); + } + + @Override + public void onError(Throwable t) { + blackhole.consume(t); + countDown(); + } + + @Override + public void onComplete() { + countDown(); + } + } + + @SuppressWarnings("unused") + @Benchmark + @Threads(Threads.MAX) + public Object measureThroughput(Blackhole blackhole, JmhSubscriber subscriber) throws InterruptedException { + subscriber.blackhole = blackhole; + source.subscribe(subscriber); + subscriber.await(); + return subscriber; + } +} diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxPublishStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxPublishStressTest.java index f1e77df700..d7e71a2235 100644 --- a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxPublishStressTest.java +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxPublishStressTest.java @@ -26,6 +26,8 @@ import org.openjdk.jcstress.infra.results.IIIIII_Result; import reactor.core.scheduler.Schedulers; import reactor.util.annotation.Nullable; +import org.openjdk.jcstress.infra.results.III_Result; +import reactor.core.Disposable; import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; @@ -445,4 +447,50 @@ public void arbiter(IIIIII_Result r) { r.r6 = subscriber2.onErrorCalls.get(); } } + +// TODO: uncomment me. Proper discard is not supported yet since we dont have stable +// downstream context available all the time. This should be uncommented once we have +// an explicitly passed onDiscard handler +// @JCStressTest +// @Outcome(id = {"10, 1, 0"}, expect = ACCEPTABLE, desc = "all values and completion delivered") +// @Outcome(id = {"10, 0, 1"}, expect = ACCEPTABLE, desc = "some values are delivered some dropped since overflow") +// @State +// public static class ConcurrentDisposeAndProduceStressTest { +// +// final Sinks.Many producer = Sinks.unsafe().many().multicast().directAllOrNothing(); +// +// final ConnectableFlux sharedSource = producer.asFlux().publish(5); +// +// final StressSubscriber subscriber = new StressSubscriber<>(); +// +// final Disposable disposable; +// +// { +// sharedSource.subscribe(subscriber); +// disposable = sharedSource.connect(); +// } +// +// @Actor +// public void dispose() { +// disposable.dispose(); +// } +// +// @Actor +// public void emitValues() { +// for (int i = 0; i < 10; i++) { +// if (producer.tryEmitNext(i) != Sinks.EmitResult.OK) { +// Operators.onDiscard(i, subscriber.context); +// } +// } +// +// producer.tryEmitComplete(); +// } +// +// @Arbiter +// public void arbiter(III_Result r) { +// r.r1 = subscriber.onNextCalls.get() + subscriber.onNextDiscarded.get(); +// r.r2 = subscriber.onCompleteCalls.get(); +// r.r3 = subscriber.onErrorCalls.get(); +// } +// } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java b/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java index 7932c57bb1..ef0d7587bf 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java @@ -19,7 +19,6 @@ import java.util.Objects; import java.util.Queue; import java.util.concurrent.CancellationException; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; @@ -98,7 +97,7 @@ public void connect(Consumer cancelSupport) { s = u; } - doConnect = s.tryConnect(); + doConnect = s.tryConnect(); break; } @@ -130,12 +129,15 @@ public void subscribe(CoreSubscriber actual) { if (c.add(inner)) { if (inner.isCancelled()) { c.remove(inner); - } else { + } + else { inner.parent = c; } - c.drain(); + + c.drainFromInner(); break; - } else if (!this.resetUponSourceTermination) { + } + else if (!this.resetUponSourceTermination) { if (c.error != null) { inner.actual.onError(c.error); } else { @@ -168,12 +170,7 @@ static final class PublishSubscriber final FluxPublish parent; - volatile Subscription s; - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater S = - AtomicReferenceFieldUpdater.newUpdater(PublishSubscriber.class, - Subscription.class, - "s"); + Subscription s; volatile PubSubInner[] subscribers; @@ -183,16 +180,10 @@ static final class PublishSubscriber PubSubInner[].class, "subscribers"); - volatile int wip; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP = - AtomicIntegerFieldUpdater.newUpdater(PublishSubscriber.class, "wip"); - - volatile int connected; + volatile long state; @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater CONNECTED = - AtomicIntegerFieldUpdater.newUpdater(PublishSubscriber.class, - "connected"); + static final AtomicLongFieldUpdater STATE = + AtomicLongFieldUpdater.newUpdater(PublishSubscriber.class, "state"); //notes: FluxPublish needs to distinguish INIT from CANCELLED in order to correctly //drop values in case of an early connect() without any subscribers. @@ -203,11 +194,11 @@ static final class PublishSubscriber @SuppressWarnings("rawtypes") static final PubSubInner[] TERMINATED = new PublishInner[0]; - volatile Queue queue; + Queue queue; int sourceMode; - volatile boolean done; + boolean done; volatile Throwable error; @@ -217,7 +208,6 @@ static final class PublishSubscriber Throwable.class, "error"); - @SuppressWarnings("unchecked") PublishSubscriber(int prefetch, FluxPublish parent) { this.prefetch = prefetch; this.parent = parent; @@ -230,7 +220,9 @@ boolean isTerminated(){ @Override public void onSubscribe(Subscription s) { - if (Operators.setOnce(S, this, s)) { + if (Operators.validate(this.s, s)) { + this.s = s; + if (s instanceof Fuseable.QueueSubscription) { @SuppressWarnings("unchecked") Fuseable.QueueSubscription f = (Fuseable.QueueSubscription) s; @@ -239,46 +231,82 @@ public void onSubscribe(Subscription s) { if (m == Fuseable.SYNC) { sourceMode = m; queue = f; - drain(); + long previousState = markSubscriptionSetAndAddWork(this); + if (isCancelled(previousState)) { + s.cancel(); + return; + } + + if (hasWorkInProgress(previousState)) { + return; + } + + drain(previousState | SUBSCRIPTION_SET_FLAG | 1); return; } + if (m == Fuseable.ASYNC) { sourceMode = m; queue = f; - s.request(Operators.unboundedOrPrefetch(prefetch)); + long previousState = markSubscriptionSet(this); + if (isCancelled(previousState)) { + s.cancel(); + } + else { + s.request(Operators.unboundedOrPrefetch(prefetch)); + } return; } } queue = parent.queueSupplier.get(); - - s.request(Operators.unboundedOrPrefetch(prefetch)); + long previousState = markSubscriptionSet(this); + if (isCancelled(previousState)) { + s.cancel(); + } + else { + s.request(Operators.unboundedOrPrefetch(prefetch)); + } } } @Override - public void onNext(T t) { + public void onNext(@Nullable T t) { if (done) { if (t != null) { Operators.onNextDropped(t, currentContext()); } return; } - if (sourceMode == Fuseable.ASYNC) { - drain(); - return; - } - - if (!queue.offer(t)) { + boolean isAsyncMode = sourceMode == Fuseable.ASYNC; + if (!isAsyncMode && !queue.offer(t)) { Throwable ex = Operators.onOperatorError(s, - Exceptions.failWithOverflow(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL), t, currentContext()); + Exceptions.failWithOverflow(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL), + t, + currentContext()); if (!Exceptions.addThrowable(ERROR, this, ex)) { Operators.onErrorDroppedMulticast(ex, subscribers); return; } done = true; } - drain(); + + long previousState = addWork(this); + + if (isFinalized(previousState)) { + clear(); + return; + } + + if (isTerminated(previousState) || isCancelled(previousState)) { + return; + } + + if (hasWorkInProgress(previousState)) { + return; + } + + drain(previousState + 1); } @Override @@ -287,13 +315,22 @@ public void onError(Throwable t) { Operators.onErrorDroppedMulticast(t, subscribers); return; } - if (Exceptions.addThrowable(ERROR, this, t)) { - done = true; - drain(); - } - else { + + done = true; + if (!Exceptions.addThrowable(ERROR, this, t)) { Operators.onErrorDroppedMulticast(t, subscribers); } + + long previousState = markTerminated(this); + if (isTerminated(previousState) || isCancelled(previousState)) { + return; + } + + if (hasWorkInProgress(previousState)) { + return; + } + + drain((previousState | TERMINATED_FLAG) + 1); } @Override @@ -302,7 +339,17 @@ public void onComplete() { return; } done = true; - drain(); + + long previousState = markTerminated(this); + if (isTerminated(previousState) || isCancelled(previousState)) { + return; + } + + if (hasWorkInProgress(previousState)) { + return; + } + + drain((previousState | TERMINATED_FLAG) + 1); } @Override @@ -311,19 +358,40 @@ public void dispose() { return; } if (CONNECTION.compareAndSet(parent, this, null)) { - Operators.terminate(S, this); - if (WIP.getAndIncrement(this) != 0) { + long previousState = markCancelled(this); + if (isTerminated(previousState) || isCancelled(previousState)) { + return; + } + + if (hasWorkInProgress(previousState)) { return; } - disconnectAction(); + + disconnectAction(previousState); } } - void disconnectAction() { + void clear() { + if (sourceMode == Fuseable.NONE) { + T t; + while ((t = queue.poll()) != null) { + Operators.onDiscard(t, currentContext()); + } + } + else { + queue.clear(); + } + } + + void disconnectAction(long previousState) { + if (isSubscriptionSet(previousState)) { + this.s.cancel(); + clear(); + } + @SuppressWarnings("unchecked") PubSubInner[] inners = SUBSCRIBERS.getAndSet(this, CANCELLED); if (inners.length > 0) { - queue.clear(); CancellationException ex = new CancellationException("Disconnected"); for (PubSubInner inner : inners) { @@ -348,7 +416,6 @@ boolean add(PublishInner inner) { } } - @SuppressWarnings("unchecked") public void remove(PubSubInner inner) { for (; ; ) { PubSubInner[] a = subscribers; @@ -392,16 +459,26 @@ PubSubInner[] terminate() { } boolean tryConnect() { - return connected == 0 && CONNECTED.compareAndSet(this, 0, 1); + long previousState = markConnected(this); + + return !isConnected(previousState); } - final void drain() { - if (WIP.getAndIncrement(this) != 0) { + final void drainFromInner() { + long previousState = addWorkIfSubscribed(this); + + if (!isSubscriptionSet(previousState)) { return; } - int missed = 1; + if (hasWorkInProgress(previousState)) { + return; + } + drain(previousState + 1); + } + + final void drain(long expectedState) { for (; ; ) { boolean d = done; @@ -411,7 +488,7 @@ final void drain() { boolean empty = q == null || q.isEmpty(); - if (checkTerminated(d, empty)) { + if (checkTerminated(d, empty, null)) { return; } @@ -446,7 +523,7 @@ final void drain() { d = true; v = null; } - if (checkTerminated(d, v == null)) { + if (checkTerminated(d, v == null, v)) { return; } if (mode != Fuseable.SYNC) { @@ -474,7 +551,7 @@ final void drain() { empty = v == null; - if (checkTerminated(d, empty)) { + if (checkTerminated(d, empty, v)) { return; } @@ -482,7 +559,7 @@ final void drain() { //async mode only needs to break but SYNC mode needs to perform terminal cleanup here... if (mode == Fuseable.SYNC) { done = true; - checkTerminated(true, true); + checkTerminated(true, true, null); } break; } @@ -509,21 +586,28 @@ final void drain() { } else if (q != null && mode == Fuseable.SYNC) { done = true; - if (checkTerminated(true, empty)) { + if (checkTerminated(true, empty, null)) { break; } } - missed = WIP.addAndGet(this, -missed); - if (missed == 0) { - break; + expectedState = markWorkDone(this, expectedState); + if (isCancelled(expectedState)) { + clearAndFinalize(this); + return; + } + + if (!hasWorkInProgress(expectedState)) { + return; } } } - boolean checkTerminated(boolean d, boolean empty) { - if (s == Operators.cancelledSubscription()) { - disconnectAction(); + boolean checkTerminated(boolean d, boolean empty, @Nullable T t) { + long state = this.state; + if (isCancelled(state)) { + Operators.onDiscard(t, currentContext()); + disconnectAction(state); return true; } if (d) { @@ -578,9 +662,193 @@ public Object scanUnsafe(Attr key) { @Override public boolean isDisposed() { - return s == Operators.cancelledSubscription() || done; + long state = this.state; + return isTerminated(state) || isCancelled(state); } + static void clearAndFinalize(PublishSubscriber instance) { + for (; ; ) { + final long state = instance.state; + + if (isFinalized(state)) { + instance.clear(); + return; + } + + if (isSubscriptionSet(state)) { + instance.clear(); + } + + if (STATE.compareAndSet( + instance, state, + (state & ~WORK_IN_PROGRESS_MASK) | FINALIZED_FLAG)) { + break; + } + } + } + + static long addWork(PublishSubscriber instance) { + for (;;) { + long state = instance.state; + + if (STATE.compareAndSet(instance, state, addWork(state))) { + return state; + } + } + } + + static long addWorkIfSubscribed(PublishSubscriber instance) { + for (;;) { + long state = instance.state; + + if (!isSubscriptionSet(state)) { + return state; + } + + if (STATE.compareAndSet(instance, state, addWork(state))) { + return state; + } + } + } + + static long addWork(long state) { + if ((state & WORK_IN_PROGRESS_MASK) == WORK_IN_PROGRESS_MASK) { + return (state &~ WORK_IN_PROGRESS_MASK) | 1; + } + else { + return state + 1; + } + } + + static long markTerminated(PublishSubscriber instance) { + for (;;) { + long state = instance.state; + + if (isCancelled(state) || isTerminated(state)) { + return state; + } + + long nextState = addWork(state); + if (STATE.compareAndSet(instance, state, nextState | TERMINATED_FLAG)) { + return state; + } + } + } + + static long markConnected(PublishSubscriber instance) { + for (;;) { + long state = instance.state; + + if (isConnected(state)) { + return state; + } + + if (STATE.compareAndSet(instance, state, state | CONNECTED_FLAG)) { + return state; + } + } + } + + static long markSubscriptionSet(PublishSubscriber instance) { + for (;;) { + long state = instance.state; + + if (isCancelled(state)) { + return state; + } + + if (STATE.compareAndSet(instance, state, state | SUBSCRIPTION_SET_FLAG)) { + return state; + } + } + } + + static long markSubscriptionSetAndAddWork(PublishSubscriber instance) { + for (;;) { + long state = instance.state; + + if (isCancelled(state)) { + return state; + } + + long nextState = addWork(state); + if (STATE.compareAndSet(instance, state, nextState | SUBSCRIPTION_SET_FLAG)) { + return state; + } + } + } + + static long markCancelled(PublishSubscriber instance) { + for (;;) { + long state = instance.state; + + if (isCancelled(state)) { + return state; + } + + long nextState = addWork(state); + if (STATE.compareAndSet(instance, state, nextState | CANCELLED_FLAG)) { + return state; + } + } + } + + static long markWorkDone(PublishSubscriber instance, long expectedState) { + for (;;) { + long state = instance.state; + + if (expectedState != state) { + return state; + } + + long nextState = state & ~WORK_IN_PROGRESS_MASK; + if (STATE.compareAndSet(instance, state, nextState)) { + return nextState; + } + } + } + + static boolean isConnected(long state) { + return (state & CONNECTED_FLAG) == CONNECTED_FLAG; + } + + static boolean isFinalized(long state) { + return (state & FINALIZED_FLAG) == FINALIZED_FLAG; + } + + static boolean isCancelled(long state) { + return (state & CANCELLED_FLAG) == CANCELLED_FLAG; + } + + static boolean isTerminated(long state) { + return (state & TERMINATED_FLAG) == TERMINATED_FLAG; + } + + static boolean isSubscriptionSet(long state) { + return (state & SUBSCRIPTION_SET_FLAG) == SUBSCRIPTION_SET_FLAG; + } + + static boolean hasWorkInProgress(long state) { + return (state & WORK_IN_PROGRESS_MASK) > 0; + } + + static final long FINALIZED_FLAG = + 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + + static final long CANCELLED_FLAG = + 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + + static final long TERMINATED_FLAG = + 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + + static final long SUBSCRIPTION_SET_FLAG = + 0b0000_1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + + static final long CONNECTED_FLAG = + 0b0000_0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + + static final long WORK_IN_PROGRESS_MASK = + 0b0000_0000_0000_0000_0000_0000_0000_0000_1111_1111_1111_1111_1111_1111_1111_1111L; } static abstract class PubSubInner implements InnerProducer { @@ -649,7 +917,7 @@ static final class PublishInner extends PubSubInner { void drainParent() { PublishSubscriber p = parent; if (p != null) { - p.drain(); + p.drainFromInner(); } } @@ -658,7 +926,7 @@ void removeAndDrainParent() { PublishSubscriber p = parent; if (p != null) { p.remove(this); - p.drain(); + p.drainFromInner(); } } diff --git a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java index f1cc13d99d..dbcbebf449 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java @@ -169,6 +169,13 @@ public class OnDiscardShouldNotLeakTest { .map(Function.identity()) .map(Function.identity()) .publishOn(Schedulers.immediate())), + // TODO: uncomment me. Proper discard is not supported yet since we dont have stable + // downstream context available all the time. This should be uncommented once we have + // an explicitly passed onDiscard handler + /*DiscardScenario.fluxSource("publishOnAndPublish", main -> main + .publishOn(Schedulers.immediate()) + .publish() + .refCount()),*/ DiscardScenario.sinkSource("unicastSink", Sinks.unsafe().many().unicast()::onBackpressureBuffer, null), DiscardScenario.sinkSource("unicastSinkAndPublishOn", Sinks.unsafe().many().unicast()::onBackpressureBuffer, f -> f.publishOn(Schedulers.immediate())), From e166117d6f62aa842c75b24dffe7b6d9db288b82 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 15 Aug 2023 08:42:35 +0300 Subject: [PATCH 161/312] Update Micrometer version to 1.10.10 (#3560) Update Context Propagation to version 1.0.5 Update Micrometer Tracing Test to version 1.0.9 --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f6eb4ee6f..cefb1ff445 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.10-SNAPSHOT" +micrometer = "1.10.10" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -26,10 +26,10 @@ micrometer102Compatible-bom = { module = "io.micrometer:micrometer-bom", version micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.4" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.7" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.9" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 77541615768672cd1563627871cd574d058f3dd9 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 15 Aug 2023 10:26:40 +0300 Subject: [PATCH 162/312] [release] Prepare and release 3.5.9 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3fab106cad..9ac06128ed 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.8" - testCompile "io.projectreactor:reactor-test:3.5.8" + compile "io.projectreactor:reactor-core:3.5.9" + testCompile "io.projectreactor:reactor-test:3.5.9" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.9-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.9-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.10-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.10-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.8" + // implementation "io.projectreactor:reactor-tools:3.5.9" } ``` diff --git a/gradle.properties b/gradle.properties index 8cc34e84b9..7eecd642e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.9-SNAPSHOT -bomVersion=2022.0.9 -metricsMicrometerVersion=1.0.9-SNAPSHOT +version=3.5.9 +bomVersion=2022.0.10 +metricsMicrometerVersion=1.0.9 From 086c0edbeff3fc093be153b4df9903e6f6ec6f98 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 15 Aug 2023 11:05:19 +0300 Subject: [PATCH 163/312] [release] Next development version 3.5.10-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gradle.properties b/gradle.properties index 7eecd642e6..bdef927861 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.9 +version=3.5.10-SNAPSHOT bomVersion=2022.0.10 -metricsMicrometerVersion=1.0.9 +metricsMicrometerVersion=1.0.10-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cefb1ff445..2d2613b2dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,14 +7,14 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.8" -baselinePerfCore = "3.5.8" +baseline-core-api = "3.5.9" +baselinePerfCore = "3.5.9" baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.10" +micrometer = "1.10.11-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -26,10 +26,10 @@ micrometer102Compatible-bom = { module = "io.micrometer:micrometer-bom", version micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.6-SNAPSHOT" +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.9" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.10-SNAPSHOT" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 984c4eb4b4e9da103e1fec84abe4c0e0ae47254b Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 15 Aug 2023 11:54:12 +0300 Subject: [PATCH 164/312] Update context propagation to 1.0.5 There is no 1.0.6-SNAPSHOT --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2d2613b2dc..8d539c2ba4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ micrometer102Compatible-bom = { module = "io.micrometer:micrometer-bom", version micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.6-SNAPSHOT" +micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.10-SNAPSHOT" From 372a7f31ab34df597ad404aeef2a22028465ca98 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 15 Aug 2023 13:46:22 +0300 Subject: [PATCH 165/312] upgrades to latest micrometer versions (#3561) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9656a17e4..6dce880ee2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.11-SNAPSHOT" +micrometer = "1.12.0-M2" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -29,7 +29,7 @@ micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.10-SNAPSHOT" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M2" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 68f35b62902dd1403dcf2912e2ef42b4935d5a18 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 15 Aug 2023 13:52:37 +0300 Subject: [PATCH 166/312] [release] Prepare and release 3.6.0-M2 --- README.md | 6 +++--- gradle.properties | 6 +++--- gradle/libs.versions.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c369f06ba2..e4f8e5c96a 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0-M1" - testCompile "io.projectreactor:reactor-test:3.6.0-M1" + compile "io.projectreactor:reactor-core:3.6.0-M2" + testCompile "io.projectreactor:reactor-test:3.6.0-M2" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0-M1" + // implementation "io.projectreactor:reactor-tools:3.6.0-M2" } ``` diff --git a/gradle.properties b/gradle.properties index cdaefca474..872b56d607 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-SNAPSHOT -bomVersion=2023.0.0-M1 -metricsMicrometerVersion=1.1.0-SNAPSHOT +version=3.6.0-M2 +bomVersion=2023.0.0-M2 +metricsMicrometerVersion=1.1.0-M2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6dce880ee2..59fe05474d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M2" micrometer-test = { module = "io.micrometer:micrometer-test" } From 6dfcd3bd0a5fadd673be4bc9d1ced10261c5a816 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 15 Aug 2023 14:09:08 +0300 Subject: [PATCH 167/312] [release] Next development version 3.6.0-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index 872b56d607..e357cf47df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-M2 +version=3.6.0-SNAPSHOT bomVersion=2023.0.0-M2 -metricsMicrometerVersion=1.1.0-M2 +metricsMicrometerVersion=1.1.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59fe05474d..e93bdc4c6c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-M2" +micrometer = "1.12.0-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M2" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-SNAPSHOT" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 9dabc1374be09ddc126c0224020d2880ac97d7d8 Mon Sep 17 00:00:00 2001 From: Yevhen Surovskyi <66682229+ajax-surovskyi-y@users.noreply.github.com> Date: Thu, 17 Aug 2023 14:10:27 +0300 Subject: [PATCH 168/312] fixes return type for Mono.tap in javadoc (#3564) --- reactor-core/src/main/java/reactor/core/publisher/Mono.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 9e482626b7..cc5752426d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -4745,7 +4745,7 @@ public SignalListener createListener(Publisher ignored1, Context * in conjunction with the use of {@link #contextCapture()} operator down the chain. * * @param listenerFactory the {@link SignalListenerFactory} to create a new {@link SignalListener} on each subscription - * @return a new {@link Flux} with side effects defined by generated {@link SignalListener} + * @return a new {@link Mono} with side effects defined by generated {@link SignalListener} * @see #tap(Supplier) * @see #tap(Function) */ From 7f9c77ec28dc0bdfc1eafe04062cb1195fd9b552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 12 Sep 2023 12:53:01 +0200 Subject: [PATCH 169/312] [release] Prepare and release 3.5.10 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9ac06128ed..533ac11810 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.9" - testCompile "io.projectreactor:reactor-test:3.5.9" + compile "io.projectreactor:reactor-core:3.5.10" + testCompile "io.projectreactor:reactor-test:3.5.10" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.10-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.10-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.11-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.11-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.9" + // implementation "io.projectreactor:reactor-tools:3.5.10" } ``` diff --git a/gradle.properties b/gradle.properties index bdef927861..925b0608d4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.10-SNAPSHOT -bomVersion=2022.0.10 -metricsMicrometerVersion=1.0.10-SNAPSHOT +version=3.5.10 +bomVersion=2022.0.11 +metricsMicrometerVersion=1.0.10 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d539c2ba4..1c1252f11f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.11-SNAPSHOT" +micrometer = "1.10.11" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.10-SNAPSHOT" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.10" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From ef308ff96b781d6095093516da64b58fb148d82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 12 Sep 2023 14:13:03 +0200 Subject: [PATCH 170/312] [release] Next development version 3.5.11-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index 925b0608d4..f80d25a9de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.10 +version=3.5.11-SNAPSHOT bomVersion=2022.0.11 -metricsMicrometerVersion=1.0.10 +metricsMicrometerVersion=1.0.11-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c1252f11f..12120015dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,14 +7,14 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.9" -baselinePerfCore = "3.5.9" +baseline-core-api = "3.5.10" +baselinePerfCore = "3.5.10" baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.11" +micrometer = "1.10.12-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.10" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.11-SNAPSHOT" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 0aa71d9fe757444544002896e473105013a97189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 12 Sep 2023 16:15:48 +0200 Subject: [PATCH 171/312] [release] Prepare and release 3.6.0-M3 --- README.md | 6 +++--- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e4f8e5c96a..7a13235eef 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0-M2" - testCompile "io.projectreactor:reactor-test:3.6.0-M2" + compile "io.projectreactor:reactor-core:3.6.0-M3" + testCompile "io.projectreactor:reactor-test:3.6.0-M3" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0-M2" + // implementation "io.projectreactor:reactor-tools:3.6.0-M3" } ``` diff --git a/gradle.properties b/gradle.properties index e357cf47df..ab667b97b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-SNAPSHOT -bomVersion=2023.0.0-M2 -metricsMicrometerVersion=1.1.0-SNAPSHOT +version=3.6.0-M3 +bomVersion=2023.0.0-M3 +metricsMicrometerVersion=1.1.0-M3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 261a353c26..1379d4c12d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-SNAPSHOT" +micrometer = "1.12.0-M3" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-SNAPSHOT" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M3" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 7b97f57c906220196283d45da8cf9827edee2909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 13 Sep 2023 09:40:50 +0200 Subject: [PATCH 172/312] Revert "[release] Prepare and release 3.6.0-M3" This reverts commit 0aa71d9fe757444544002896e473105013a97189. --- README.md | 6 +++--- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7a13235eef..e4f8e5c96a 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0-M3" - testCompile "io.projectreactor:reactor-test:3.6.0-M3" + compile "io.projectreactor:reactor-core:3.6.0-M2" + testCompile "io.projectreactor:reactor-test:3.6.0-M2" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0-M3" + // implementation "io.projectreactor:reactor-tools:3.6.0-M2" } ``` diff --git a/gradle.properties b/gradle.properties index ab667b97b6..e357cf47df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-M3 -bomVersion=2023.0.0-M3 -metricsMicrometerVersion=1.1.0-M3 +version=3.6.0-SNAPSHOT +bomVersion=2023.0.0-M2 +metricsMicrometerVersion=1.1.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1379d4c12d..261a353c26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-M3" +micrometer = "1.12.0-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M3" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-SNAPSHOT" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 47c145e9695b7ec0605c118aa98bd48c83de82bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 13 Sep 2023 10:23:34 +0200 Subject: [PATCH 173/312] [release] Prepare and release 3.6.0-M3 --- README.md | 6 +++--- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e4f8e5c96a..7a13235eef 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0-M2" - testCompile "io.projectreactor:reactor-test:3.6.0-M2" + compile "io.projectreactor:reactor-core:3.6.0-M3" + testCompile "io.projectreactor:reactor-test:3.6.0-M3" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0-M2" + // implementation "io.projectreactor:reactor-tools:3.6.0-M3" } ``` diff --git a/gradle.properties b/gradle.properties index e357cf47df..ab667b97b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-SNAPSHOT -bomVersion=2023.0.0-M2 -metricsMicrometerVersion=1.1.0-SNAPSHOT +version=3.6.0-M3 +bomVersion=2023.0.0-M3 +metricsMicrometerVersion=1.1.0-M3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 261a353c26..638691e79d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-SNAPSHOT" +micrometer = "1.12.0-M3" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-SNAPSHOT" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M2" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From c6e3291f20344a43303c955082d7f9c638acb3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 13 Sep 2023 13:06:29 +0200 Subject: [PATCH 174/312] Revert "[release] Prepare and release 3.6.0-M3" and disable tests This reverts commit 47c145e9695b7ec0605c118aa98bd48c83de82bc. --- .github/workflows/publish.yml | 20 ++++++++++---------- README.md | 6 +++--- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0de6bd2325..ddfa1d98d7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,11 +47,11 @@ jobs: #output: versionType, fullVersion #fails if versionType is BAD, which interrupts the workflow run: ./gradlew qualifyVersionGha - - name: run checks - id: checks - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 - with: - arguments: check -Pjunit-tags=!slow -x jcstress +# - name: run checks +# id: checks +# uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 +# with: +# arguments: check -Pjunit-tags=!slow -x jcstress slowerChecks: # similar limitations as in prepare, but we parallelize slower tests here @@ -81,11 +81,11 @@ jobs: with: distribution: 'temurin' java-version: 8 - - name: run slower tests - id: slowerTests - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 - with: - arguments: reactor-core:test -Pjunit-tags=slow +# - name: run slower tests +# id: slowerTests +# uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 +# with: +# arguments: reactor-core:test -Pjunit-tags=slow #deploy the snapshot artifacts to Artifactory deploySnapshot: diff --git a/README.md b/README.md index 7a13235eef..e4f8e5c96a 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0-M3" - testCompile "io.projectreactor:reactor-test:3.6.0-M3" + compile "io.projectreactor:reactor-core:3.6.0-M2" + testCompile "io.projectreactor:reactor-test:3.6.0-M2" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0-M3" + // implementation "io.projectreactor:reactor-tools:3.6.0-M2" } ``` diff --git a/gradle.properties b/gradle.properties index ab667b97b6..e357cf47df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-M3 -bomVersion=2023.0.0-M3 -metricsMicrometerVersion=1.1.0-M3 +version=3.6.0-SNAPSHOT +bomVersion=2023.0.0-M2 +metricsMicrometerVersion=1.1.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 638691e79d..261a353c26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-M3" +micrometer = "1.12.0-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M2" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-SNAPSHOT" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From f599fca3a0fc969cb849a13c390f26f28a8c8dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 13 Sep 2023 13:15:39 +0200 Subject: [PATCH 175/312] [release] Prepare and release 3.6.0-M3 --- README.md | 6 +++--- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e4f8e5c96a..7a13235eef 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0-M2" - testCompile "io.projectreactor:reactor-test:3.6.0-M2" + compile "io.projectreactor:reactor-core:3.6.0-M3" + testCompile "io.projectreactor:reactor-test:3.6.0-M3" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0-M2" + // implementation "io.projectreactor:reactor-tools:3.6.0-M3" } ``` diff --git a/gradle.properties b/gradle.properties index e357cf47df..ab667b97b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-SNAPSHOT -bomVersion=2023.0.0-M2 -metricsMicrometerVersion=1.1.0-SNAPSHOT +version=3.6.0-M3 +bomVersion=2023.0.0-M3 +metricsMicrometerVersion=1.1.0-M3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 261a353c26..638691e79d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-SNAPSHOT" +micrometer = "1.12.0-M3" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-SNAPSHOT" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M2" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From 2bb7ddb5a1cc0080fb26c3abc6abd96079d621e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 13 Sep 2023 13:25:04 +0200 Subject: [PATCH 176/312] [release] Next development version 3.6.0-SNAPSHOT --- .github/workflows/publish.yml | 20 ++++++++++---------- gradle.properties | 4 ++-- gradle/libs.versions.toml | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ddfa1d98d7..0de6bd2325 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -47,11 +47,11 @@ jobs: #output: versionType, fullVersion #fails if versionType is BAD, which interrupts the workflow run: ./gradlew qualifyVersionGha -# - name: run checks -# id: checks -# uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 -# with: -# arguments: check -Pjunit-tags=!slow -x jcstress + - name: run checks + id: checks + uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 + with: + arguments: check -Pjunit-tags=!slow -x jcstress slowerChecks: # similar limitations as in prepare, but we parallelize slower tests here @@ -81,11 +81,11 @@ jobs: with: distribution: 'temurin' java-version: 8 -# - name: run slower tests -# id: slowerTests -# uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 -# with: -# arguments: reactor-core:test -Pjunit-tags=slow + - name: run slower tests + id: slowerTests + uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 + with: + arguments: reactor-core:test -Pjunit-tags=slow #deploy the snapshot artifacts to Artifactory deploySnapshot: diff --git a/gradle.properties b/gradle.properties index ab667b97b6..643808d631 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-M3 +version=3.6.0-SNAPSHOT bomVersion=2023.0.0-M3 -metricsMicrometerVersion=1.1.0-M3 +metricsMicrometerVersion=1.1.0-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 638691e79d..261a353c26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-M3" +micrometer = "1.12.0-SNAPSHOT" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -27,9 +27,9 @@ micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.2"} +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-M2" +micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.2.0-SNAPSHOT" micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } From b7628b437ba3596f49c39134cae557000cbffe93 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:32:30 +0300 Subject: [PATCH 177/312] fixes CI.yml (#3575) Signed-off-by: OlegDokuka --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48037f5883..b4556e949c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: distribution: 'temurin' java-version: 21-ea - name: Setup JDK 8 - - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 + uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -81,7 +81,7 @@ jobs: distribution: 'temurin' java-version: 21-ea - name: Setup JDK 8 - - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 + uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 with: distribution: 'temurin' java-version: 8 From a363dcd95ad7179e6a298e13e607318ae37127cb Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:44:24 +0300 Subject: [PATCH 178/312] adds CI nightly builds (#3581) Signed-off-by: OlegDokuka Co-authored-by: Violeta Georgieva --- .github/workflows/nightly.yml | 64 +++++++++++++++++++++++++++++++++++ gradle/libs.versions.toml | 11 +++--- settings.gradle | 14 +++++++- 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/nightly.yml diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000000..66239a10e3 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,64 @@ +name: Nightly Check + +on: + schedule: + - cron: "0 14 * * *" +permissions: read-all +jobs: + core-fast: + name: core fast tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: [ 3.5.x, main ] + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 + with: + ref: ${{ matrix.branch }} + - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 + with: + distribution: 'temurin' + java-version: 8 + - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 + name: gradle + with: + arguments: :reactor-core:test --no-daemon -Pjunit-tags=!slow -DuseSnapshotMicrometerVersion=true + core-slow: + name: core slower tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: [ 3.5.x, main ] + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 + with: + ref: ${{ matrix.branch }} + - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 + with: + distribution: 'temurin' + java-version: 8 + - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 + name: gradle + with: + arguments: :reactor-core:test --no-daemon -Pjunit-tags=slow -DuseSnapshotMicrometerVersion=true + other: + name: other tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: [ 3.5.x, main ] + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 + with: + ref: ${{ matrix.branch }} + - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 + with: + distribution: 'temurin' + java-version: 8 + - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 + name: other tests + with: + arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12120015dd..19dd02c0ce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,10 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.12-SNAPSHOT" +micrometer = "1.10.11" +micrometerDocsGenerator = "1.0.2" +micrometerTracingTest="1.0.10" +contextPropagation="1.0.5" kotlin = "1.5.32" reactiveStreams = "1.0.4" @@ -26,10 +29,10 @@ micrometer102Compatible-bom = { module = "io.micrometer:micrometer-bom", version micrometer-commons = { module = "io.micrometer:micrometer-commons" } micrometer-core = { module = "io.micrometer:micrometer-core" } micrometer-contextPropagation102 = "io.micrometer:context-propagation:1.0.2" -micrometer-contextPropagation = "io.micrometer:context-propagation:1.0.5" -micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version = "1.0.3-SNAPSHOT"} +micrometer-contextPropagation = { module = "io.micrometer:context-propagation", version.ref = "contextPropagation" } +micrometer-docsGenerator = { module = "io.micrometer:micrometer-docs-generator", version.ref = "micrometerDocsGenerator"} micrometer-observation-test = { module = "io.micrometer:micrometer-observation-test" } -micrometer-tracing-test = "io.micrometer:micrometer-tracing-integration-test:1.0.11-SNAPSHOT" +micrometer-tracing-test = { module = "io.micrometer:micrometer-tracing-integration-test", version.ref = "micrometerTracingTest" } micrometer-test = { module = "io.micrometer:micrometer-test" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } reactiveStreams = { module = "org.reactivestreams:reactive-streams", version.ref = "reactiveStreams" } diff --git a/settings.gradle b/settings.gradle index 29b44c3347..cf0fc795e0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,4 +23,16 @@ include 'benchmarks', 'reactor-core', 'reactor-test', 'reactor-tools', 'reactor- //libs catalog is declared in ./gradle/libs.versions.toml //TODO remove once Version Catalogs are stabilized. It is also activated in buildSrc -enableFeaturePreview("VERSION_CATALOGS") \ No newline at end of file +enableFeaturePreview("VERSION_CATALOGS") +dependencyResolutionManagement { + versionCatalogs { + libs { + if (System.getProperty("useSnapshotMicrometerVersion")) { + version('micrometer', '1.10.12-SNAPSHOT') + version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") + version('micrometerTracingTest', "1.0.11-SNAPSHOT") + version('contextPropagation', "1.0.6-SNAPSHOT") + } + } + } +} \ No newline at end of file From 06d60adc385ee089a1fb818571762fed72976b35 Mon Sep 17 00:00:00 2001 From: OlegDokuka Date: Wed, 20 Sep 2023 22:02:45 +0300 Subject: [PATCH 179/312] fixes nightly build Signed-off-by: OlegDokuka --- .github/workflows/nightly.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6852730fc2..489e0a11f0 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -43,30 +43,22 @@ jobs: with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=!slow -DuseSnapshotMicrometerVersion=true java-21-core-fast: - strategy: - fail-fast: false - matrix: - branch: [ 3.5.x, main ] - if: ${{ matrix.branch == 'main' }} name: Java 21 core fast tests runs-on: ubuntu-latest steps: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 with: - ref: ${{ matrix.branch }} + ref: main - name: Download JDK 9 - if: ${{ matrix.branch == 'main' }} run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ matrix.branch == 'main' }} uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ matrix.branch == 'main' }} uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 with: distribution: 'temurin' From 323a1c746bb55b45c7b3bf438905555375c695ad Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:42:29 +0300 Subject: [PATCH 180/312] disables temporarily java 21 tests (#3590) Signed-off-by: Oleh Dokuka --- .github/workflows/ci.yml | 66 +++++++++++++++++------------------ .github/workflows/publish.yml | 2 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4556e949c..8bfb1515fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,39 +89,39 @@ jobs: name: gradle with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=!slow - java-21-core-fast: - if: ${{ github.base_ref == 'main' }} - name: Java 21 core fast tests - runs-on: ubuntu-latest - needs: preliminary - steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 - - name: Download JDK 9 - if: ${{ github.base_ref == 'main' }} - run: ${GITHUB_WORKSPACE}/.github/setup.sh - shell: bash - - name: Setup JDK 9 - if: ${{ github.base_ref == 'main' }} - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 - with: - distribution: 'jdkfile' - java-version: 9.0.4 - jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - - name: Setup JDK 21 - if: ${{ github.base_ref == 'main' }} - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 - with: - distribution: 'temurin' - java-version: 21-ea - - name: Setup JDK 8 - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 - with: - distribution: 'temurin' - java-version: 8 - - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2 - name: gradle - with: - arguments: :reactor-core:java21Test --no-daemon -Pjunit-tags=!slow +# java-21-core-fast: +# if: ${{ github.base_ref == 'main' }} +# name: Java 21 core fast tests +# runs-on: ubuntu-latest +# needs: preliminary +# steps: +# - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 +# - name: Download JDK 9 +# if: ${{ github.base_ref == 'main' }} +# run: ${GITHUB_WORKSPACE}/.github/setup.sh +# shell: bash +# - name: Setup JDK 9 +# if: ${{ github.base_ref == 'main' }} +# uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 +# with: +# distribution: 'jdkfile' +# java-version: 9.0.4 +# jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz +# - name: Setup JDK 21 +# if: ${{ github.base_ref == 'main' }} +# uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 +# with: +# distribution: 'temurin' +# java-version: 21-ea +# - name: Setup JDK 8 +# uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 +# with: +# distribution: 'temurin' +# java-version: 8 +# - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef # tag=v2 +# name: gradle +# with: +# arguments: :reactor-core:java21Test --no-daemon -Pjunit-tags=!slow core-slow: name: core slower tests runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0de6bd2325..524504fadd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,7 +51,7 @@ jobs: id: checks uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # tag=v2 with: - arguments: check -Pjunit-tags=!slow -x jcstress + arguments: check -x :reactor-core:java21Test -Pjunit-tags=!slow -x jcstress slowerChecks: # similar limitations as in prepare, but we parallelize slower tests here From 966d3e35093e26ee9705a914ec4d2df15462731b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 2 Oct 2023 10:39:11 +0300 Subject: [PATCH 181/312] Bump actions/setup-java from 3.12.0 to 3.13.0 in /.github/workflows Signed-off-by: Oleh Dokuka --- .github/workflows/nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 66239a10e3..d520a8e93f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 with: ref: ${{ matrix.branch }} - - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 + - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 with: ref: ${{ matrix.branch }} - - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 + - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -54,7 +54,7 @@ jobs: - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3 with: ref: ${{ matrix.branch }} - - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # tag=v3 + - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' java-version: 8 From 768c9881ad7bd9194a205f0d545ef2e96287a8b8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:15:34 +0300 Subject: [PATCH 182/312] provides dedicated loom oriented BoundeElasticScheduler implementation (#3566) Signed-off-by: Oleh Dokuka --- gradle/toolchains.gradle | 16 + reactor-core/build.gradle | 1 + .../reactor/core/scheduler/Schedulers.java | 33 +- .../ThreadPerTaskBoundedElasticScheduler.java | 60 + .../core/scheduler/VirtualThreadFactory.java | 54 + .../BoundedElasticSchedulerSupplier.java | 6 +- .../ThreadPerTaskBoundedElasticScheduler.java | 1362 +++++++++++++++++ .../core/scheduler/VirtualThreadFactory.java | 64 + .../BoundedElasticSchedulerTest.java | 4 +- ...eadPerTaskBoundedElasticSchedulerTest.java | 80 + ...eadPerTaskBoundedElasticSchedulerTest.java | 764 +++++++++ .../test/scheduler/VirtualTimeScheduler.java | 7 +- 12 files changed, 2445 insertions(+), 6 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java create mode 100644 reactor-core/src/main/java/reactor/core/scheduler/VirtualThreadFactory.java create mode 100644 reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java create mode 100644 reactor-core/src/main/java21/reactor/core/scheduler/VirtualThreadFactory.java create mode 100644 reactor-core/src/test/java/reactor/core/scheduler/GenericThreadPerTaskBoundedElasticSchedulerTest.java create mode 100644 reactor-core/src/test/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticSchedulerTest.java diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle index c3a0f57905..89740bdf73 100644 --- a/gradle/toolchains.gradle +++ b/gradle/toolchains.gradle @@ -94,6 +94,22 @@ pluginManager.withPlugin("me.champeau.jmh") { } } +// Configure the JCStress plugin to use the toolchain for generating and running JCStress bytecode +pluginManager.withPlugin("io.github.reyerizo.gradle.jcstress") { + if (testToolchainConfigured()) { + tasks.matching {it.name.contains('jcstress') && it.hasProperty('javaLauncher') }.configureEach { + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(testToolchainLanguageVersion()) + }) + } + tasks.withType(JavaCompile).matching {it.name.contains("Jcstress")}.configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = testToolchainLanguageVersion() + } + } + } +} + // Store resolved Toolchain JVM information as custom values in the build scan. rootProject.ext { resolvedMainToolchain = false diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index a33680e7e6..f4562cd122 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -108,6 +108,7 @@ configurations { dependencies { // Reactive Streams api libs.reactiveStreams + java21Implementation libs.reactiveStreams tckTestImplementation (libs.reactiveStreams.tck) { /* Without this exclusion, testng brings an old version of junit (3.8.1). diff --git a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java index 632f78bc6c..5d7b4465f2 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java @@ -409,6 +409,14 @@ public static Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, Thre return fromFactory; } + static Scheduler newThreadPerTaskBoundedElastic(int threadCap, int queuedTaskCap, ThreadFactory threadFactory) { + Scheduler fromFactory = factory.newThreadPerTaskBoundedElastic(threadCap, + queuedTaskCap, + threadFactory); + fromFactory.init(); + return fromFactory; + } + /** * {@link Scheduler} that hosts a fixed pool of single-threaded ExecutorService-based * workers and is suited for parallel work. This type of {@link Scheduler} detects and @@ -986,7 +994,7 @@ public interface Factory { * Workers, reusing them once the Workers have been shut down. The underlying (user or daemon) * threads can be evicted if idle for more than {@code ttlSeconds}. *

    - * The maximum number of created thread pools is bounded by the provided {@code cap}. + * The maximum number of created thread pools is bounded by the provided {@code threadCap}. * * @param threadCap maximum number of underlying threads to create * @param queuedTaskCap maximum number of tasks to enqueue when no more threads can be created. Can be {@link Integer#MAX_VALUE} for unbounded enqueueing. @@ -1000,6 +1008,29 @@ default Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, ThreadFact return new BoundedElasticScheduler(threadCap, queuedTaskCap, threadFactory, ttlSeconds); } + /** + * {@link Scheduler} that dynamically creates a bounded number of Workers. + *

    + * The maximum number of created thread pools is bounded by the provided {@code threadCap}. + *

    + * The main difference between {@link BoundedElasticScheduler} and + * {@link ThreadPerTaskBoundedElasticScheduler} is that underlying machinery + * allocates a new thread for every new task which is one of the requirements + * for usage with {@link VirtualThread}s + *

    + * Note: for now this scheduler is available only in Java 21 runtime + * + * @param threadCap maximum number of underlying threads to create + * @param queuedTaskCap maximum number of tasks to enqueue when no more threads can be created. Can be {@link Integer#MAX_VALUE} for unbounded enqueueing. + * @param threadFactory a {@link ThreadFactory} to use each thread initialization + * + * @return a new {@link Scheduler} that dynamically creates workers with an upper bound to + * the number of backing threads + */ + default Scheduler newThreadPerTaskBoundedElastic(int threadCap, int queuedTaskCap, ThreadFactory threadFactory) { + return new ThreadPerTaskBoundedElasticScheduler(threadCap, queuedTaskCap, threadFactory); + } + /** * {@link Scheduler} that hosts a fixed pool of workers and is suited for parallel * work. diff --git a/reactor-core/src/main/java/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java b/reactor-core/src/main/java/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java new file mode 100644 index 0000000000..839b3072c4 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import reactor.core.Disposable; +import reactor.core.Scannable; + +final class ThreadPerTaskBoundedElasticScheduler + implements Scheduler, SchedulerState.DisposeAwaiter, Scannable { + + ThreadPerTaskBoundedElasticScheduler(int maxThreads, int maxTaskQueuedPerThread, ThreadFactory factory) { + throw new UnsupportedOperationException("Unsupported in JDK lower thank 21"); + } + + @Override + public boolean await(BoundedServices resource, long timeout, TimeUnit timeUnit) + throws InterruptedException { + return false; + } + + @Override + public Object scanUnsafe(Attr key) { + return null; + } + + @Override + public Disposable schedule(Runnable task) { + throw new UnsupportedOperationException("Unsupported in JDK lower thank 21"); + } + + @Override + public Worker createWorker() { + throw new UnsupportedOperationException("Unsupported in JDK lower thank 21"); + } + + static final class BoundedServices { + private BoundedServices() { + + } + + BoundedServices(ThreadPerTaskBoundedElasticScheduler parent) {} + } +} \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/core/scheduler/VirtualThreadFactory.java b/reactor-core/src/main/java/reactor/core/scheduler/VirtualThreadFactory.java new file mode 100644 index 0000000000..f4b1563c1c --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/scheduler/VirtualThreadFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import reactor.util.annotation.NonNull; +import reactor.util.annotation.Nullable; + +/** + * The noop {@link VirtualThread} Reactor {@link ThreadFactory} to be + * used with {@link ThreadPerTaskBoundedElasticScheduler}. It throws exceptions when is + * being created, so it indicates that current Java Runtime does not support + * {@link VirtualThread}s. + * + * @author Oleh Dokuka + */ +class VirtualThreadFactory implements ThreadFactory, + Thread.UncaughtExceptionHandler { + + + VirtualThreadFactory(String name, + boolean inheritThreadLocals, + @Nullable BiConsumer uncaughtExceptionHandler) { + throw new UnsupportedOperationException("Virtual Threads are not supported in JVM lower than 21"); + } + + @Override + public final Thread newThread(@NonNull Runnable runnable) { + throw new UnsupportedOperationException("Virtual Threads are not supported in JVM lower than 21"); + } + + @Override + public void uncaughtException(Thread t, Throwable e) { + throw new UnsupportedOperationException("Virtual Threads are not supported in JVM lower than 21"); + } +} diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java index e3d621e39d..44dd4bb5d7 100644 --- a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java +++ b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java @@ -24,6 +24,7 @@ import static reactor.core.scheduler.Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE; import static reactor.core.scheduler.Schedulers.LOOM_BOUNDED_ELASTIC; import static reactor.core.scheduler.Schedulers.newBoundedElastic; +import static reactor.core.scheduler.Schedulers.newThreadPerTaskBoundedElastic; /** * JDK 8 Specific implementation of BoundedElasticScheduler supplier which uses @@ -35,13 +36,12 @@ class BoundedElasticSchedulerSupplier implements Supplier { @Override public Scheduler get() { if (DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS) { - return newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, + return newThreadPerTaskBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, Thread.ofVirtual() .name(LOOM_BOUNDED_ELASTIC + "-", 1) .uncaughtExceptionHandler(Schedulers::defaultUncaughtException) - .factory(), - BoundedElasticScheduler.DEFAULT_TTL_SECONDS); + .factory()); } return newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java b/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java new file mode 100644 index 0000000000..209a2ac679 --- /dev/null +++ b/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java @@ -0,0 +1,1362 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.stream.Stream; + +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.Exceptions; +import reactor.core.Scannable; +import reactor.core.publisher.Mono; +import reactor.util.Logger; +import reactor.util.Loggers; +import reactor.util.annotation.Nullable; +import reactor.util.concurrent.Queues; + +import static reactor.core.scheduler.ThreadPerTaskBoundedElasticScheduler.BoundedServices.CREATING; +import static reactor.core.scheduler.ThreadPerTaskBoundedElasticScheduler.BoundedServices.SHUTDOWN; +import static reactor.core.scheduler.ThreadPerTaskBoundedElasticScheduler.BoundedServices; +import static reactor.core.scheduler.Schedulers.onSchedule; + +final class ThreadPerTaskBoundedElasticScheduler + implements Scheduler, SchedulerState.DisposeAwaiter, Scannable { + + static final Logger LOGGER = Loggers.getLogger(ThreadPerTaskBoundedElasticScheduler.class); + + final int maxThreads; + final int maxTasksQueuedPerThread; + + final ThreadFactory factory; + + volatile SchedulerState state; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater STATE = + AtomicReferenceFieldUpdater.newUpdater(ThreadPerTaskBoundedElasticScheduler.class, SchedulerState.class, "state"); + + private static final SchedulerState INIT = + SchedulerState.init(SHUTDOWN); + + + + /** + * Create a {@link ThreadPerTaskBoundedElasticScheduler} with the given configuration. Note that backing threads + * (or executors) can be shared by each {@link reactor.core.scheduler.Scheduler.Worker}, so each worker + * can contribute to the task queue size. + * + * @param maxThreads the maximum number of backing threads to spawn, must be strictly positive + * @param maxTasksQueuedPerThread the maximum amount of tasks an executor can queue up + * @param threadFactory the {@link ThreadFactory} to name the backing threads + */ + ThreadPerTaskBoundedElasticScheduler(int maxThreads, int maxTasksQueuedPerThread, ThreadFactory threadFactory) { + if (maxThreads <= 0) { + throw new IllegalArgumentException("maxThreads must be strictly positive, was " + maxThreads); + } + if (maxTasksQueuedPerThread <= 0) { + throw new IllegalArgumentException("maxTasksQueuedPerThread must be strictly positive, was " + maxTasksQueuedPerThread); + } + this.maxThreads = maxThreads; + this.maxTasksQueuedPerThread = maxTasksQueuedPerThread; + this.factory = threadFactory; + + STATE.lazySet(this, INIT); + } + + @Override + public boolean isDisposed() { + // we only consider disposed as actually shutdown + SchedulerState current = this.state; + return current != INIT && current.currentResource == SHUTDOWN; + } + + @Override + public void init() { + for (;;) { + SchedulerState a = this.state; + if (a != INIT) { + if (a.currentResource == SHUTDOWN) { + throw new IllegalStateException( + "Initializing a disposed scheduler is not permitted"); + } + // return early - scheduler already initialized + return; + } + + SchedulerState b = SchedulerState.init(new BoundedServices(this)); + if (!STATE.compareAndSet(this, INIT, b)) { + return; + } + } + } + + @Override + @Deprecated + public void start() { + throw new UnsupportedOperationException("Use init method instead"); + } + + @Override + public boolean await(BoundedServices boundedServices, long timeout, TimeUnit timeUnit) throws InterruptedException { + if (!boundedServices.sharedDelayedTasksScheduler.awaitTermination(timeout, timeUnit)) { + return false; + } + for (SequentialThreadPerTaskExecutor bs : boundedServices.activeExecutorsState.array) { + if (!bs.await(timeout, timeUnit)) { + return false; + } + } + return true; + } + + @Override + public void dispose() { + SchedulerState previous = state; + + if (previous.currentResource == SHUTDOWN) { + // A dispose process might be ongoing, but we want a forceful shutdown, + // so we do our best to release the resources without updating the state. + if (previous.initialResource != null) { + previous.initialResource.sharedDelayedTasksScheduler.shutdownNow(); + for (SequentialThreadPerTaskExecutor bs : previous.initialResource.activeExecutorsState.array) { + bs.shutdown(true); + } + } + return; + } + + final SequentialThreadPerTaskExecutor[] toAwait = previous.currentResource.dispose(); + SchedulerState shutDown = SchedulerState.transition( + previous.currentResource, + SHUTDOWN, this + ); + + STATE.compareAndSet(this, previous, shutDown); + // If unsuccessful - either another thread disposed or restarted - no issue, + // we only care about the one stored in shutDown. + + assert shutDown.initialResource != null; + shutDown.initialResource.sharedDelayedTasksScheduler.shutdownNow(); + for (SequentialThreadPerTaskExecutor bs : toAwait) { + bs.shutdown(true); + } + } + + @Override + public Mono disposeGracefully() { + return Mono.defer(() -> { + SchedulerState previous = state; + + if (previous.currentResource == SHUTDOWN) { + return previous.onDispose; + } + + final SequentialThreadPerTaskExecutor[] toAwait = previous.currentResource.dispose(); + SchedulerState shutDown = SchedulerState.transition( + previous.currentResource, + SHUTDOWN, this + ); + + STATE.compareAndSet(this, previous, shutDown); + // If unsuccessful - either another thread disposed or restarted - no issue, + // we only care about the one stored in shutDown. + + assert shutDown.initialResource != null; + shutDown.initialResource.sharedDelayedTasksScheduler.shutdown(); + for (SequentialThreadPerTaskExecutor bs : toAwait) { + bs.shutdown(false); + } + return shutDown.onDispose; + }); + } + + @Override + public Disposable schedule(Runnable task) { + //tasks running once will call dispose on the SingleThreadExecutor, decreasing its usage by one + SequentialThreadPerTaskExecutor picked = state.currentResource.pickOrAllocate(); + try { + return picked.schedule(task, null); + } + catch (RejectedExecutionException ex) { + // ensure to free the SingleThreadExecutor so it can be reused + picked.dispose(); + throw ex; + } + } + + @Override + public Disposable schedule(Runnable task, long delay, TimeUnit unit) { + //tasks running once will call dispose on the SingleThreadExecutor, decreasing its usage by one + final SequentialThreadPerTaskExecutor picked = state.currentResource.pickOrAllocate(); + try { + return picked.schedule(task, delay, unit, null); + } + catch (RejectedExecutionException ex) { + // ensure to free the SingleThreadExecutor so it can be reused + picked.dispose(); + throw ex; + } + } + + @Override + public Disposable schedulePeriodically(Runnable task, + long initialDelay, + long period, + TimeUnit unit) { + final SequentialThreadPerTaskExecutor picked = state.currentResource.pickOrAllocate(); + try { + return picked.schedulePeriodically(task, + initialDelay, + period, + unit, + null); + } + catch (RejectedExecutionException ex) { + // ensure to free the SingleThreadExecutor so it can be reused + picked.dispose(); + throw ex; + } + } + + @Override + public String toString() { + StringBuilder ts = new StringBuilder(Schedulers.BOUNDED_ELASTIC) + .append('('); + if (factory instanceof ReactorThreadFactory) { + ts.append('\"').append(((ReactorThreadFactory) factory).get()).append("\","); + } + ts.append("maxThreads=").append(maxThreads) + .append(",maxTasksQueuedPerThread=").append( + maxTasksQueuedPerThread == Integer.MAX_VALUE ? "unbounded" : + maxTasksQueuedPerThread); + return ts.toString(); + } + + /** + * @return a best effort total count of the spinned up executors + */ + int estimateSize() { + return state.currentResource.activeExecutorsState.array.length; + } + + /** + * Best effort snapshot of the remaining queue capacity for pending tasks across all the backing executors. + * + * @return the total task capacity + */ + int estimateRemainingTaskCapacity() { + if (maxTasksQueuedPerThread == Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + + SequentialThreadPerTaskExecutor[] busyArray = state.currentResource.activeExecutorsState.array; + + long numberOfTotalAvailableSlots = 0; + for (SequentialThreadPerTaskExecutor state : busyArray) { + numberOfTotalAvailableSlots += state.numberOfAvailableSlots(); + } + + numberOfTotalAvailableSlots += (maxThreads - busyArray.length) * (long) maxTasksQueuedPerThread; + + return (int) Math.min(numberOfTotalAvailableSlots, Integer.MAX_VALUE); + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.TERMINATED || key == Attr.CANCELLED) return isDisposed(); + if (key == Attr.BUFFERED) return estimateSize(); + if (key == Attr.CAPACITY) return maxThreads; + if (key == Attr.NAME) return this.toString(); + + return null; + } + + @Override + public Stream inners() { + BoundedServices services = state.currentResource; + return Stream.of(services.activeExecutorsState.array) + .filter(obj -> obj != null && obj != CREATING); + } + + @Override + public Worker createWorker() { + return new SingleThreadExecutorWorker(state.currentResource.pickOrAllocate()); + } + + static final class BoundedServices { + + static final class ActiveExecutorsState { + final SequentialThreadPerTaskExecutor[] array; + final boolean shutdown; + + public ActiveExecutorsState(SequentialThreadPerTaskExecutor[] array, boolean shutdown) { + this.array = array; + this.shutdown = shutdown; + } + } + + static final ActiveExecutorsState INITIAL = new ActiveExecutorsState(new SequentialThreadPerTaskExecutor[0], false); + static final ActiveExecutorsState ALL_SHUTDOWN = new ActiveExecutorsState(new SequentialThreadPerTaskExecutor[0], true); + static final ScheduledExecutorService DELAYED_TASKS_SCHEDULER_SHUTDOWN; + + static final BoundedServices SHUTDOWN; + static final BoundedServices SHUTTING_DOWN; + static final SequentialThreadPerTaskExecutor CREATING; + + static { + DELAYED_TASKS_SCHEDULER_SHUTDOWN = Executors.newSingleThreadScheduledExecutor(); + DELAYED_TASKS_SCHEDULER_SHUTDOWN.shutdownNow(); + + SHUTDOWN = new BoundedServices(); + SHUTTING_DOWN = new BoundedServices(); + SHUTDOWN.dispose(); + SHUTTING_DOWN.dispose(); + + CREATING = new SequentialThreadPerTaskExecutor(SHUTDOWN, false) { + @Override + public String toString() { + return "CREATING SingleThreadExecutor"; + } + }; + CREATING.wipAndRefCnt = -1; //always -1, ensures tryPick never returns true + } + + static final AtomicLong DELAYED_TASKS_SCHEDULER_COUNTER = new AtomicLong(); + static final ThreadFactory DELAYED_TASKS_SCHEDULER_FACTORY = r -> { + Thread t = new Thread(r, + Schedulers.LOOM_BOUNDED_ELASTIC + "-delayed-tasks-scheduler-" + DELAYED_TASKS_SCHEDULER_COUNTER.incrementAndGet()); + t.setDaemon(true); + return t; + }; + + final ThreadPerTaskBoundedElasticScheduler parent; + final ScheduledExecutorService sharedDelayedTasksScheduler; + final ThreadFactory factory; + final int maxTasksQueuedPerThread; + + volatile ActiveExecutorsState activeExecutorsState; + static final AtomicReferenceFieldUpdater ACTIVE_EXECUTORS_STATE = + AtomicReferenceFieldUpdater.newUpdater(BoundedServices.class, ActiveExecutorsState.class, + "activeExecutorsState"); + + //constructor for SHUTDOWN + private BoundedServices() { + this.parent = null; + this.maxTasksQueuedPerThread = 0; + this.factory = null; + this.activeExecutorsState = ALL_SHUTDOWN; + this.sharedDelayedTasksScheduler = DELAYED_TASKS_SCHEDULER_SHUTDOWN; + } + + BoundedServices(ThreadPerTaskBoundedElasticScheduler parent) { + this.parent = parent; + this.maxTasksQueuedPerThread = parent.maxTasksQueuedPerThread; + this.factory = parent.factory; + this.sharedDelayedTasksScheduler = new ScheduledThreadPoolExecutor(1, DELAYED_TASKS_SCHEDULER_FACTORY); + + ACTIVE_EXECUTORS_STATE.lazySet(this, INITIAL); + } + + void remove(SequentialThreadPerTaskExecutor sequentialThreadPerTaskExecutor) { + for(;;) { + ActiveExecutorsState current = activeExecutorsState; + SequentialThreadPerTaskExecutor[] arr = activeExecutorsState.array; + int len = arr.length; + + if (len == 0 || current.shutdown) { + return; + } + + ActiveExecutorsState replacement = null; + if (len == 1) { + if (arr[0] == sequentialThreadPerTaskExecutor) { + replacement = INITIAL; + } + } + else { + for (int i = 0; i < len; i++) { + SequentialThreadPerTaskExecutor state = arr[i]; + if (state == sequentialThreadPerTaskExecutor) { + replacement = new ActiveExecutorsState( + new SequentialThreadPerTaskExecutor[len - 1], false + ); + System.arraycopy(arr, 0, replacement.array, 0, i); + System.arraycopy(arr, i + 1, replacement.array, i, len - i - 1); + break; + } + } + } + if (replacement == null) { + //bounded state not found, ignore + return; + } + if (ACTIVE_EXECUTORS_STATE.compareAndSet(this, current, replacement)) { + return; + } + } + } + + SequentialThreadPerTaskExecutor pickOrAllocate() { + for (;;) { + ActiveExecutorsState activeState = activeExecutorsState; + if (activeState == ALL_SHUTDOWN || activeState.shutdown) { + return CREATING; //synonym for shutdown, since the underlying executor is shut down + } + + SequentialThreadPerTaskExecutor[] arr = activeState.array; + int len = arr.length; + + if (len < parent.maxThreads) { + //try to build a new resource + SequentialThreadPerTaskExecutor + newExecutor = new SequentialThreadPerTaskExecutor(this, true); + + SequentialThreadPerTaskExecutor[] replacement = new SequentialThreadPerTaskExecutor[len + 1]; + System.arraycopy(arr, 0, replacement, 0, len); + replacement[len] = newExecutor; + if (ACTIVE_EXECUTORS_STATE.compareAndSet(this, activeState, new ActiveExecutorsState(replacement, false))) { + return newExecutor; + } + //else optimistically retry (implicit continue here) + } + else { + SequentialThreadPerTaskExecutor choice = arr[0]; + int leastBusy = Integer.MAX_VALUE; + + for (int i = 0; i < len; i++) { + SequentialThreadPerTaskExecutor state = arr[i]; + int busy = state.refCnt(); + if (busy < leastBusy) { + leastBusy = busy; + choice = state; + } + } + + if (choice.retain()) { + return choice; + } + //else optimistically retry (implicit continue here) + } + } + } + + public SequentialThreadPerTaskExecutor[] dispose() { + ActiveExecutorsState current; + for (;;) { + current = activeExecutorsState; + + if (current.shutdown) { + return current.array; + } + + if (ACTIVE_EXECUTORS_STATE.compareAndSet(this, + current, new ActiveExecutorsState(current.array, true))) { + break; + } + } + + return Arrays.copyOf(current.array, current.array.length); + } + } + + static class SequentialThreadPerTaskExecutor extends CountDownLatch implements Disposable, Scannable { + + final BoundedServices parent; + + final int queueCapacity; + + final Queue tasksQueue; + final ScheduledExecutorService scheduledTasksExecutor; + + volatile long size; + static final AtomicLongFieldUpdater SIZE = + AtomicLongFieldUpdater.newUpdater( + SequentialThreadPerTaskExecutor.class, "size"); + + volatile long wipAndRefCnt; + static final VarHandle WIP_AND_REF_CNT; + + static { + try { + WIP_AND_REF_CNT = MethodHandles.lookup() + .findVarHandle(SequentialThreadPerTaskExecutor.class, "wipAndRefCnt", Long.TYPE); + } + catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + final ThreadFactory factory; + + SchedulerTask activeTask; + + SequentialThreadPerTaskExecutor(BoundedServices parent, boolean markPicked) { + super(1); + this.parent = parent; + this.tasksQueue = Queues.unboundedMultiproducer().get(); + this.queueCapacity = parent.maxTasksQueuedPerThread; + this.factory = parent.factory; + this.scheduledTasksExecutor = parent.sharedDelayedTasksScheduler; + + if (markPicked) { + WIP_AND_REF_CNT.set(this, 1L << 31); + } + } + + void incrementTasksCount() { + for (; ; ) { + long size = this.size; + + if (canNotAcceptTasks(size)) { + throw Exceptions.failWithRejected(); + } + + long nextSize = size + 1; + if (queueCapacity != Integer.MAX_VALUE && nextSize > queueCapacity) { + throw Exceptions.failWithRejected( + "Task capacity of bounded elastic scheduler reached while scheduling a new tasks (" + nextSize + "/" + queueCapacity + ")"); + } + + if (SIZE.compareAndSet(this, size, nextSize)) { + return; + } + } + } + + void decrementTasksCount() { + long actualState = SIZE.decrementAndGet(this); + + if (canNotAcceptTasks(actualState) && tasksCount(actualState) == 0) { + trySchedule(); + } + } + + void stopAcceptingTasks() { + for (;;) { + long size = this.size; + + if (canNotAcceptTasks(size)) { + return; + } + + if (SIZE.weakCompareAndSet(this, size, size | Long.MIN_VALUE)) { + return; + } + } + } + + long numberOfEnqueuedTasks() { + return tasksCount(this.size); + } + + int numberOfAvailableSlots() { + if (queueCapacity == Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + + return queueCapacity - (int) numberOfEnqueuedTasks(); + } + + /** + * Try to mark this {@link SequentialThreadPerTaskExecutor} as picked. + * + * @return true if this state could atomically be marked as picked, false if + * eviction started on it in the meantime + */ + boolean retain() { + long previousState = retain(this); + + return !isShutdown(previousState); + } + + /** + * Release the {@link SequentialThreadPerTaskExecutor}, ie atomically decrease the counter of times it has been picked + * and remove from the list of active executors if that counter reaches 0. + * This is called when a worker is done using the executor. {@link #dispose()} is an alias + * to this method (for APIs that take a {@link Disposable}). + * + * @see #shutdown(boolean) + * @see #dispose() + */ + void release() { + long previousState = release(this); + if (isShutdown(previousState)) { + return; + } + + if (refCnt(previousState) == 1) { + stopAcceptingTasks(); + parent.remove(this); + + if (!hasWork(previousState)) { + clearAllTask(); + countDown(); + } + } + } + + int refCnt() { + return refCnt(this.wipAndRefCnt); + } + + /** + * Forcibly shut down the executor. Can only be called from the parent + * {@link BoundedServices} during {@link BoundedServices#dispose()} or + * {@link BoundedServices#disposeGracefully()} + * + * @see #release() + * @see #dispose() + */ + void shutdown(boolean now) { + long previousState = markShutdown(this, now); + + if (isShutdown(previousState)) { + return; + } + + stopAcceptingTasks(); + + if (hasWork(previousState)) { + return; + } + + drain(); + } + + /** + * An alias for {@link #release()}. + */ + @Override + public void dispose() { + this.release(); + } + + Disposable schedule(Runnable task, @Nullable Composite disposables) { + incrementTasksCount(); + + Runnable decoratedTask = onSchedule(task); + boolean isDirect = disposables == null; + SchedulerTask disposable = + new SchedulerTask(this, decoratedTask, -1, TimeUnit.NANOSECONDS, disposables); + + if (!isDirect) { + if (!disposables.add(disposable)) { + throw Exceptions.failWithRejected(); + } + } + + this.tasksQueue.offer(disposable); + + trySchedule(); + + return disposable; + } + + Disposable schedule(Runnable task, long delay, TimeUnit unit, @Nullable Composite disposables) { + Objects.requireNonNull(unit, "TimeUnit should be non-null"); + + incrementTasksCount(); + + Runnable decoratedTask = onSchedule(task); + boolean isDirect = disposables == null; + SchedulerTask disposable = + new SchedulerTask(this, decoratedTask, -1, unit, disposables); + + if (!isDirect) { + if (!disposables.add(disposable)) { + throw Exceptions.failWithRejected(); + } + } + + if (delay <= 0) { + this.tasksQueue.offer(disposable); + + trySchedule(); + } + else { + disposable.schedule(delay, unit); + } + + return disposable; + } + + Disposable schedulePeriodically(Runnable task, + long initialDelay, + long period, + TimeUnit unit, + @Nullable Composite disposables) { + Objects.requireNonNull(unit, "TimeUnit should be non-null"); + + incrementTasksCount(); + + Runnable decoratedTask = onSchedule(task); + boolean isDirect = disposables == null; + + SchedulerTask disposable = new SchedulerTask(this, + decoratedTask, + period < 0 ? 0 : period, + unit, + disposables); + + if (!isDirect) { + if (!disposables.add(disposable)) { + throw Exceptions.failWithRejected(); + } + } + + if (period <= 0) { + if (initialDelay <= 0) { + this.tasksQueue.offer(disposable); + + trySchedule(); + } + else { + disposable.schedule(initialDelay, unit); + } + } + else { + disposable.scheduleAtFixedRate(initialDelay, period, unit); + } + + return disposable; + } + + /** + * Is being called from public API and tries to add work and then execute it + */ + void trySchedule() { + long previousState = addWork(this); + if (hasWork(previousState) || isShutdownNow(previousState)) { + return; + } + + drain(); + } + + void drain() { + final Queue q = this.tasksQueue; + + long state = this.wipAndRefCnt; + for (;;) { + for (;;) { + if (isShutdownNow(this.wipAndRefCnt)) { + clearAllTask(); + countDown(); + return; + } + + final SchedulerTask task = q.poll(); + + if (task == null) { + break; + } + + this.activeTask = task; + if (task.start()) { + return; + } + } + + state = markWorkDone(this, state); + + if (isShutdown(state) && numberOfEnqueuedTasks() == 0) { + countDown(); + return; + } + + if (!hasWork(state)) { + return; + } + } + } + + void clearAllTask() { + SchedulerTask activeTask = this.activeTask; + if (activeTask != null) { + activeTask.dispose(); + } + + Queue q = this.tasksQueue; + + SchedulerTask d; + while ((d = q.poll()) != null) { + d.dispose(); + } + } + + /** + * Is this {@link SequentialThreadPerTaskExecutor} still in use by workers. + * + * @return true if in use, false if it has been disposed enough times + */ + @Override + public boolean isDisposed() { + return isShutdown(this.wipAndRefCnt) && numberOfEnqueuedTasks() == 0; + } + + @Override + public Object scanUnsafe(Attr key) { + if (Attr.TERMINATED == key) return isDisposed(); + if (Attr.BUFFERED == key) return numberOfEnqueuedTasks(); + if (Attr.CAPACITY == key) return this.queueCapacity; + return null; + } + + @Override + public String toString() { + return "SingleThreadExecutor@" + System.identityHashCode(this) + "{" + " backing=" + refCnt(this.wipAndRefCnt) + '}'; + } + + static boolean canNotAcceptTasks(long state) { + return (state & Long.MIN_VALUE) == Long.MIN_VALUE; + } + + static long tasksCount(long state) { + return state & Long.MAX_VALUE; + } + + static final long WIP_MASK = + 0b0000_0000_0000_0000_0000_0000_0000_0000_0111_1111_1111_1111_1111_1111_1111_1111L; + static final long REF_CNT_MASK = + 0b0011_1111_1111_1111_1111_1111_1111_1111_1000_0000_0000_0000_0000_0000_0000_0000L; + + static final long SHUTDOWN_FLAG = + 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long SHUTDOWN_NOW_FLAG = + 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + + static int refCnt(long state) { + return (int) ((state & REF_CNT_MASK) >> 31); + } + + static long retain(SequentialThreadPerTaskExecutor instance) { + for (;;) { + long state = instance.wipAndRefCnt; + + if (isShutdown(state)) { + return state; + } + + long nextState = incrementRefCnt(state); + if (WIP_AND_REF_CNT.weakCompareAndSet(instance, state, nextState)) { + return state; + } + } + } + + static long incrementRefCnt(long state) { + long rawRefCnt = state & REF_CNT_MASK; + return (rawRefCnt) == REF_CNT_MASK ? state : (rawRefCnt >> 31 + 1) << 31 | (state &~ REF_CNT_MASK); + } + + static long release(SequentialThreadPerTaskExecutor instance) { + for (;;) { + long state = instance.wipAndRefCnt; + + if (isShutdown(state)) { + return state; + } + + long refCnt = (state & REF_CNT_MASK) >> 31; + long nextRefCnt = refCnt - 1; + long nextState; + if (nextRefCnt == 0) { + nextState = + incrementWork(state & ~REF_CNT_MASK | SHUTDOWN_NOW_FLAG | SHUTDOWN_FLAG); + } else { + nextState = (refCnt) == 0 ? state : nextRefCnt << 31 | (state & ~REF_CNT_MASK); + } + if (WIP_AND_REF_CNT.weakCompareAndSetPlain(instance, state, nextState)) { + return state; + } + } + } + + static long markShutdown(SequentialThreadPerTaskExecutor instance, boolean now) { + for (;;) { + long state = instance.wipAndRefCnt; + + if (isShutdownNow(state) || (!now && isShutdown(state))) { + return state; + } + + if (WIP_AND_REF_CNT.weakCompareAndSetPlain(instance, state, state | SHUTDOWN_FLAG | (now ? SHUTDOWN_NOW_FLAG : 0))) { + return state; + } + } + } + + static boolean isShutdown(long state) { + return (state & SHUTDOWN_FLAG) == SHUTDOWN_FLAG; + } + + static boolean isShutdownNow(long state) { + return (state & SHUTDOWN_NOW_FLAG) == SHUTDOWN_NOW_FLAG; + } + + static boolean hasWork(long state) { + return (state & WIP_MASK) > 0; + } + + static long markWorkDone(SequentialThreadPerTaskExecutor instance, long expectedState) { + for (;;) { + long currentState = instance.wipAndRefCnt; + + if (expectedState != currentState) { + return currentState; + } + + long nextState = currentState &~ WIP_MASK; + if (WIP_AND_REF_CNT.weakCompareAndSetPlain(instance, currentState, nextState)) { + return nextState; + } + } + } + + static long addWork(SequentialThreadPerTaskExecutor instance) { + for (;;) { + long state = instance.wipAndRefCnt; + + long nextState = incrementWork(state); + if (WIP_AND_REF_CNT.weakCompareAndSetPlain(instance, state, nextState)) { + return state; + } + } + } + + static long incrementWork(long state) { + return ((state & WIP_MASK) == WIP_MASK ? (state &~ WIP_MASK) : state) + 1; + } + } + + final static class SchedulerTask extends AtomicInteger + implements Disposable, Callable, Runnable { + + static final int INITIAL_STATE = 0b0000_0000_0000_0000_0000_0000_0000_0000; + static final int SCHEDULED_STATE = 0b0000_0000_0000_0000_0000_0000_0000_0001; + static final int STARTING_STATE = 0b0000_0000_0000_0000_0000_0000_0000_0010; + static final int RUNNING_STATE = 0b0000_0000_0000_0000_0000_0000_0000_0100; + static final int COMPLETED_STATE = 0b0000_0000_0000_0000_0000_0000_0000_1000; + static final int DISPOSED_FLAG = 0b1000_0000_0000_0000_0000_0000_0000_0000; + static final int HAS_FUTURE_FLAG = 0b0000_1000_0000_0000_0000_0000_0000_0000; + + final long fixedRatePeriod; + final TimeUnit timeUnit; + final SequentialThreadPerTaskExecutor holder; + + final Runnable task; + @Nullable + final Composite tracker; + + Thread carrier; + + Future scheduledFuture; + + SchedulerTask(SequentialThreadPerTaskExecutor holder, + Runnable task, + long fixedRatePeriod, + TimeUnit timeUnit, + @Nullable Composite tracker) { + this.fixedRatePeriod = fixedRatePeriod; + this.timeUnit = timeUnit; + this.holder = holder; + this.task = task; + this.tracker = tracker; + } + + @Override + public void run() { + int previousState = markRunning(this); + + if (isDisposed(previousState)) { + this.holder.drain(); + return; + } + + try { + task.run(); + } + catch (Throwable ex) { + boolean handled = false; + try { + Schedulers.handleError(ex); + handled = true; + } + finally { + if (!handled) { + if (isPeriodic()) { + this.holder.decrementTasksCount(); + } + + if (this.tracker == null) { + this.holder.release(); + } + else { + this.tracker.remove(this); + } + this.holder.drain(); + } + } + } + + + if (isPeriodic()) { + boolean isInstant = this.fixedRatePeriod == 0; + + previousState = isInstant ? markInitial(this) : markRescheduled(this); + boolean isDisposed = isDisposed(previousState); + boolean isShutdown = holder.isDisposed(); + + if (isInstant) { + if (!isDisposed && !isShutdown) { + // we do not schedule task since execution time is greater than + // fixed rate period which means we need to run this task right away + this.holder.tasksQueue.offer(this); + } + } + + if (isDisposed || isShutdown) { + this.holder.decrementTasksCount(); + if (this.tracker == null) { + this.holder.release(); + } + } + + // and drain next task if available + this.holder.drain(); + return; + } + + markCompleted(this); + + if (this.tracker == null) { + this.holder.release(); + } + else { + this.tracker.remove(this); + } + + this.holder.drain(); + } + + @Override + public Void call() { + if (isDisposed()) { + return null; + } + + final SequentialThreadPerTaskExecutor parent = this.holder; + + parent.tasksQueue.offer(this); + parent.trySchedule(); + return null; + } + + @Override + public void dispose() { + int previousState = markDisposed(this); + + if (isDisposed(previousState) || isCompleted(previousState)) { + return; + } + + boolean isDirect = this.tracker == null; + if (!isDirect) { + this.tracker.remove(this); + } + + if (isScheduled(previousState)) { + this.scheduledFuture.cancel(true); + this.holder.decrementTasksCount(); + if (isDirect) { + this.holder.release(); + } + return; + } + + if (isRunning(previousState)) { + if (hasFuture(previousState)) { + this.scheduledFuture.cancel(true); + } + this.carrier.interrupt(); + return; + } + + if (isInitialState(previousState)) { + this.holder.decrementTasksCount(); + } + else if (isPeriodic()) { + if (hasFuture(previousState)) { + this.scheduledFuture.cancel(true); + } + this.holder.decrementTasksCount(); + } + + if (isDirect) { + holder.release(); + } + + // don't do anything else, this task is marked as disposed so once it is + // drained, the disposed flag should be observed and the task skipped + } + + @Override + public boolean isDisposed() { + int state = get(); + return isDisposed(state) || isCompleted(state); + } + + boolean start() { + int previousState = markStarting(this); + if (isDisposed(previousState)) { + return false; + } + + Thread carrier = this.holder.factory.newThread(this); + this.carrier = carrier; + + if (!isPeriodic()) { + this.holder.decrementTasksCount(); + } + + carrier.start(); + + return true; + } + + boolean isPeriodic() { + return fixedRatePeriod >= 0; + } + + void schedule(long delay, TimeUnit unit) { + final ScheduledFuture future = this.holder.scheduledTasksExecutor.schedule((Callable) this, delay, unit); + this.scheduledFuture = future; + + final int previousState = markScheduled(this); + if (isDisposed(previousState)) { + future.cancel(true); + } + } + + void scheduleAtFixedRate(long initialDelay, long delay, TimeUnit unit) { + final ScheduledFuture future = + this.holder.scheduledTasksExecutor.scheduleAtFixedRate(this::call, initialDelay, delay, unit); + this.scheduledFuture = future; + + final int previousState = markScheduled(this); + if (isDisposed(previousState)) { + future.cancel(true); + } + } + + static boolean isInitialState(int state) { + return state == 0; + } + + static boolean isStarting(int state) { + return (state & STARTING_STATE) == STARTING_STATE; + } + + static boolean isRunning(int state) { + return (state & RUNNING_STATE) == RUNNING_STATE; + } + + static boolean isCompleted(int state) { + return (state & COMPLETED_STATE) == COMPLETED_STATE; + } + + static boolean isScheduled(int state) { + return (state & SCHEDULED_STATE) == SCHEDULED_STATE; + } + + static boolean isDisposed(int state) { + return (state & DISPOSED_FLAG) == DISPOSED_FLAG; + } + + static boolean hasFuture(int state) { + return (state & HAS_FUTURE_FLAG) == HAS_FUTURE_FLAG; + } + + static int markInitial(SchedulerTask disposable) { + for (; ; ) { + int state = disposable.get(); + + if (isDisposed(state)) { + return state; + } + + if (disposable.weakCompareAndSetPlain(state, INITIAL_STATE)) { + return state; + } + } + } + + static int markStarting(SchedulerTask disposable) { + for (;;) { + int state = disposable.get(); + + if (isDisposed(state)) { + return state; + } + + if (disposable.weakCompareAndSetPlain(state, (state & HAS_FUTURE_FLAG) | STARTING_STATE)) { + return state; + } + } + } + + static int markRunning(SchedulerTask disposable) { + for (;;) { + int state = disposable.get(); + + if (isDisposed(state)) { + return state; + } + + if (disposable.weakCompareAndSetPlain(state, (state & HAS_FUTURE_FLAG) | RUNNING_STATE)) { + return state; + } + } + } + + static int markScheduled(SchedulerTask disposable) { + for (;;) { + int state = disposable.get(); + + if (isDisposed(state)) { + return state; + } + + if (disposable.weakCompareAndSetRelease(state, !isInitialState(state) ? HAS_FUTURE_FLAG : SCHEDULED_STATE | HAS_FUTURE_FLAG)) { + return state; + } + } + } + + static int markRescheduled(SchedulerTask disposable) { + for (;;) { + int state = disposable.get(); + + if (isDisposed(state)) { + return state; + } + + if (disposable.weakCompareAndSetRelease(state, HAS_FUTURE_FLAG | SCHEDULED_STATE)) { + return state; + } + } + } + + static int markDisposed(SchedulerTask disposable) { + for (;;) { + int state = disposable.get(); + + if (isDisposed(state) || isCompleted(state)) { + return state; + } + + if (disposable.weakCompareAndSetAcquire(state, state | DISPOSED_FLAG)) { + return state; + } + } + } + + static void markCompleted(SchedulerTask disposable) { + int state = disposable.get(); + + if (isDisposed(state)) { + return; + } + + disposable.weakCompareAndSetPlain(state, COMPLETED_STATE); + } + + @Override + public String toString() { + return "SchedulerTask(" + hashCode() +"){" + "carrier=" + carrier + ", " + + "scheduledFuture=" + scheduledFuture + "state= " + Integer.toBinaryString(get()) + '}'; + } + } + + static class SingleThreadExecutorWorker implements Worker, Disposable, Scannable { + + final Composite disposables; + final SequentialThreadPerTaskExecutor executor; + + SingleThreadExecutorWorker(SequentialThreadPerTaskExecutor executor) { + this.executor = executor; + this.disposables = Disposables.composite(); + } + + @Override + public void dispose() { + disposables.dispose(); + executor.release(); + } + + @Override + public boolean isDisposed() { + return disposables.isDisposed(); + } + + @Override + public Disposable schedule(Runnable task) { + return executor.schedule(task, disposables); + } + + @Override + public Disposable schedule(Runnable task, long delay, TimeUnit unit) { + return executor.schedule(task, delay, unit, disposables); + } + + @Override + public Disposable schedulePeriodically(Runnable task, + long initialDelay, + long period, + TimeUnit unit) { + return executor.schedulePeriodically(task, initialDelay, period, unit, disposables); + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.BUFFERED) return disposables.size(); + if (key == Attr.TERMINATED || key == Attr.CANCELLED) return isDisposed(); + if (key == Attr.NAME) return "SingleThreadExecutorWorker"; + + return executor.scanUnsafe(key); + } + } + +} diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/VirtualThreadFactory.java b/reactor-core/src/main/java21/reactor/core/scheduler/VirtualThreadFactory.java new file mode 100644 index 0000000000..5dbbf1371e --- /dev/null +++ b/reactor-core/src/main/java21/reactor/core/scheduler/VirtualThreadFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import java.util.concurrent.ThreadFactory; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import reactor.util.annotation.NonNull; +import reactor.util.annotation.Nullable; + +/** + * The {@link VirtualThread} Reactor {@link ThreadFactory} to be used with + * {@link ThreadPerTaskBoundedElasticScheduler}, delegates all allocations to real {@link + * java.lang.ThreadBuilders.VirtualThreadFactory} + * + * @author Oleh Dokuka + */ +class VirtualThreadFactory implements ThreadFactory, + Thread.UncaughtExceptionHandler { + + final ThreadFactory delegate; + @Nullable + final BiConsumer uncaughtExceptionHandler; + + VirtualThreadFactory(String name, + boolean inheritThreadLocals, + @Nullable BiConsumer uncaughtExceptionHandler) { + this.uncaughtExceptionHandler = uncaughtExceptionHandler; + this.delegate = Thread.ofVirtual() + .name(name, 1) + .uncaughtExceptionHandler(this) + .inheritInheritableThreadLocals(inheritThreadLocals) + .factory(); + } + + @Override + public final Thread newThread(@NonNull Runnable runnable) { + return delegate.newThread(runnable); + } + + @Override + public void uncaughtException(Thread t, Throwable e) { + if (uncaughtExceptionHandler == null) { + return; + } + + uncaughtExceptionHandler.accept(t,e); + } +} diff --git a/reactor-core/src/test/java/reactor/core/scheduler/BoundedElasticSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/BoundedElasticSchedulerTest.java index 55c715cdd1..74e1310349 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/BoundedElasticSchedulerTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/BoundedElasticSchedulerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ import com.pivovarit.function.ThrowingRunnable; import com.pivovarit.function.ThrowingSupplier; +import org.assertj.core.api.Assumptions; import org.assertj.core.data.Offset; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterAll; @@ -1407,6 +1408,7 @@ private ExecutorService startProducer(LinkedList> listeners) { @Test public void defaultBoundedElasticConfigurationIsConsistentWithJavadoc() { + Assumptions.assumeThat(Schedulers.DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS).isFalse(); Schedulers.CachedScheduler cachedBoundedElastic = (Schedulers.CachedScheduler) Schedulers.boundedElastic(); BoundedElasticScheduler boundedElastic = (BoundedElasticScheduler) cachedBoundedElastic.cached; diff --git a/reactor-core/src/test/java/reactor/core/scheduler/GenericThreadPerTaskBoundedElasticSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/GenericThreadPerTaskBoundedElasticSchedulerTest.java new file mode 100644 index 0000000000..8703829a59 --- /dev/null +++ b/reactor-core/src/test/java/reactor/core/scheduler/GenericThreadPerTaskBoundedElasticSchedulerTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import org.assertj.core.api.Assumptions; +import org.junit.jupiter.api.Assertions; + +class GenericThreadPerTaskBoundedElasticSchedulerTest extends AbstractSchedulerTest { + + + static boolean SUPPORTED; + + static { + try { + new VirtualThreadFactory("threadPerTaskBoundedElasticSchedulerTest", + false, + Schedulers::defaultUncaughtException); + SUPPORTED = true; + } + catch (Throwable t) { + SUPPORTED = false; + } + } + + @Override + protected ThreadPerTaskBoundedElasticScheduler scheduler() { + ThreadPerTaskBoundedElasticScheduler test = freshScheduler(); + test.init(); + return test; + } + + @Override + protected ThreadPerTaskBoundedElasticScheduler freshScheduler() { + Assumptions.assumeThat(SUPPORTED).isTrue(); + return new ThreadPerTaskBoundedElasticScheduler(4, + Integer.MAX_VALUE, + new VirtualThreadFactory( + "threadPerTaskBoundedElasticSchedulerTest", false, + Schedulers::defaultUncaughtException)); + } + + @Override + protected boolean shouldCheckInterrupted() { + return true; + } + + @Override + protected boolean shouldCheckMultipleDisposeGracefully() { + return true; + } + + @Override + public void acceptTaskAfterStartStopStart() { + Assertions.fail("no restart supported"); + } + + @Override + public void restartSupport() { + Assertions.fail("no restart supported"); + } + + @Override + void multipleRestarts() { + Assertions.fail("no restart supported"); + } +} \ No newline at end of file diff --git a/reactor-core/src/test/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticSchedulerTest.java b/reactor-core/src/test/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticSchedulerTest.java new file mode 100644 index 0000000000..953e627220 --- /dev/null +++ b/reactor-core/src/test/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticSchedulerTest.java @@ -0,0 +1,764 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import com.pivovarit.function.ThrowingRunnable; +import org.assertj.core.api.Assertions; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; +import reactor.test.util.RaceTestUtils; +import reactor.util.concurrent.Queues; + +class ThreadPerTaskBoundedElasticSchedulerTest { + + ThreadPerTaskBoundedElasticScheduler scheduler; + List disposables = new ArrayList<>(); + + @BeforeEach + void setup() { + scheduler = newScheduler(2, 3); + scheduler.init(); + } + + @AfterEach + void teardown() { + disposables.forEach(Disposable::dispose); + disposables.clear(); + } + + ThreadPerTaskBoundedElasticScheduler newScheduler(int maxThreads, int maxCapacity) { + ThreadPerTaskBoundedElasticScheduler scheduler = + new ThreadPerTaskBoundedElasticScheduler(maxThreads, + maxCapacity, + Thread.ofVirtual() + .name("virtualThreadPerTaskBoundedElasticScheduler", 1) + .uncaughtExceptionHandler(Schedulers::defaultUncaughtException) + .factory()); + + disposables.add(scheduler); + return scheduler; + } + + @Test + public void ensuresTasksScheduling() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + + Disposable disposable = scheduler.schedule(latch::countDown); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(disposable.isDisposed()).isTrue(); + } + + @Test + public void ensuresTasksDelayedScheduling() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch awaiter = new CountDownLatch(1); + + ThreadPerTaskBoundedElasticScheduler.BoundedServices resource = + scheduler.state.currentResource; + // submit task which occupy shared single threaded scheduler + resource.sharedDelayedTasksScheduler.submit(() -> { + awaiter.await(); + return null; + }); + + // ensures task is picked + Awaitility.await() + .atMost(Duration.ofSeconds(2)) + .until(() -> ((ScheduledThreadPoolExecutor) resource.sharedDelayedTasksScheduler).getQueue().isEmpty()); + + // schedule delayed task which should go to sharedDelayedTasksScheduler + Disposable disposable = scheduler.schedule(latch::countDown, 1, TimeUnit.MILLISECONDS); + + Assertions.assertThat(((ScheduledThreadPoolExecutor) resource.sharedDelayedTasksScheduler).getQueue().size()).isOne(); + + awaiter.countDown(); + + Awaitility.await() + .atMost(Duration.ofSeconds(2)) + .until(() -> ((ScheduledThreadPoolExecutor) resource.sharedDelayedTasksScheduler).getQueue().isEmpty()); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(disposable.isDisposed()).isTrue(); + } + + @Test + public void ensuresTasksDelayedZeroDelayScheduling() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch awaiter = new CountDownLatch(1); + + ThreadPerTaskBoundedElasticScheduler.BoundedServices resource = + scheduler.state.currentResource; + // submit task which occupy shared single threaded scheduler + resource.sharedDelayedTasksScheduler.submit(() -> { + awaiter.await(); + return null; + }); + + // ensures task is picked + Awaitility.await() + .atMost(Duration.ofSeconds(2)) + .until(() -> ((ScheduledThreadPoolExecutor) resource.sharedDelayedTasksScheduler).getQueue().isEmpty()); + + Disposable disposable = scheduler.schedule(latch::countDown, 0, TimeUnit.MILLISECONDS); + + // assures that no tasks is scheduled for shared scheduler + Assertions.assertThat(((ScheduledThreadPoolExecutor) resource.sharedDelayedTasksScheduler).getQueue().size()).isZero(); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(disposable.isDisposed()).isTrue(); + + // unblock scheduler + awaiter.countDown(); + } + + @Test + public void ensuresTasksPeriodicScheduling() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(10); + + Disposable disposable = scheduler.schedulePeriodically(latch::countDown, + 1, + 10, + TimeUnit.MILLISECONDS); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(disposable.isDisposed()).isFalse(); + disposable.dispose(); + Assertions.assertThat(disposable.isDisposed()).isTrue(); + } + + @Test + public void ensuresTasksPeriodicZeroInitialDelayScheduling() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(10); + + Disposable disposable = scheduler.schedulePeriodically(latch::countDown, + 0, + 10, + TimeUnit.MILLISECONDS); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(disposable.isDisposed()).isFalse(); + disposable.dispose(); + Assertions.assertThat(disposable.isDisposed()).isTrue(); + } + + @Test + public void ensuresTasksPeriodicWithInitialDelayAndInstantPeriodScheduling() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(10); + + Disposable disposable = scheduler.schedulePeriodically(latch::countDown, + 1, + 0, + TimeUnit.MILLISECONDS); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(disposable.isDisposed()).isFalse(); + disposable.dispose(); + Assertions.assertThat(disposable.isDisposed()).isTrue(); + } + + @Test + public void ensuresTasksPeriodicWithZeroInitialDelayAndInstantPeriodScheduling() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(10); + + Disposable disposable = scheduler.schedulePeriodically(latch::countDown, + 0, + 0, + TimeUnit.MILLISECONDS); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + Assertions.assertThat(disposable.isDisposed()).isFalse(); + disposable.dispose(); + Assertions.assertThat(disposable.isDisposed()).isTrue(); + } + + @Test + public void ensuresConcurrentTasksSchedulingWithinSingleWorker() throws InterruptedException { + Queue queue = Queues.unboundedMultiproducer() + .get(); + for (int i = 0; i < 100; i++) { + CountDownLatch latch = new CountDownLatch(2); + + Scheduler.Worker worker = scheduler.createWorker(); + + RaceTestUtils.race(() -> worker.schedule(() -> { + queue.offer("1"); + queue.offer("1"); + queue.offer("1"); + latch.countDown(); + }), () -> worker.schedule(() -> { + queue.offer("2"); + queue.offer("2"); + queue.offer("2"); + latch.countDown(); + })); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)) + .isTrue(); + + Object value1 = queue.poll(); + Assertions.assertThat(value1).isEqualTo(queue.poll()); + Assertions.assertThat(value1).isEqualTo(queue.poll()); + + Object value2 = queue.poll(); + Assertions.assertThat(value2).isEqualTo(queue.poll()); + Assertions.assertThat(value2).isEqualTo(queue.poll()); + worker.dispose(); + } + } + + @Test + public void ensuresConcurrentDelayedTasksSchedulingSingleWorker() throws InterruptedException { + Queue queue = Queues.unboundedMultiproducer() + .get(); + for (int i = 0; i < 100; i++) { + CountDownLatch latch = new CountDownLatch(3); + + Scheduler.Worker worker = scheduler.createWorker(); + + RaceTestUtils.race(() -> worker.schedule(() -> { + queue.offer("1"); + queue.offer("1"); + queue.offer("1"); + latch.countDown(); + }, 1, TimeUnit.MILLISECONDS), () -> worker.schedule(() -> { + queue.offer("2"); + queue.offer("2"); + queue.offer("2"); + latch.countDown(); + }), () -> worker.schedule(() -> { + queue.offer("3"); + queue.offer("3"); + queue.offer("3"); + latch.countDown(); + }, 1, TimeUnit.MILLISECONDS)); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)) + .isTrue(); + + Object value1 = queue.poll(); + Assertions.assertThat(value1).isEqualTo(queue.poll()); + Assertions.assertThat(value1).isEqualTo(queue.poll()); + + Object value2 = queue.poll(); + Assertions.assertThat(value2).isEqualTo(queue.poll()); + Assertions.assertThat(value2).isEqualTo(queue.poll()); + + Object value3 = queue.poll(); + Assertions.assertThat(value3).isEqualTo(queue.poll()); + Assertions.assertThat(value3).isEqualTo(queue.poll()); + worker.dispose(); + } + } + + @Test + public void ensuresConcurrentPeriodicTasksSchedulingSingleWorker() throws InterruptedException { + Queue queue = Queues.unboundedMultiproducer() + .get(); + for (int i = 0; i < 100; i++) { + CountDownLatch latch = new CountDownLatch(10); + + Scheduler.Worker worker = scheduler.createWorker(); + + RaceTestUtils.race(() -> worker.schedulePeriodically(() -> { + queue.offer("1"); + queue.offer("1"); + queue.offer("1"); + latch.countDown(); + }, 1, 0, TimeUnit.MILLISECONDS), () -> worker.schedule(() -> { + queue.offer("2"); + queue.offer("2"); + queue.offer("2"); + latch.countDown(); + }, 1, TimeUnit.MILLISECONDS), () -> worker.schedulePeriodically(() -> { + queue.offer("3"); + queue.offer("3"); + queue.offer("3"); + latch.countDown(); + }, 1, 1, TimeUnit.MILLISECONDS)); + + Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)) + .isTrue(); + + for (int j = 0; j < 10; j++) { + Object value = queue.poll(); + Assertions.assertThat(value) + .isEqualTo(queue.poll()); + Assertions.assertThat(value) + .isEqualTo(queue.poll()); + } + worker.dispose(); + } + } + + @Test + public void ensuresConcurrentWorkerTaskDisposure() throws InterruptedException { + for (int i = 0; i < 100; i++) { + CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch2 = new CountDownLatch(1); + + Scheduler.Worker worker = scheduler.createWorker(); + worker.schedule(()-> { + try { + latch2.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + Disposable disposable = worker.schedule(latch::countDown); + RaceTestUtils.race(() -> worker.dispose(), () -> disposable.dispose()); + latch2.countDown(); + Assertions.assertThat(latch.getCount()) + .isOne(); + Assertions.assertThat(worker.isDisposed()).isTrue(); + Assertions.assertThat(disposable.isDisposed()).isTrue(); + } + } + + @Test + public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenWorkerIsDisposed() throws InterruptedException { + ThreadPerTaskBoundedElasticScheduler + scheduler = newScheduler(2, 1000); + scheduler.init(); + Runnable task = () -> { + }; + + for (int i = 0; i < 100; i++) { + CountDownLatch latch = new CountDownLatch(1); + + Scheduler.Worker worker = scheduler.createWorker(); + List tasks = new ArrayList<>(); + tasks.add(worker.schedule(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(2000); + for (int j = 0; j < 1000; j++) { + tasks.add(worker.schedule(task)); + } + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(1000); + + latch.countDown(); + Thread.yield(); + worker.dispose(); + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(2000); + Assertions.assertThat(worker.isDisposed()) + .isTrue(); + Assertions.assertThat(tasks) + .allMatch(Disposable::isDisposed); + } + + } + + @Test + public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDisposed() throws InterruptedException { + ThreadPerTaskBoundedElasticScheduler + scheduler = newScheduler(2, 1000); + + scheduler.init(); + Runnable task = () -> { + }; + + for (int i = 0; i < 100; i++) { + CountDownLatch latch = new CountDownLatch(1); + + ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + List tasks = new ArrayList<>(); + tasks.add(worker.schedule(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(2000); + for (int j = 0; j < 1000; j++) { + tasks.add(worker.schedule(task)); + } + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(1000); + + latch.countDown(); + Thread.yield(); + tasks.forEach(Disposable::dispose); + + Awaitility.await() + .atMost(Duration.ofSeconds(5)) + .until(() -> !ThreadPerTaskBoundedElasticScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(2000); + Assertions.assertThat(worker.isDisposed()) + .isFalse(); + Assertions.assertThat(tasks) + .allMatch(Disposable::isDisposed); + } + } + + @Test + public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDisposedDelayedCase() throws InterruptedException { + ThreadPerTaskBoundedElasticScheduler + scheduler = newScheduler(2, 1000); + + scheduler.init(); + Runnable task = () -> { + }; + + for (int i = 0; i < 100; i++) { + CountDownLatch latch = new CountDownLatch(1); + + ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + List tasks = new ArrayList<>(); + tasks.add(worker.schedule(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(2000); + for (int j = 0; j < 1000; j++) { + tasks.add(worker.schedulePeriodically(task, 1,1, TimeUnit.MILLISECONDS)); + } + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(1000); + + latch.countDown(); + Thread.yield(); + tasks.forEach(Disposable::dispose); + + Awaitility.await() + .atMost(Duration.ofSeconds(5)) + .until(() -> !ThreadPerTaskBoundedElasticScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(2000); + Assertions.assertThat(worker.isDisposed()) + .isFalse(); + Assertions.assertThat(tasks) + .allMatch(Disposable::isDisposed); + } + } + + @Test + public void ensuresRandomTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDisposed() throws InterruptedException { + ThreadPerTaskBoundedElasticScheduler + scheduler = newScheduler(2, 10000); + scheduler.init(); + Runnable task = () -> { + }; + + for (int i = 0; i < 100; i++) { + CountDownLatch latch = new CountDownLatch(1); + + ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + List tasks = new ArrayList<>(); + tasks.add(worker.schedule(() -> { + try { + latch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(20000); + for (int j = 0; j < 10000; j++) { + switch (ThreadLocalRandom.current() + .nextInt(5)) { + case 0: + tasks.add(worker.schedule(task)); + break; + case 1: + tasks.add(worker.schedule(task, 1, TimeUnit.NANOSECONDS)); + break; + case 2: + tasks.add(worker.schedulePeriodically(task, 1, 1, TimeUnit.NANOSECONDS)); + break; + case 3: + tasks.add(worker.schedulePeriodically(task, 0, 1, TimeUnit.NANOSECONDS)); + break; + case 4: + tasks.add(worker.schedulePeriodically(task, 0, 0, TimeUnit.NANOSECONDS)); + break; + } + } + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(10000); + + latch.countDown(); + Thread.yield(); + tasks.forEach(Disposable::dispose); + + Awaitility.await() + .atMost(Duration.ofSeconds(5)) + .until(() -> !ThreadPerTaskBoundedElasticScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) + .isEqualTo(20000); + Assertions.assertThat(worker.isDisposed()) + .isFalse(); + Assertions.assertThat(tasks) + .allMatch(Disposable::isDisposed); + } + } + + @Test + public void ensuresTasksAreOrderedWithinAWorker() throws InterruptedException { + ThreadPerTaskBoundedElasticScheduler scheduler = newScheduler(1, 1000); + scheduler.init(); + + ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = + (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + + ConcurrentLinkedQueue tasksIds = new ConcurrentLinkedQueue<>(); + + for (int i = 0; i < 1000; i++) { + int taskId = i; + worker.schedule(() -> tasksIds.offer(taskId)); + } + + + worker.executor.shutdown(false); + Assertions.assertThat(worker.executor.await(10, TimeUnit.SECONDS)).isTrue(); + + Assertions.assertThat(tasksIds).containsExactlyElementsOf(Flux.range(0, 1000).collectList().block()); + } + + @Test + public void ensuresDelayedTasksAreOrderedWithinAWorker() throws InterruptedException { + ThreadPerTaskBoundedElasticScheduler scheduler = newScheduler(1, 1000); + scheduler.init(); + + ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = + (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + + ConcurrentLinkedQueue tasksIds = new ConcurrentLinkedQueue<>(); + + for (int i = 0; i < 1000; i++) { + int taskId = i; + worker.schedule(() -> tasksIds.offer(taskId), 1, TimeUnit.MILLISECONDS); + } + + worker.executor.shutdown(false); + Assertions.assertThat(worker.executor.await(10, TimeUnit.SECONDS)).isTrue(); + + Assertions.assertThat(tasksIds).containsExactlyElementsOf(Flux.range(0, 1000).collectList().block()); + } + + @Test + public void ensuresWorkersAreNotIntersecting() throws InterruptedException { + ThreadPerTaskBoundedElasticScheduler scheduler = newScheduler(1, 1000); + scheduler.init(); + + ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = + (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + + AtomicInteger counter = new AtomicInteger(); + + for (int i = 0; i < 500; i++) { + switch (i % 4) { + case 0 : worker.schedule(counter::incrementAndGet); break; + case 1 : worker.schedule(counter::incrementAndGet, 1, TimeUnit.MILLISECONDS); break; + case 2 : worker.schedulePeriodically(new Runnable() { + boolean once = false; + @Override + public void run() { + if (!once) { + once = true; + counter.incrementAndGet(); + } + } + }, 0, 0, TimeUnit.MILLISECONDS); break; + case 3 : worker.schedulePeriodically(new Runnable() { + boolean once = false; + @Override + public void run() { + if (!once) { + once = true; + counter.incrementAndGet(); + } + } + }, 1, 0, TimeUnit.MILLISECONDS); break; + } + } + + for (int i = 0; i < 500; i++) { + Scheduler.Worker localWorker = scheduler.createWorker(); + switch (ThreadLocalRandom.current().nextInt(0, 3)) { + case 0 : localWorker.schedule(() -> {}); break; + case 1 : localWorker.schedule(() -> {}, 1, TimeUnit.MILLISECONDS); break; + case 2 : localWorker.schedulePeriodically(() -> {}, 1, 1, TimeUnit.MILLISECONDS); break; + } + localWorker.dispose(); + } + + worker.executor.shutdown(false); + Assertions.assertThat(worker.executor.await(10, TimeUnit.SECONDS)).isTrue(); + + Assertions.assertThat(counter).hasValue(500); + } + + @Test + public void ensuresSupportGracefulShutdown() { + ThreadPerTaskBoundedElasticScheduler scheduler = newScheduler(100, 100_000); + scheduler.init(); + + AtomicInteger counter = new AtomicInteger(); + + for (int i = 0; i < 100_000; i++) { + switch (i % 4) { + case 0 : scheduler.schedule(counter::incrementAndGet); break; + case 1 : scheduler.schedule(counter::incrementAndGet, 1, TimeUnit.MILLISECONDS); break; + case 2 : scheduler.schedulePeriodically(new Runnable() { + boolean once = false; + @Override + public void run() { + if (!once) { + once = true; + counter.incrementAndGet(); + } + } + }, 1, 0, TimeUnit.MILLISECONDS); break; + case 3 : scheduler.schedulePeriodically(new Runnable() { + boolean once = false; + @Override + public void run() { + if (!once) { + once = true; + counter.incrementAndGet(); + } + } + }, 0, 0, TimeUnit.MILLISECONDS); break; + // we can not test that real scheduledAtFixedRate task is awaited since + // it is not awaited by ScheduledExecutorService, thus no way to + // observe it + /*case 4 : scheduler.schedulePeriodically(new Runnable() { + boolean once = false; + @Override + public void run() { + if (!once) { + once = true; + counter.incrementAndGet(); + } + } + }, 1, 1, TimeUnit.MILLISECONDS); break;*/ + } + } + + StepVerifier.create(scheduler.disposeGracefully()) + .expectSubscription() + .expectComplete() + .verify(Duration.ofSeconds(100)); + + Assertions.assertThat(scheduler.isDisposed()).isTrue(); + Assertions.assertThat(counter).hasValue(100_000); + } + + @Test + void ensuresTotalTasksMathIsDoneCorrectlyInOverflow() { + ThreadPerTaskBoundedElasticScheduler scheduler = + newScheduler(10, + Integer.MAX_VALUE - 1); + scheduler.init(); + CountDownLatch latch = new CountDownLatch(1); + + Runnable task = () -> { + try { + latch.await(); + } + catch (InterruptedException e) { + + } + }; + + for (int i = 0; i < 10; i++) { + Scheduler.Worker worker = scheduler.createWorker(); + for (int j = 0; j < 100; j++) { + worker.schedule(task); + } + } + + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()).isEqualTo(Integer.MAX_VALUE); + } + + @Test + void ensuresTotalTasksMathIsDoneCorrectlyInEdgeCase() { + ThreadPerTaskBoundedElasticScheduler scheduler = + newScheduler(10, + Integer.MAX_VALUE / 10 + 1); + scheduler.init(); + CountDownLatch latch = new CountDownLatch(1); + + Runnable task = () -> { + try { + latch.await(); + } + catch (InterruptedException e) { + + } + }; + + for (int i = 0; i < 10; i++) { + Scheduler.Worker worker = scheduler.createWorker(); + for (int j = 0; j < 100; j++) { + worker.schedule(task); + } + } + + // Note +10 means that 10 tasks are in fly blocked, and they are not included + // in the capacity counting since they don't occupy a queue + Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()).isEqualTo(10L * (Integer.MAX_VALUE / 10 + 1) - 1000 + 10); + } +} \ No newline at end of file diff --git a/reactor-test/src/main/java/reactor/test/scheduler/VirtualTimeScheduler.java b/reactor-test/src/main/java/reactor/test/scheduler/VirtualTimeScheduler.java index 415ce8c383..38d0c42f69 100644 --- a/reactor-test/src/main/java/reactor/test/scheduler/VirtualTimeScheduler.java +++ b/reactor-test/src/main/java/reactor/test/scheduler/VirtualTimeScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -473,6 +473,11 @@ public Scheduler newBoundedElastic(int threadCap, int taskCap, ThreadFactory thr return s; } + @Override + public Scheduler newThreadPerTaskBoundedElastic(int threadCap, int queuedTaskCap, ThreadFactory threadFactory) { + return s; + } + @Override public Scheduler newParallel(int parallelism, ThreadFactory threadFactory) { return s; From 9b6a53e018511b4ab6adb877694284e7ddcfe337 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:02:47 +0300 Subject: [PATCH 183/312] improves build speed (#3597) * improves build speed Signed-off-by: Oleh Dokuka * fixes Signed-off-by: Oleh Dokuka --------- Signed-off-by: Oleh Dokuka --- .github/workflows/ci.yml | 2 +- .github/workflows/nightly.yml | 2 +- reactor-core/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 985984598d..12c17d6d48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,4 +185,4 @@ jobs: - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 name: other tests with: - arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon + arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -Pjcstress.mode=sanity diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 23a86b6725..843adf8f11 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -145,4 +145,4 @@ jobs: - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 name: other tests with: - arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true \ No newline at end of file + arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index f4562cd122..ed1281b57c 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -335,7 +335,7 @@ blockHoundTest { } jcstress { - mode = 'quick' //quick, default, tough + mode = project.hasProperty('jcstress.mode') ? project.getProperty('jcstress.mode') : 'quick'//quick, default, tough jcstressDependency 'org.openjdk.jcstress:jcstress-core:0.16' heapPerFork = 512 } From 12926b99db1c62d850d78e968d0057aeacb322e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 3 Oct 2023 15:18:59 +0200 Subject: [PATCH 184/312] Hardening automatic context propagation (#3549) This change addresses Flux, Mono, and Publisher implementations that are implemented outside of reactor-core for the purposes of automatic restoration of ThreadLocal state from the Context. Arbitrary Flux, Mono, or raw Publisher implementations can deliver signals from any Thread they desire, with no need to use Scheduler-controlled Threads. The implementations in reactor-core, whenever they switch Threads or when they receive signals out-of-band (e.g. request(n)) restore the ThreadLocal state. Recently, factories for Flux and Mono creation from external sources, such as a raw Publisher, or Future implementations, have added guards via a dedicated ThreadLocal-restoring Subscriber to deal with signals from the sources that are not performing the restoration. However, custom Flux, Mono, and Publisher sources not wrapped via from() method can deliver signals without the ThreadLocal state restored. This is the case with reactor-netty's Flux implementation delivering signals from Netty's event loop. Also, while wrapping the sources obtained via Flux.from and Mono.from factories works fine, plenty of operators, such as flatMap, accept a raw Publisher and don't provide any guard against changing Thread, which means these operators need to be addressed. This change applies wrapping of either the Producer or Subscriber. It is driven by a Scannable flag on the Publisher. We do it in these scenarios: 1. Wrap the Publisher when we assemble an operator at runtime of another operator (flatMap for example). 2. Wrap the Subscriber at operators that allow the user to directly emit signals from a different Thread (Sinks for example). 3. Wrap the Subscriber when subscribe(Subscriber) from RS Publisher is called and the source is not protected. The following changes have been introduced: * Added an internal Scannable attribute: INTERNAL_PRODUCER, which allows determining if a Flux or Mono is a reactor-core implementation. * Reviewed Scannable implementations to report INTERNAL_PRODUCER properly for internal implementations. * Introduced new methods in Operators class, which allow wrapping the Subscriber with a context restoration guard whenever the Publisher is not an INTERNAL_PRODUCER and allow wrapping the Publisher directly by applying an operator that restores ThreadLocals when signals travel downstream. * In Flux, Mono, InternalFluxPublisher, InternalMonoPublisher, FluxFromMonoOperator, MonoFromFluxOperator, MonoFromPublisher, the subscribe(Subscriber) method from Publisher uses Subscriber wrapping as a last-resort chance to restore ThreadLocals. In case subscribe(CoreSubscriber) is used, we have no way to intercept it, as it is abstract, so we need other means of wrapping too. * Flux.from, Mono.from, Mono.fromDirect and their respective wrap() methods also apply the wrapping. * In operators which receive a Publisher from the user during runtime (for instance flatMap receives it as a return value of a lambda provided by the user), either Operators.toFluxOrMono is used or direct type wrapping when it is known what is expected. * Added plenty of tests that depict the issue for the identified cases that are able to accept a Publisher that can switch a Thread when delivering signals. Resolves #3478. --- reactor-core/build.gradle | 31 +- .../src/main/java/reactor/core/Scannable.java | 5 +- .../core/publisher/ConnectableFluxHide.java | 4 +- .../publisher/ConnectableFluxOnAssembly.java | 4 +- .../core/publisher/ConnectableLift.java | 5 +- .../publisher/ConnectableLiftFuseable.java | 5 +- .../core/publisher/ContextPropagation.java | 10 + .../publisher/ContextPropagationSupport.java | 12 + .../java/reactor/core/publisher/Flux.java | 34 +- .../reactor/core/publisher/FluxArray.java | 5 +- .../core/publisher/FluxBufferWhen.java | 4 +- .../reactor/core/publisher/FluxCallable.java | 4 +- .../core/publisher/FluxCombineLatest.java | 6 +- .../core/publisher/FluxConcatArray.java | 10 +- .../core/publisher/FluxConcatIterable.java | 6 +- ...FluxContextWriteRestoringThreadLocals.java | 46 +- .../reactor/core/publisher/FluxCreate.java | 10 +- .../reactor/core/publisher/FluxDefer.java | 4 +- .../core/publisher/FluxDeferContextual.java | 6 +- .../reactor/core/publisher/FluxEmpty.java | 4 +- .../reactor/core/publisher/FluxError.java | 4 +- .../core/publisher/FluxErrorOnRequest.java | 4 +- .../core/publisher/FluxErrorSupplied.java | 4 +- .../core/publisher/FluxFirstWithSignal.java | 8 +- .../core/publisher/FluxFirstWithValue.java | 8 +- .../reactor/core/publisher/FluxFlatMap.java | 3 + .../core/publisher/FluxFromMonoOperator.java | 4 +- .../reactor/core/publisher/FluxGenerate.java | 11 +- .../reactor/core/publisher/FluxInterval.java | 5 +- .../reactor/core/publisher/FluxIterable.java | 4 +- .../java/reactor/core/publisher/FluxJust.java | 4 +- .../reactor/core/publisher/FluxMerge.java | 7 +- .../core/publisher/FluxMergeComparing.java | 11 +- .../reactor/core/publisher/FluxNever.java | 4 +- .../reactor/core/publisher/FluxOperator.java | 6 +- .../reactor/core/publisher/FluxPublish.java | 1 + .../reactor/core/publisher/FluxRange.java | 4 +- .../core/publisher/FluxRepeatWhen.java | 3 +- .../reactor/core/publisher/FluxReplay.java | 3 +- .../reactor/core/publisher/FluxRetryWhen.java | 12 +- .../reactor/core/publisher/FluxSource.java | 99 +- .../core/publisher/FluxSourceFuseable.java | 4 +- .../reactor/core/publisher/FluxStream.java | 4 +- .../publisher/FluxSubscribeOnCallable.java | 3 +- .../core/publisher/FluxSubscribeOnValue.java | 3 +- .../core/publisher/FluxSwitchOnFirst.java | 12 +- .../FluxTapRestoringThreadLocals.java | 4 +- .../reactor/core/publisher/FluxUsing.java | 4 +- .../reactor/core/publisher/FluxUsingWhen.java | 15 +- .../core/publisher/FluxWindowPredicate.java | 4 +- .../core/publisher/FluxWindowTimeout.java | 4 +- .../java/reactor/core/publisher/FluxZip.java | 6 +- .../reactor/core/publisher/GroupedLift.java | 5 +- .../core/publisher/GroupedLiftFuseable.java | 5 +- .../reactor/core/publisher/InnerProducer.java | 3 +- .../InternalConnectableFluxOperator.java | 3 +- .../core/publisher/InternalFluxOperator.java | 13 +- .../core/publisher/InternalMonoOperator.java | 16 +- .../core/publisher/InternalProducerAttr.java | 28 + .../java/reactor/core/publisher/Mono.java | 84 +- .../reactor/core/publisher/MonoCallable.java | 4 +- .../core/publisher/MonoCompletionStage.java | 116 +- ...MonoContextWriteRestoringThreadLocals.java | 1 + .../reactor/core/publisher/MonoCreate.java | 14 +- .../core/publisher/MonoCurrentContext.java | 3 +- .../reactor/core/publisher/MonoDefer.java | 4 +- .../core/publisher/MonoDeferContextual.java | 6 +- .../reactor/core/publisher/MonoDelay.java | 5 +- .../core/publisher/MonoDelayUntil.java | 7 +- .../reactor/core/publisher/MonoEmpty.java | 4 +- .../reactor/core/publisher/MonoError.java | 4 +- .../core/publisher/MonoErrorSupplied.java | 4 +- .../core/publisher/MonoFirstWithSignal.java | 8 +- .../core/publisher/MonoFirstWithValue.java | 6 +- .../core/publisher/MonoFromFluxOperator.java | 4 +- .../core/publisher/MonoFromPublisher.java | 8 +- .../core/publisher/MonoIgnorePublisher.java | 7 +- .../core/publisher/MonoIgnoreThen.java | 7 +- .../java/reactor/core/publisher/MonoJust.java | 4 +- .../reactor/core/publisher/MonoNever.java | 4 +- .../reactor/core/publisher/MonoOperator.java | 7 +- .../reactor/core/publisher/MonoRetryWhen.java | 4 +- .../reactor/core/publisher/MonoRunnable.java | 4 +- .../core/publisher/MonoSequenceEqual.java | 8 +- .../core/publisher/MonoSingleCallable.java | 4 +- .../publisher/MonoSingleOptionalCallable.java | 2 +- .../reactor/core/publisher/MonoSource.java | 94 +- .../publisher/MonoSubscribeOnCallable.java | 3 +- .../core/publisher/MonoSubscribeOnValue.java | 3 +- .../reactor/core/publisher/MonoSupplier.java | 4 +- .../MonoTapRestoringThreadLocals.java | 1 + .../reactor/core/publisher/MonoUsing.java | 8 +- .../reactor/core/publisher/MonoUsingWhen.java | 11 +- .../java/reactor/core/publisher/MonoWhen.java | 6 +- .../java/reactor/core/publisher/MonoZip.java | 8 +- .../reactor/core/publisher/Operators.java | 89 +- .../core/publisher/ParallelArraySource.java | 3 +- .../core/publisher/ParallelCollect.java | 5 +- .../core/publisher/ParallelConcatMap.java | 5 +- .../core/publisher/ParallelDoOnEach.java | 5 +- .../core/publisher/ParallelFilter.java | 5 +- .../core/publisher/ParallelFlatMap.java | 5 +- .../reactor/core/publisher/ParallelFlux.java | 9 +- .../core/publisher/ParallelFluxHide.java | 5 +- .../core/publisher/ParallelFluxName.java | 6 +- .../publisher/ParallelFluxOnAssembly.java | 5 +- .../ParallelFluxRestoringThreadLocals.java | 52 + .../reactor/core/publisher/ParallelGroup.java | 5 +- .../reactor/core/publisher/ParallelLift.java | 11 +- .../core/publisher/ParallelLiftFuseable.java | 11 +- .../reactor/core/publisher/ParallelLog.java | 5 +- .../reactor/core/publisher/ParallelMap.java | 5 +- .../core/publisher/ParallelMergeOrdered.java | 5 +- .../core/publisher/ParallelMergeReduce.java | 5 +- .../publisher/ParallelMergeSequential.java | 5 +- .../core/publisher/ParallelMergeSort.java | 5 +- .../reactor/core/publisher/ParallelPeek.java | 5 +- .../core/publisher/ParallelReduceSeed.java | 5 +- .../reactor/core/publisher/ParallelRunOn.java | 5 +- .../core/publisher/ParallelSource.java | 5 +- .../reactor/core/publisher/ParallelThen.java | 5 +- .../core/publisher/SinkEmptyMulticast.java | 11 +- .../core/publisher/SinkManyBestEffort.java | 14 +- .../publisher/SinkManyEmitterProcessor.java | 11 +- .../publisher/SinkManyReplayProcessor.java | 9 +- .../core/publisher/SinkManyUnicast.java | 11 +- .../SinkManyUnicastNoBackpressure.java | 13 +- .../core/publisher/SinkOneMulticast.java | 12 +- .../core/publisher/SourceProducer.java | 7 +- .../AutomaticContextPropagationTest.java | 1451 ++++++++++++++++- ...ContextWriteRestoringThreadLocalsTest.java | 7 +- .../core/publisher/ThreadSwitchingFlux.java | 68 + .../core/publisher/ThreadSwitchingMono.java | 68 + .../ThreadSwitchingParallelFlux.java | 78 + 134 files changed, 2316 insertions(+), 751 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/publisher/InternalProducerAttr.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/ParallelFluxRestoringThreadLocals.java create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingFlux.java create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingMono.java create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingParallelFlux.java diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index ed1281b57c..ab859689e0 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -202,19 +202,23 @@ def japicmpReport = tasks.register('japicmpReport') { } doLast { def reportFile = file("${project.buildDir}/reports/japi.txt") - if (reportFile.exists()) { - println "\n **********************************" - println " * /!\\ API compatibility failures *" - println " **********************************" - println "Japicmp report was filtered and interpreted to find the following incompatibilities:" - reportFile.eachLine { - if (it.contains("*") && (!it.contains("***") || it.contains("****"))) - println "source incompatible change: $it" - else if (it.contains("!")) - println "binary incompatible change: $it" - } + if (reportFile.exists()) { + println "\n **********************************" + println " * /!\\ API compatibility failures *" + println " **********************************" + println "Japicmp report was filtered and interpreted to find the following incompatibilities:" + reportFile.eachLine { + if (it.contains("*") && (!it.contains("***") || it.contains("****"))) { + println "source incompatible change: $it" + } + else if (it.contains("!")) { + println "binary incompatible change: $it" + } } - else println "No incompatible change to report" + } + else { + println "No incompatible change to report" + } } } @@ -252,6 +256,9 @@ task japicmp(type: JapicmpTask) { classExcludes = [ ] methodExcludes = [ + "reactor.core.publisher.Operators#toFluxOrMono(org.reactivestreams.Publisher)", + "reactor.core.publisher.Operators#toFluxOrMono(org.reactivestreams.Publisher[])", + "reactor.core.publisher.ParallelFlux#from(reactor.core.publisher.ParallelFlux)" ] } diff --git a/reactor-core/src/main/java/reactor/core/Scannable.java b/reactor-core/src/main/java/reactor/core/Scannable.java index 8f5ee597f0..828c0aba03 100644 --- a/reactor-core/src/main/java/reactor/core/Scannable.java +++ b/reactor-core/src/main/java/reactor/core/Scannable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,12 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.Spliterators; import java.util.function.Function; import java.util.regex.Pattern; @@ -38,7 +36,6 @@ import reactor.core.scheduler.Scheduler.Worker; import reactor.util.annotation.Nullable; import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; /** * A Scannable component exposes state in a non strictly memory consistent way and diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxHide.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxHide.java index e8c49a6565..bcf2779198 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxHide.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxHide.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return super.scanUnsafe(key); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxOnAssembly.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxOnAssembly.java index 993e2fc7f6..35234024b6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxOnAssembly.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxOnAssembly.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.ACTUAL_METADATA) return !stacktrace.isCheckpoint; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return super.scanUnsafe(key); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java index e4686ff659..35d7e86bc7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,8 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Scannable.from(source).scanUnsafe(key); if (key == Attr.LIFTER) return liftFunction.name; - return null; + + return super.scanUnsafe(key); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java index 9dd538af45..b73831c3b9 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,8 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Scannable.from(source).scanUnsafe(key); if (key == Attr.LIFTER) return liftFunction.name; - return null; + + return super.scanUnsafe(key); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index f331b6a5c2..ec630b3ed3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -60,6 +60,14 @@ final class ContextPropagation { } } + static Flux fluxRestoreThreadLocals(Flux flux) { + return new FluxContextWriteRestoringThreadLocals<>(flux, Function.identity()); + } + + static Mono monoRestoreThreadLocals(Mono mono) { + return new MonoContextWriteRestoringThreadLocals<>(mono, Function.identity()); + } + static void configureContextSnapshotFactory(boolean clearMissing) { if (ContextPropagationSupport.isContextPropagation103OnClasspath) { globalContextSnapshotFactory = ContextSnapshotFactory.builder() @@ -117,6 +125,8 @@ public static Function scopePassingOnScheduleHook() { }; } + + /** * Create a support function that takes a snapshot of thread locals and merges them with the * provided {@link Context}, resulting in a new {@link Context} which includes entries diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java index c705899a40..ec9004f77d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java @@ -16,6 +16,13 @@ package reactor.core.publisher; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import reactor.core.CorePublisher; +import reactor.core.Fuseable; +import reactor.core.Scannable; import reactor.util.Logger; import reactor.util.Loggers; @@ -65,6 +72,11 @@ static boolean shouldPropagateContextToThreadLocals() { return isContextPropagationOnClasspath && propagateContextToThreadLocals; } + static boolean shouldWrapPublisher(Publisher publisher) { + return shouldPropagateContextToThreadLocals() && + !Scannable.from(publisher).scanOrDefault(InternalProducerAttr.INSTANCE, false); + } + static boolean shouldRestoreThreadLocalsInSomeOperators() { return isContextPropagationOnClasspath && !propagateContextToThreadLocals; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index 89afdf55b3..b9a3213cb2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -16,6 +16,8 @@ package reactor.core.publisher; +import java.io.File; +import java.lang.reflect.ParameterizedType; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -44,6 +46,7 @@ import java.util.function.Supplier; import java.util.logging.Level; import java.util.stream.Collector; +import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -1060,7 +1063,7 @@ public static Flux firstWithValue(Publisher first, Publisher */ public static Flux from(Publisher source) { //duplicated in wrap, but necessary to detect early and thus avoid applying assembly - if (source instanceof Flux) { + if (source instanceof Flux && !ContextPropagationSupport.shouldWrapPublisher(source)) { @SuppressWarnings("unchecked") Flux casted = (Flux) source; return casted; @@ -8770,6 +8773,7 @@ public final void subscribe(Subscriber actual) { } } + subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(publisher, subscriber); publisher.subscribe(subscriber); } catch (Throwable e) { @@ -11065,8 +11069,12 @@ static BiFunction> tuple2Function() { */ @SuppressWarnings("unchecked") static Flux wrap(Publisher source) { - if (source instanceof Flux) { - return (Flux) source; + boolean shouldWrap = ContextPropagationSupport.shouldWrapPublisher(source); + if (source instanceof Flux) { + if (!shouldWrap) { + return (Flux) source; + } + return ContextPropagation.fluxRestoreThreadLocals((Flux) source); } //for scalars we'll instantiate the operators directly to avoid onAssembly @@ -11084,16 +11092,22 @@ static Flux wrap(Publisher source) { } } - if(source instanceof Mono){ - if(source instanceof Fuseable){ - return new FluxSourceMonoFuseable<>((Mono)source); + Flux target; + if (source instanceof Mono) { + if (source instanceof Fuseable) { + target = new FluxSourceMonoFuseable<>((Mono) source); + } else { + target = new FluxSourceMono<>((Mono) source); } - return new FluxSourceMono<>((Mono)source); + } else if (source instanceof Fuseable) { + target = new FluxSourceFuseable<>(source); + } else { + target = new FluxSource<>(source); } - if(source instanceof Fuseable){ - return new FluxSourceFuseable<>(source); + if (shouldWrap) { + return ContextPropagation.fluxRestoreThreadLocals(target); } - return new FluxSource<>(source); + return target; } @SuppressWarnings("rawtypes") diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxArray.java b/reactor-core/src/main/java/reactor/core/publisher/FluxArray.java index 72870d6770..a288edc2bf 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxArray.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxArray.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,8 +64,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.BUFFERED) return array.length; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - - return null; + return SourceProducer.super.scanUnsafe(key); } static final class ArraySubscription diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java index 80347fd5e8..8f92516842 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -431,7 +431,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.ERROR) return errors; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return InnerOperator.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxCallable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxCallable.java index d479901d20..f8bad62c58 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxCallable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxCallable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,6 @@ public T call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxCombineLatest.java b/reactor-core/src/main/java/reactor/core/publisher/FluxCombineLatest.java index 5725871e83..2e022b7dd3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxCombineLatest.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxCombineLatest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -166,6 +166,8 @@ else if (!(actual instanceof QueueSubscription)) { } } + Operators.toFluxOrMono(a); + Queue queue = queueSupplier.get(); CombineLatestCoordinator coordinator = @@ -180,7 +182,7 @@ else if (!(actual instanceof QueueSubscription)) { public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return prefetch; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class CombineLatestCoordinator diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatArray.java b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatArray.java index f735711b59..4a775aa3e0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatArray.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatArray.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ public void subscribe(CoreSubscriber actual) { if (p == null) { Operators.error(actual, new NullPointerException("The single source Publisher is null")); } else { - p.subscribe(actual); + Operators.toFluxOrMono(p).subscribe(actual); } return; } @@ -83,7 +83,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.DELAY_ERROR) return delayError; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } /** @@ -255,7 +255,7 @@ public void onComplete() { if (this.cancelled) { return; } - p.subscribe(this); + Operators.toFluxOrMono(p).subscribe(this); final Object state = this.get(); if (state != DONE) { @@ -440,7 +440,7 @@ public void onComplete() { return; } - p.subscribe(this); + Operators.toFluxOrMono(p).subscribe(this); final Object state = this.get(); if (state != DONE) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatIterable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatIterable.java index a4a1b00349..cd59babd64 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatIterable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatIterable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +64,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class ConcatIterableSubscriber @@ -144,7 +144,7 @@ public void onComplete() { produced(c); } - p.subscribe(this); + Operators.toFluxOrMono(p).subscribe(this); if (isCancelled()) { return; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java index 415c852596..c425db10c8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java @@ -22,6 +22,7 @@ import io.micrometer.context.ContextSnapshot; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; import reactor.core.Fuseable.ConditionalSubscriber; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -49,10 +50,11 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return super.scanUnsafe(key); } - static final class ContextWriteRestoringThreadLocalsSubscriber + static class ContextWriteRestoringThreadLocalsSubscriber implements ConditionalSubscriber, InnerOperator { final CoreSubscriber actual; @@ -171,4 +173,46 @@ public void cancel() { } } } + + static final class FuseableContextWriteRestoringThreadLocalsSubscriber + extends ContextWriteRestoringThreadLocalsSubscriber + implements Fuseable.QueueSubscription { + + FuseableContextWriteRestoringThreadLocalsSubscriber( + CoreSubscriber actual, Context context) { + super(actual, context); + } + + // Required for + // FuseableBestPracticesTest.coreFuseableSubscribersShouldNotExtendNonFuseableOnNext + @Override + public void onNext(T t) { + super.onNext(t); + } + + @Override + public T poll() { + throw new UnsupportedOperationException("Nope"); + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public int size() { + throw new UnsupportedOperationException("Nope"); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException("Nope"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Nope"); + } + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java b/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java index 27564cfc37..fcd01bc9d0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java @@ -88,9 +88,11 @@ static BaseSink createSink(CoreSubscriber t, @Override public void subscribe(CoreSubscriber actual) { - BaseSink sink = createSink(actual, backpressure); + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + BaseSink sink = createSink(wrapped, backpressure); - actual.onSubscribe(sink); + wrapped.onSubscribe(sink); try { source.accept( createMode == CreateMode.PUSH_PULL ? new SerializedFluxSink<>(sink) : @@ -98,14 +100,14 @@ public void subscribe(CoreSubscriber actual) { } catch (Throwable ex) { Exceptions.throwIfFatal(ex); - sink.error(Operators.onOperatorError(ex, actual.currentContext())); + sink.error(Operators.onOperatorError(ex, wrapped.currentContext())); } } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } /** diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxDefer.java b/reactor-core/src/main/java/reactor/core/publisher/FluxDefer.java index 17a56d90e8..c8f116ccd9 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxDefer.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxDefer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +58,6 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxDeferContextual.java b/reactor-core/src/main/java/reactor/core/publisher/FluxDeferContextual.java index 128cde8bbf..c06186a2b4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxDeferContextual.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxDeferContextual.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,13 +54,13 @@ public void subscribe(CoreSubscriber actual) { return; } - from(p).subscribe(actual); + Operators.toFluxOrMono(p).subscribe(actual); } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxEmpty.java b/reactor-core/src/main/java/reactor/core/publisher/FluxEmpty.java index 70bf235770..c4da511602 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxEmpty.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxEmpty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } /** diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxError.java b/reactor-core/src/main/java/reactor/core/publisher/FluxError.java index 930a3f62fb..bf853e2848 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxError.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxError.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,6 @@ public Object call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxErrorOnRequest.java b/reactor-core/src/main/java/reactor/core/publisher/FluxErrorOnRequest.java index c68f8bff1d..b819832d9d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxErrorOnRequest.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxErrorOnRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class ErrorSubscription implements InnerProducer { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxErrorSupplied.java b/reactor-core/src/main/java/reactor/core/publisher/FluxErrorSupplied.java index c1f7cca997..af49df8a8d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxErrorSupplied.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxErrorSupplied.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,6 +57,6 @@ public Object call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithSignal.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithSignal.java index 5db4d8ca84..336a8b6182 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithSignal.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithSignal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,11 +127,13 @@ public void subscribe(CoreSubscriber actual) { new NullPointerException("The single source Publisher is null")); } else { - p.subscribe(actual); + Operators.toFluxOrMono(p).subscribe(actual); } return; } + Operators.toFluxOrMono(a); + RaceCoordinator coordinator = new RaceCoordinator<>(n); coordinator.subscribe(a, n, actual); @@ -164,7 +166,7 @@ FluxFirstWithSignal orAdditionalSource(Publisher source) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class RaceCoordinator diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithValue.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithValue.java index bb71c0817c..bd20447ef6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithValue.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -159,7 +159,7 @@ public void subscribe(CoreSubscriber actual) { return; } if (n == 1) { - Publisher p = a[0]; + Publisher p = Flux.from(a[0]); if (p == null) { Operators.error(actual, @@ -178,7 +178,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class RaceValuesCoordinator @@ -237,7 +237,7 @@ void subscribe(Publisher[] sources, return; } - sources[i].subscribe(subscribers[i]); + Operators.toFluxOrMono(sources[i]).subscribe(subscribers[i]); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFlatMap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFlatMap.java index e14d6eb1fc..9e384109a7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFlatMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFlatMap.java @@ -30,6 +30,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import reactor.core.CorePublisher; import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Fuseable; @@ -196,6 +197,7 @@ static boolean trySubscribeScalarMap(Publisher source, } } else { + p = Operators.toFluxOrMono(p); if (!fuseableExpected || p instanceof Fuseable) { p.subscribe(s); } @@ -424,6 +426,7 @@ else if (!delayError || !Exceptions.addThrowable(ERROR, this, e_)) { else { FlatMapInner inner = new FlatMapInner<>(this, prefetch); if (add(inner)) { + p = Operators.toFluxOrMono(p); p.subscribe(inner); } else { Operators.onDiscard(t, actual.currentContext()); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFromMonoOperator.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFromMonoOperator.java index bcecb5bbf6..b7c8b1b3f7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFromMonoOperator.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFromMonoOperator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +63,7 @@ protected FluxFromMonoOperator(Mono source) { public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.PARENT) return source; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } @@ -80,6 +81,7 @@ public final void subscribe(CoreSubscriber subscriber) { } OptimizableOperator newSource = operator.nextOptimizableSource(); if (newSource == null) { + subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(operator.source(), subscriber); operator.source().subscribe(subscriber); return; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxGenerate.java b/reactor-core/src/main/java/reactor/core/publisher/FluxGenerate.java index 3416db971b..fbc2a0f4bb 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxGenerate.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxGenerate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,21 +74,24 @@ final class FluxGenerate @Override public void subscribe(CoreSubscriber actual) { + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + S state; try { state = stateSupplier.call(); } catch (Throwable e) { - Operators.error(actual, Operators.onOperatorError(e, actual.currentContext())); + Operators.error(wrapped, Operators.onOperatorError(e, wrapped.currentContext())); return; } - actual.onSubscribe(new GenerateSubscription<>(actual, state, generator, stateConsumer)); + wrapped.onSubscribe(new GenerateSubscription<>(wrapped, state, generator, stateConsumer)); } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class GenerateSubscription diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxInterval.java b/reactor-core/src/main/java/reactor/core/publisher/FluxInterval.java index 22e40a2922..c3b2a5f416 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxInterval.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxInterval.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,8 +80,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.RUN_ON) return timedScheduler; if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; - - return null; + return SourceProducer.super.scanUnsafe(key); } static final class IntervalRunnable implements Runnable, Subscription, diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxIterable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxIterable.java index 30e8e704e0..12bb21a6e9 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxIterable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxIterable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,7 +92,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) { return Attr.RunStyle.SYNC; } - return null; + return SourceProducer.super.scanUnsafe(key); } /** diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxJust.java b/reactor-core/src/main/java/reactor/core/publisher/FluxJust.java index 18efb5e48b..53ba8f424e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxJust.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxJust.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ public void subscribe(final CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.BUFFERED) return 1; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxMerge.java b/reactor-core/src/main/java/reactor/core/publisher/FluxMerge.java index 3275a6b473..8c97c44cc3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxMerge.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxMerge.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,9 @@ final class FluxMerge extends Flux implements SourceProducer { throw new IllegalArgumentException("maxConcurrency > 0 required but it was " + maxConcurrency); } this.sources = Objects.requireNonNull(sources, "sources"); + + Operators.toFluxOrMono(this.sources); + this.delayError = delayError; this.maxConcurrency = maxConcurrency; this.prefetch = prefetch; @@ -106,7 +109,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return prefetch; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxMergeComparing.java b/reactor-core/src/main/java/reactor/core/publisher/FluxMergeComparing.java index a61480fa2a..d12f9d54d5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxMergeComparing.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxMergeComparing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,8 @@ final class FluxMergeComparing extends Flux implements SourceProducer { } } + Operators.toFluxOrMono(this.sources); + this.prefetch = prefetch; this.valueComparator = valueComparator; this.delayError = delayError; @@ -111,8 +113,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return prefetch; if (key == Attr.DELAY_ERROR) return delayError; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - - return null; + return SourceProducer.super.scanUnsafe(key); } @Override @@ -396,7 +397,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.REQUESTED_FROM_DOWNSTREAM) return requested - emitted; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return InnerProducer.super.scanUnsafe(key); } } @@ -492,7 +493,7 @@ public Object scanUnsafe(Attr key){ if (key == Attr.BUFFERED) return queue.size(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return InnerOperator.super.scanUnsafe(key); } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxNever.java b/reactor-core/src/main/java/reactor/core/publisher/FluxNever.java index b63f1ecf7b..b783bd4782 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxNever.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxNever.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } /** diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxOperator.java b/reactor-core/src/main/java/reactor/core/publisher/FluxOperator.java index 20a519598b..b0d416ce9f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxOperator.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxOperator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,8 @@ package reactor.core.publisher; import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; import org.reactivestreams.Publisher; -import reactor.core.CoreSubscriber; import reactor.core.Scannable; import reactor.util.annotation.Nullable; @@ -50,6 +47,7 @@ protected FluxOperator(Flux source) { public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.PARENT) return source; + if (key == InternalProducerAttr.INSTANCE) return false; // public class! return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java b/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java index ef0d7587bf..68814dc316 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java @@ -159,6 +159,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRange.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRange.java index 95e004de56..9c0391946a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRange.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRange.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class RangeSubscription implements InnerProducer, diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java index 37d892c4a1..30726f9ed7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -225,6 +225,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return main.otherArbiter; if (key == Attr.ACTUAL) return main; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxReplay.java b/reactor-core/src/main/java/reactor/core/publisher/FluxReplay.java index e6c1d2bcdc..d264e7f1cb 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxReplay.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxReplay.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1214,6 +1214,7 @@ public Object scanUnsafe(Scannable.Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.RUN_ON) return scheduler; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRetryWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRetryWhen.java index b01172497c..e953e8ad03 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRetryWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRetryWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,12 +53,14 @@ final class FluxRetryWhen extends InternalFluxOperator { static void subscribe(CoreSubscriber s, Retry whenSourceFactory, CorePublisher source) { + CorePublisher wrapped = Operators.toFluxOrMono(source); + RetryWhenOtherSubscriber other = new RetryWhenOtherSubscriber(); CoreSubscriber serial = Operators.serialize(s); RetryWhenMainSubscriber main = - new RetryWhenMainSubscriber<>(serial, other.completionSignal, source, whenSourceFactory.retryContext()); + new RetryWhenMainSubscriber<>(serial, other.completionSignal, wrapped, whenSourceFactory.retryContext()); other.main = main; serial.onSubscribe(main); @@ -71,10 +73,11 @@ static void subscribe(CoreSubscriber s, s.onError(Operators.onOperatorError(e, s.currentContext())); return; } - p.subscribe(other); + + Operators.toFluxOrMono(p).subscribe(other); if (!main.cancelled) { - source.subscribe(main); + wrapped.subscribe(main); } } @@ -255,6 +258,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return main.otherArbiter; if (key == Attr.ACTUAL) return main; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java index 92fdfc4cfc..6a0ee648f2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSource.java @@ -68,11 +68,7 @@ final class FluxSource extends Flux implements SourceProducer, @Override @SuppressWarnings("unchecked") public void subscribe(CoreSubscriber actual) { - if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { - source.subscribe(new FluxSourceRestoringThreadLocalsSubscriber<>(actual)); - } else { - source.subscribe(actual); - } + source.subscribe(actual); } @Override @@ -96,97 +92,6 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Scannable.from(source).scanUnsafe(key); - return null; - } - - static final class FluxSourceRestoringThreadLocalsSubscriber - implements Fuseable.ConditionalSubscriber, InnerConsumer { - - final CoreSubscriber actual; - final Fuseable.ConditionalSubscriber actualConditional; - - Subscription s; - - @SuppressWarnings("unchecked") - FluxSourceRestoringThreadLocalsSubscriber(CoreSubscriber actual) { - this.actual = actual; - if (actual instanceof Fuseable.ConditionalSubscriber) { - this.actualConditional = (Fuseable.ConditionalSubscriber) actual; - } - else { - this.actualConditional = null; - } - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) { - return s; - } - if (key == Attr.RUN_STYLE) { - return Attr.RunStyle.SYNC; - } - if (key == Attr.ACTUAL) { - return actual; - } - return null; - } - - @Override - public Context currentContext() { - return actual.currentContext(); - } - - @SuppressWarnings("try") - @Override - public void onSubscribe(Subscription s) { - // This is needed, as the downstream can then switch threads, - // continue the subscription using different primitives and omit this operator - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - actual.onSubscribe(s); - } - } - - @SuppressWarnings("try") - @Override - public void onNext(T t) { - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - actual.onNext(t); - } - } - - @SuppressWarnings("try") - @Override - public boolean tryOnNext(T t) { - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - if (actualConditional != null) { - return actualConditional.tryOnNext(t); - } - actual.onNext(t); - return true; - } - } - - @SuppressWarnings("try") - @Override - public void onError(Throwable t) { - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - actual.onError(t); - } - } - - @SuppressWarnings("try") - @Override - public void onComplete() { - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - actual.onComplete(); - } - } + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSourceFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSourceFuseable.java index ffbae0c511..04859c3fc7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSourceFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSourceFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,6 +81,6 @@ public Object scanUnsafe(Scannable.Attr key) { if (key == Scannable.Attr.PREFETCH) return getPrefetch(); if (key == Scannable.Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Scannable.from(source).scanUnsafe(key); - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxStream.java b/reactor-core/src/main/java/reactor/core/publisher/FluxStream.java index 270ddd2045..466a0be78d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxStream.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,6 +72,6 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSubscribeOnCallable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSubscribeOnCallable.java index 944acba2d9..57fde807fc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSubscribeOnCallable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSubscribeOnCallable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.RUN_ON) return scheduler; if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSubscribeOnValue.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSubscribeOnValue.java index 78cbc0c5c8..989249d746 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSubscribeOnValue.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSubscribeOnValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,6 +73,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.RUN_ON) return scheduler; if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchOnFirst.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchOnFirst.java index 78a0dcd700..1e3f20c294 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchOnFirst.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchOnFirst.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -542,7 +542,7 @@ public final void onNext(T t) { return; } - outboundPublisher.subscribe(o); + Operators.toFluxOrMono(outboundPublisher).subscribe(o); return; } @@ -586,7 +586,7 @@ public final void onError(Throwable t) { return; } - result.subscribe(o); + Operators.toFluxOrMono(result).subscribe(o); } } @@ -623,7 +623,7 @@ public final void onComplete() { return; } - result.subscribe(o); + Operators.toFluxOrMono(result).subscribe(o); } } @@ -868,7 +868,7 @@ public boolean tryOnNext(T t) { return true; } - result.subscribe(o); + Operators.toFluxOrMono(result).subscribe(o); return true; } @@ -1013,7 +1013,7 @@ public final Object scanUnsafe(Attr key) { if (key == Attr.CANCELLED) return hasOutboundCancelled(this.parent.state); if (key == Attr.TERMINATED) return hasOutboundTerminated(this.parent.state); - return null; + return InnerOperator.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java index 21d90be019..48d2776125 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTapRestoringThreadLocals.java @@ -88,7 +88,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - + if (key == InternalProducerAttr.INSTANCE) return true; return super.scanUnsafe(key); } @@ -130,7 +130,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return s; if (key == Attr.TERMINATED) return done; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - + if (key == InternalProducerAttr.INSTANCE) return true; return InnerOperator.super.scanUnsafe(key); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxUsing.java b/reactor-core/src/main/java/reactor/core/publisher/FluxUsing.java index 2053e68ffd..70f7da430e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxUsing.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxUsing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,7 +119,7 @@ else if (actual instanceof ConditionalSubscriber) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class UsingSubscriber diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxUsingWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxUsingWhen.java index 774f7415c1..ac9906638b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxUsingWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxUsingWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,7 +91,7 @@ public void subscribe(CoreSubscriber actual) { asyncCancel, null); - p.subscribe(subscriber); + Operators.toFluxOrMono(p).subscribe(subscriber); } } catch (Throwable e) { @@ -101,13 +101,14 @@ public void subscribe(CoreSubscriber actual) { } //trigger the resource creation and delay the subscription to actual - resourceSupplier.subscribe(new ResourceSubscriber(actual, resourceClosure, asyncComplete, asyncError, asyncCancel, resourceSupplier instanceof Mono)); + Operators.toFluxOrMono(resourceSupplier).subscribe(new ResourceSubscriber(actual, + resourceClosure, asyncComplete, asyncError, asyncCancel, resourceSupplier instanceof Mono)); } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } private static Publisher deriveFluxFromResource( @@ -192,7 +193,7 @@ public void onNext(S resource) { final Publisher p = deriveFluxFromResource(resource, resourceClosure); - p.subscribe(FluxUsingWhen.prepareSubscriberForResource(resource, + Operators.toFluxOrMono(p).subscribe(FluxUsingWhen.prepareSubscriberForResource(resource, this.actual, this.asyncComplete, this.asyncError, @@ -361,7 +362,7 @@ public void onError(Throwable t) { return; } - p.subscribe(new RollbackInner(this, t)); + Operators.toFluxOrMono(p).subscribe(new RollbackInner(this, t)); } } @@ -381,7 +382,7 @@ public void onComplete() { return; } - p.subscribe(new CommitInner(this)); + Operators.toFluxOrMono(p).subscribe(new CommitInner(this)); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowPredicate.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowPredicate.java index 53d44cdc13..9324ce7d26 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowPredicate.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowPredicate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,7 +76,7 @@ final class FluxWindowPredicate extends InternalFluxOperator> int prefetch, Predicate predicate, Mode mode) { - super(source); + super(Flux.from(source)); if (prefetch <= 0) { throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowTimeout.java index 3b1c346c66..c916a508b9 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowTimeout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ final class FluxWindowTimeout extends InternalFluxOperator> { TimeUnit unit, Scheduler timer, boolean fairBackpressure) { - super(source); + super(Flux.from(source)); if (timespan <= 0) { throw new IllegalArgumentException("Timeout period must be strictly positive"); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxZip.java b/reactor-core/src/main/java/reactor/core/publisher/FluxZip.java index fc6b2aded3..89f1e1dc38 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxZip.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxZip.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -294,6 +294,8 @@ void handleBoth(CoreSubscriber s, @Nullable Object[] scalars, int n, int sc) { + Operators.toFluxOrMono(srcs); + if (sc != 0 && scalars != null) { if (n != sc) { ZipSingleCoordinator coordinator = @@ -321,7 +323,7 @@ void handleBoth(CoreSubscriber s, public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return prefetch; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class ZipScalarCoordinator implements InnerProducer, diff --git a/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java b/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java index d87576b941..a74c73cd50 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,6 +62,9 @@ public Object scanUnsafe(Attr key) { if (key == Attr.LIFTER) { return liftFunction.name; } + if (key == InternalProducerAttr.INSTANCE) { + return true; + } return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java index f8acbdef5d..3b1cbbfa0e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,9 @@ public Object scanUnsafe(Attr key) { if (key == Attr.LIFTER) { return liftFunction.name; } + if (key == InternalProducerAttr.INSTANCE) { + return true; + } return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/InnerProducer.java b/reactor-core/src/main/java/reactor/core/publisher/InnerProducer.java index 8245345c17..1930bcdff7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/InnerProducer.java +++ b/reactor-core/src/main/java/reactor/core/publisher/InnerProducer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ default Object scanUnsafe(Attr key){ if (key == Attr.ACTUAL) { return actual(); } + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/InternalConnectableFluxOperator.java b/reactor-core/src/main/java/reactor/core/publisher/InternalConnectableFluxOperator.java index 1af4962249..1e8f412c63 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/InternalConnectableFluxOperator.java +++ b/reactor-core/src/main/java/reactor/core/publisher/InternalConnectableFluxOperator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,6 +90,7 @@ public final CorePublisher source() { public Object scanUnsafe(Scannable.Attr key) { if (key == Scannable.Attr.PREFETCH) return getPrefetch(); if (key == Scannable.Attr.PARENT) return source; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/InternalFluxOperator.java b/reactor-core/src/main/java/reactor/core/publisher/InternalFluxOperator.java index 06b13d642f..07f3b151c0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/InternalFluxOperator.java +++ b/reactor-core/src/main/java/reactor/core/publisher/InternalFluxOperator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,12 +54,18 @@ public final void subscribe(CoreSubscriber subscriber) { while (true) { subscriber = operator.subscribeOrReturn(subscriber); if (subscriber == null) { + // if internally subscribed, it means the optimized operator is + // already within the internals and subscribing up the chain will + // at some point need to consider the source and wrap it + // null means "I will subscribe myself", returning... return; } OptimizableOperator newSource = operator.nextOptimizableSource(); if (newSource == null) { - operator.source().subscribe(subscriber); + CorePublisher operatorSource = operator.source(); + subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(operatorSource, subscriber); + operatorSource.subscribe(subscriber); return; } operator = newSource; @@ -89,7 +95,8 @@ public final CorePublisher source() { public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.PARENT) return source; - return null; + if (key == InternalProducerAttr.INSTANCE) return true; + return super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/InternalMonoOperator.java b/reactor-core/src/main/java/reactor/core/publisher/InternalMonoOperator.java index e21db81e8d..6c25c2bb3c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/InternalMonoOperator.java +++ b/reactor-core/src/main/java/reactor/core/publisher/InternalMonoOperator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,6 +48,12 @@ protected InternalMonoOperator(Mono source) { } } + @Override + public Object scanUnsafe(Attr key) { + if (key == InternalProducerAttr.INSTANCE) return true; + return super.scanUnsafe(key); + } + @Override @SuppressWarnings("unchecked") public final void subscribe(CoreSubscriber subscriber) { @@ -56,12 +62,18 @@ public final void subscribe(CoreSubscriber subscriber) { while (true) { subscriber = operator.subscribeOrReturn(subscriber); if (subscriber == null) { + // if internally subscribed, it means the optimized operator is + // already within the internals and subscribing up the chain will + // at some point need to consider the source and wrap it + // null means "I will subscribe myself", returning... return; } OptimizableOperator newSource = operator.nextOptimizableSource(); if (newSource == null) { - operator.source().subscribe(subscriber); + CorePublisher operatorSource = operator.source(); + subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(operatorSource, subscriber); + operatorSource.subscribe(subscriber); return; } operator = newSource; diff --git a/reactor-core/src/main/java/reactor/core/publisher/InternalProducerAttr.java b/reactor-core/src/main/java/reactor/core/publisher/InternalProducerAttr.java new file mode 100644 index 0000000000..910b944289 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/InternalProducerAttr.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import reactor.core.Scannable; + +class InternalProducerAttr extends Scannable.Attr { + + private InternalProducerAttr(Boolean defaultValue) { + super(defaultValue); + } + + static final InternalProducerAttr INSTANCE = new InternalProducerAttr(true); +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index cc5752426d..8faa8a0c41 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -480,18 +480,26 @@ public static Mono firstWithValue(Mono first, Mono Mono from(Publisher source) { //some sources can be considered already assembled monos //all conversion methods (from, fromDirect, wrap) must accommodate for this - if (source instanceof Mono) { + boolean shouldWrap = ContextPropagationSupport.shouldWrapPublisher(source); + if (source instanceof Mono && !shouldWrap) { @SuppressWarnings("unchecked") Mono casted = (Mono) source; return casted; } + if (source instanceof FluxSourceMono || source instanceof FluxSourceMonoFuseable) { @SuppressWarnings("unchecked") FluxFromMonoOperator wrapper = (FluxFromMonoOperator) source; @SuppressWarnings("unchecked") Mono extracted = (Mono) wrapper.source; - return extracted; + boolean shouldWrapExtracted = ContextPropagationSupport.shouldWrapPublisher(extracted); + if (!shouldWrapExtracted) { + return extracted; + } else { + // Skip assembly hook + return wrap(extracted, false); + } } //we delegate to `wrap` and apply assembly hooks @@ -573,7 +581,8 @@ public static Mono fromCompletionStage(Supplier Mono fromDirect(Publisher source){ //some sources can be considered already assembled monos //all conversion methods (from, fromDirect, wrap) must accommodate for this - if(source instanceof Mono){ + boolean shouldWrap = ContextPropagationSupport.shouldWrapPublisher(source); + if (source instanceof Mono && !shouldWrap) { @SuppressWarnings("unchecked") Mono m = (Mono)source; return m; @@ -584,7 +593,14 @@ public static Mono fromDirect(Publisher source){ FluxFromMonoOperator wrapper = (FluxFromMonoOperator) source; @SuppressWarnings("unchecked") Mono extracted = (Mono) wrapper.source; - return extracted; + boolean shouldWrapExtracted = + ContextPropagationSupport.shouldWrapPublisher(extracted); + if (!shouldWrapExtracted) { + return extracted; + } else { + // Skip assembly hook + return wrap(extracted, false); + } } //we delegate to `wrap` and apply assembly hooks @@ -4492,6 +4508,7 @@ public final void subscribe(Subscriber actual) { } } + subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(publisher, subscriber); publisher.subscribe(subscriber); } catch (Throwable e) { @@ -5332,39 +5349,54 @@ static Mono doOnTerminalSignal(Mono source, static Mono wrap(Publisher source, boolean enforceMonoContract) { //some sources can be considered already assembled monos //all conversion methods (from, fromDirect, wrap) must accommodate for this + boolean shouldWrap = ContextPropagationSupport.shouldWrapPublisher(source); if (source instanceof Mono) { - return (Mono) source; + if (!shouldWrap) { + return (Mono) source; + } + return ContextPropagation.monoRestoreThreadLocals((Mono) source); } - if (source instanceof FluxSourceMono - || source instanceof FluxSourceMonoFuseable) { - @SuppressWarnings("unchecked") - Mono extracted = (Mono) ((FluxFromMonoOperator) source).source; - return extracted; + + if (source instanceof FluxSourceMono || source instanceof FluxSourceMonoFuseable) { + @SuppressWarnings("unchecked") Mono extracted = + (Mono) ((FluxFromMonoOperator) source).source; + boolean shouldWrapExtracted = + ContextPropagationSupport.shouldWrapPublisher(extracted); + if (!shouldWrapExtracted) { + return extracted; + } + return ContextPropagation.monoRestoreThreadLocals(extracted); + } + + if (source instanceof Flux && source instanceof Callable) { + @SuppressWarnings("unchecked") Callable m = (Callable) source; + return Flux.wrapToMono(m); } + Mono target; + //equivalent to what from used to be, without assembly hooks if (enforceMonoContract) { - if (source instanceof Flux && source instanceof Callable) { - @SuppressWarnings("unchecked") Callable m = (Callable) source; - return Flux.wrapToMono(m); - } if (source instanceof Flux) { - return new MonoNext<>((Flux) source); + target = new MonoNext<>((Flux) source); + } else { + target = new MonoFromPublisher<>(source); } - return new MonoFromPublisher<>(source); - } - //equivalent to what fromDirect used to be without onAssembly - if(source instanceof Flux && source instanceof Fuseable) { - return new MonoSourceFluxFuseable<>((Flux) source); + } else if (source instanceof Flux && source instanceof Fuseable) { + target = new MonoSourceFluxFuseable<>((Flux) source); + } else if (source instanceof Flux) { + target = new MonoSourceFlux<>((Flux) source); + } else if (source instanceof Fuseable) { + target = new MonoSourceFuseable<>(source); + } else { + target = new MonoSource<>(source); } - if (source instanceof Flux) { - return new MonoSourceFlux<>((Flux) source); - } - if(source instanceof Fuseable) { - return new MonoSourceFuseable<>(source); + + if (shouldWrap) { + return ContextPropagation.monoRestoreThreadLocals(target); } - return new MonoSource<>(source); + return target; } @SuppressWarnings("unchecked") diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCallable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCallable.java index 313e13713b..a6fb161454 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCallable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCallable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,7 +75,7 @@ public T call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static class MonoCallableSubscription diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java index 153b0e0b0d..04bcec2f1e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCompletionStage.java @@ -52,19 +52,15 @@ final class MonoCompletionStage extends Mono @Override public void subscribe(CoreSubscriber actual) { - if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { - actual.onSubscribe( - new MonoCompletionStageRestoringThreadLocalsSubscription<>( - actual, future, suppressCancellation)); - } else { - actual.onSubscribe(new MonoCompletionStageSubscription<>( - actual, future, suppressCancellation)); - } + CoreSubscriber wrapped = Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + wrapped.onSubscribe(new MonoCompletionStageSubscription<>( + wrapped, future, suppressCancellation)); } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } @@ -165,108 +161,4 @@ public void cancel() { } } } - - static class MonoCompletionStageRestoringThreadLocalsSubscription - implements InnerProducer, BiFunction { - - final CoreSubscriber actual; - final CompletionStage future; - final boolean suppressCancellation; - - volatile int requestedOnce; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater REQUESTED_ONCE = - AtomicIntegerFieldUpdater.newUpdater(MonoCompletionStageRestoringThreadLocalsSubscription.class, "requestedOnce"); - - volatile boolean cancelled; - - MonoCompletionStageRestoringThreadLocalsSubscription( - CoreSubscriber actual, - CompletionStage future, - boolean suppressCancellation) { - this.actual = actual; - this.future = future; - this.suppressCancellation = suppressCancellation; - } - - @Override - public CoreSubscriber actual() { - return this.actual; - } - - @Override - public Void apply(@Nullable T value, @Nullable Throwable e) { - final CoreSubscriber actual = this.actual; - - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - if (this.cancelled) { - //nobody is interested in the Mono anymore, don't risk dropping errors - final Context ctx = actual.currentContext(); - if (e == null || e instanceof CancellationException) { - //we discard any potential value and ignore Future cancellations - Operators.onDiscard(value, ctx); - } - else { - //we make sure we keep _some_ track of a Future failure AFTER the Mono cancellation - Operators.onErrorDropped(e, ctx); - //and we discard any potential value just in case both e and v are not null - Operators.onDiscard(value, ctx); - } - - return null; - } - - try { - if (e instanceof CompletionException) { - actual.onError(e.getCause()); - } - else if (e != null) { - actual.onError(e); - } - else if (value != null) { - actual.onNext(value); - actual.onComplete(); - } - else { - actual.onComplete(); - } - } - catch (Throwable e1) { - Operators.onErrorDropped(e1, actual.currentContext()); - throw Exceptions.bubble(e1); - } - return null; - } - } - - @Override - public void request(long n) { - if (this.cancelled) { - return; - } - - if (this.requestedOnce == 1 || !REQUESTED_ONCE.compareAndSet(this, 0 , 1)) { - return; - } - - future.handle(this); - } - - @Override - public void cancel() { - this.cancelled = true; - - final CompletionStage future = this.future; - if (!suppressCancellation && future instanceof Future) { - try { - //noinspection unchecked - ((Future) future).cancel(true); - } - catch (Throwable t) { - Operators.onErrorDropped(t, this.actual.currentContext()); - } - } - } - } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java index 69c533c7f3..eaa1b56fcc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoContextWriteRestoringThreadLocals.java @@ -48,6 +48,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return super.scanUnsafe(key); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCreate.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCreate.java index 1116722470..1f7fbb56c0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCreate.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCreate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,22 +50,26 @@ final class MonoCreate extends Mono implements SourceProducer { @Override public void subscribe(CoreSubscriber actual) { - DefaultMonoSink emitter = new DefaultMonoSink<>(actual); + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); - actual.onSubscribe(emitter); + DefaultMonoSink emitter = new DefaultMonoSink<>(wrapped); + + wrapped.onSubscribe(emitter); try { callback.accept(emitter); } catch (Throwable ex) { - emitter.error(Operators.onOperatorError(ex, actual.currentContext())); + emitter.error(Operators.onOperatorError(ex, wrapped.currentContext())); } } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; - return null; + if (key == InternalProducerAttr.INSTANCE) return true; + return SourceProducer.super.scanUnsafe(key); } static final class DefaultMonoSink extends AtomicBoolean diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCurrentContext.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCurrentContext.java index c12791b4cc..118e9347fc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCurrentContext.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCurrentContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoDefer.java b/reactor-core/src/main/java/reactor/core/publisher/MonoDefer.java index 2ab29550ef..b7ca5cefa1 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoDefer.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoDefer.java @@ -50,12 +50,12 @@ public void subscribe(CoreSubscriber actual) { return; } - p.subscribe(actual); + fromDirect(p).subscribe(actual); } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoDeferContextual.java b/reactor-core/src/main/java/reactor/core/publisher/MonoDeferContextual.java index d5b2109f72..5fbc53fc4d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoDeferContextual.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoDeferContextual.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,12 +52,12 @@ public void subscribe(CoreSubscriber actual) { return; } - p.subscribe(actual); + Operators.toFluxOrMono(p).subscribe(actual); } @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; //no particular key to be represented, still useful in hooks + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoDelay.java b/reactor-core/src/main/java/reactor/core/publisher/MonoDelay.java index 35b40d8ca6..c075ecc612 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoDelay.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoDelay.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,8 +71,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.RUN_ON) return timedScheduler; if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; - - return null; + return SourceProducer.super.scanUnsafe(key); } static final class MonoDelayRunnable implements Runnable, InnerProducer { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoDelayUntil.java b/reactor-core/src/main/java/reactor/core/publisher/MonoDelayUntil.java index f000c44f7a..51eb2610e6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoDelayUntil.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoDelayUntil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,7 +94,7 @@ MonoDelayUntil copyWithNewTriggerGenerator(boolean delayError, @Override public void subscribe(CoreSubscriber actual) { try { - source.subscribe(subscribeOrReturn(actual)); + Operators.toFluxOrMono(source).subscribe(subscribeOrReturn(actual)); } catch (Throwable e) { Operators.error(actual, Operators.onOperatorError(e, actual.currentContext())); @@ -123,6 +123,7 @@ public final CorePublisher source() { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; //no particular key to be represented, still useful in hooks } @@ -303,7 +304,7 @@ void subscribeNextTrigger() { this.triggerSubscriber = triggerSubscriber; } - p.subscribe(triggerSubscriber); + Operators.toFluxOrMono(p).subscribe(triggerSubscriber); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoEmpty.java b/reactor-core/src/main/java/reactor/core/publisher/MonoEmpty.java index 55e1a010df..5a69c4207a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoEmpty.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoEmpty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,6 +78,6 @@ public Object block() { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoError.java b/reactor-core/src/main/java/reactor/core/publisher/MonoError.java index d74dc02976..c06936018b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoError.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoError.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,6 @@ public Object call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoErrorSupplied.java b/reactor-core/src/main/java/reactor/core/publisher/MonoErrorSupplied.java index 339fc39c00..0c0a16cbb7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoErrorSupplied.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoErrorSupplied.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,6 @@ public T call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithSignal.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithSignal.java index d226e52ef9..29fa86f816 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithSignal.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithSignal.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -136,11 +136,13 @@ public void subscribe(CoreSubscriber actual) { actual.currentContext())); } else { - p.subscribe(actual); + Operators.toFluxOrMono(p).subscribe(actual); } return; } + Operators.toFluxOrMono(a); + FluxFirstWithSignal.RaceCoordinator coordinator = new FluxFirstWithSignal.RaceCoordinator<>(n); @@ -150,7 +152,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithValue.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithValue.java index 983148500a..cd2c2e0edd 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithValue.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,7 +147,7 @@ public void subscribe(CoreSubscriber actual) { return; } if (n == 1) { - Publisher p = a[0]; + Publisher p = Mono.from(a[0]); if (p == null) { Operators.error(actual, @@ -169,6 +169,6 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFromFluxOperator.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFromFluxOperator.java index 61d36edb4f..f5db3c1d65 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFromFluxOperator.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFromFluxOperator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,6 +62,7 @@ protected MonoFromFluxOperator(Flux source) { public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return Integer.MAX_VALUE; if (key == Attr.PARENT) return source; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } @@ -78,6 +79,7 @@ public final void subscribe(CoreSubscriber subscriber) { } OptimizableOperator newSource = operator.nextOptimizableSource(); if (newSource == null) { + subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(operator.source(), subscriber); operator.source().subscribe(subscriber); return; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java index 9aa1156288..61ab8be8d1 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFromPublisher.java @@ -55,15 +55,12 @@ final class MonoFromPublisher extends Mono implements Scannable, @Override @SuppressWarnings("unchecked") public void subscribe(CoreSubscriber actual) { - if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { - actual = new MonoSource.MonoSourceRestoringThreadLocalsSubscriber<>(actual); - } - try { CoreSubscriber subscriber = subscribeOrReturn(actual); if (subscriber == null) { return; } + subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(source, subscriber); source.subscribe(subscriber); } catch (Throwable e) { @@ -96,6 +93,9 @@ public Object scanUnsafe(Scannable.Attr key) { if (key == Scannable.Attr.RUN_STYLE) { return Attr.RunStyle.SYNC; } + if (key == InternalProducerAttr.INSTANCE) { + return true; + } return null; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoIgnorePublisher.java b/reactor-core/src/main/java/reactor/core/publisher/MonoIgnorePublisher.java index cfb21ef1e7..9488a7e0f7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoIgnorePublisher.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoIgnorePublisher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ final class MonoIgnorePublisher extends Mono implements Scannable, final OptimizableOperator optimizableOperator; MonoIgnorePublisher(Publisher source) { - this.source = Objects.requireNonNull(source, "publisher"); + this.source = Operators.toFluxOrMono(Objects.requireNonNull(source, "publisher")); if (source instanceof OptimizableOperator) { @SuppressWarnings("unchecked") OptimizableOperator optimSource = (OptimizableOperator) source; @@ -86,6 +86,9 @@ public Object scanUnsafe(Scannable.Attr key) { if (key == Attr.RUN_STYLE) { return Attr.RunStyle.SYNC; } + if (key == InternalProducerAttr.INSTANCE) { + return true; + } return null; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoIgnoreThen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoIgnoreThen.java index 7eef207f20..4627a406c2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoIgnoreThen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoIgnoreThen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,6 +71,7 @@ MonoIgnoreThen shift(Mono newLast) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } @@ -237,7 +238,7 @@ void subscribeNext() { } onComplete(); } else { - m.subscribe(this); + Operators.toFluxOrMono(m).subscribe(this); } return; } else { @@ -260,7 +261,7 @@ void subscribeNext() { continue; } - m.subscribe((CoreSubscriber) this); + Operators.toFluxOrMono(m).subscribe((CoreSubscriber) this); return; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoJust.java b/reactor-core/src/main/java/reactor/core/publisher/MonoJust.java index 37368657a7..4c79998098 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoJust.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoJust.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,6 +59,6 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.BUFFERED) return 1; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoNever.java b/reactor-core/src/main/java/reactor/core/publisher/MonoNever.java index e670ef94f0..6020091848 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoNever.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoNever.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } /** diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoOperator.java b/reactor-core/src/main/java/reactor/core/publisher/MonoOperator.java index 5034d3c8c2..3fc6d7158c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoOperator.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoOperator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,8 @@ package reactor.core.publisher; import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Function; - import org.reactivestreams.Publisher; -import reactor.core.CoreSubscriber; import reactor.core.Scannable; import reactor.util.annotation.Nullable; @@ -51,6 +47,7 @@ protected MonoOperator(Mono source) { public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return Integer.MAX_VALUE; if (key == Attr.PARENT) return source; + if (key == InternalProducerAttr.INSTANCE) return false; // public class! return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoRetryWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoRetryWhen.java index 9ebeec9898..76bd7d7c14 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoRetryWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoRetryWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ final class MonoRetryWhen extends InternalMonoOperator { final Retry whenSourceFactory; MonoRetryWhen(Mono source, Retry whenSourceFactory) { - super(source); + super(Mono.fromDirect(source)); this.whenSourceFactory = Objects.requireNonNull(whenSourceFactory, "whenSourceFactory"); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoRunnable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoRunnable.java index 1f30b8db31..e84ee52af8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoRunnable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoRunnable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,7 @@ public Void call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class MonoRunnableEagerSubscription extends AtomicBoolean implements Subscription { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSequenceEqual.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSequenceEqual.java index 960edfebf9..8dd645eca2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSequenceEqual.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSequenceEqual.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,8 +42,8 @@ final class MonoSequenceEqual extends Mono implements SourceProducer MonoSequenceEqual(Publisher first, Publisher second, BiPredicate comparer, int prefetch) { - this.first = Objects.requireNonNull(first, "first"); - this.second = Objects.requireNonNull(second, "second"); + this.first = Operators.toFluxOrMono(Objects.requireNonNull(first, "first")); + this.second = Operators.toFluxOrMono(Objects.requireNonNull(second, "second")); this.comparer = Objects.requireNonNull(comparer, "comparer"); if(prefetch < 1){ throw new IllegalArgumentException("Buffer size must be strictly positive: " + @@ -64,7 +64,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return prefetch; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class EqualCoordinator implements InnerProducer { diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSingleCallable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSingleCallable.java index dcbdbdcdf4..f06c97ab0c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSingleCallable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSingleCallable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -122,6 +122,6 @@ else if (v == null) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptionalCallable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptionalCallable.java index 360e53ce45..f074b4620e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptionalCallable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSingleOptionalCallable.java @@ -93,6 +93,6 @@ public Optional call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java index d4bcccf472..f910f31d79 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSource.java @@ -65,11 +65,7 @@ final class MonoSource extends Mono implements Scannable, SourceProducer actual) { - if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { - source.subscribe(new MonoSourceRestoringThreadLocalsSubscriber<>(actual)); - } else { - source.subscribe(actual); - } + source.subscribe(actual); } @Override @@ -96,92 +92,6 @@ public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) { return Scannable.from(source).scanUnsafe(key); } - return null; - } - - static final class MonoSourceRestoringThreadLocalsSubscriber - implements InnerConsumer { - - final CoreSubscriber actual; - - Subscription s; - boolean done; - - MonoSourceRestoringThreadLocalsSubscriber(CoreSubscriber actual) { - this.actual = actual; - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) { - return s; - } - if (key == Attr.RUN_STYLE) { - return Attr.RunStyle.SYNC; - } - if (key == Attr.ACTUAL) { - return actual; - } - return null; - } - - @Override - public Context currentContext() { - return actual.currentContext(); - } - - @SuppressWarnings("try") - @Override - public void onSubscribe(Subscription s) { - // This is needed, as the downstream can then switch threads, - // continue the subscription using different primitives and omit this operator - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - actual.onSubscribe(s); - } - } - - @SuppressWarnings("try") - @Override - public void onNext(T t) { - this.done = true; - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - actual.onNext(t); - actual.onComplete(); - } - } - - @SuppressWarnings("try") - @Override - public void onError(Throwable t) { - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - if (this.done) { - Operators.onErrorDropped(t, actual.currentContext()); - return; - } - - this.done = true; - - actual.onError(t); - } - } - - @SuppressWarnings("try") - @Override - public void onComplete() { - if (this.done) { - return; - } - - this.done = true; - - try (ContextSnapshot.Scope ignored = - ContextPropagation.setThreadLocals(actual.currentContext())) { - actual.onComplete(); - } - } + return SourceProducer.super.scanUnsafe(key); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSubscribeOnCallable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSubscribeOnCallable.java index b4853fb322..ea53f9a505 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSubscribeOnCallable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSubscribeOnCallable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,6 +62,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Scannable.Attr key) { if (key == Scannable.Attr.RUN_ON) return scheduler; if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSubscribeOnValue.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSubscribeOnValue.java index e99aeec10f..c63c18631e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSubscribeOnValue.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSubscribeOnValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.RUN_ON) return scheduler; if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSupplier.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSupplier.java index ac0efa5999..7ed6bfcddd 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSupplier.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ public T call() throws Exception { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static class MonoSupplierSubscription diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTapRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTapRestoringThreadLocals.java index 14b329c99b..84d8715f97 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTapRestoringThreadLocals.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTapRestoringThreadLocals.java @@ -83,6 +83,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return -1; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return super.scanUnsafe(key); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoUsing.java b/reactor-core/src/main/java/reactor/core/publisher/MonoUsing.java index 001fe3b7cd..a227587cc6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoUsing.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoUsing.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,8 +82,8 @@ public void subscribe(CoreSubscriber actual) { Mono p; try { - p = Objects.requireNonNull(sourceFactory.apply(resource), - "The sourceFactory returned a null value"); + p = Mono.fromDirect(Objects.requireNonNull(sourceFactory.apply(resource), + "The sourceFactory returned a null value")); } catch (Throwable e) { @@ -117,7 +117,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class MonoUsingSubscriber diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoUsingWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoUsingWhen.java index 73bbeee498..507c78be77 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoUsingWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoUsingWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,7 +84,7 @@ public void subscribe(CoreSubscriber actual) { asyncCancel, null); - p.subscribe(subscriber); + fromDirect(p).subscribe(subscriber); } } catch (Throwable e) { @@ -93,7 +93,8 @@ public void subscribe(CoreSubscriber actual) { return; } - resourceSupplier.subscribe(new ResourceSubscriber(actual, resourceClosure, + Operators.toFluxOrMono(resourceSupplier).subscribe(new ResourceSubscriber(actual, + resourceClosure, asyncComplete, asyncError, asyncCancel, resourceSupplier instanceof Mono)); } @@ -101,7 +102,7 @@ public void subscribe(CoreSubscriber actual) { @Override public Object scanUnsafe(Attr key) { if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } private static Mono deriveMonoFromResource( @@ -180,7 +181,7 @@ public void onNext(S resource) { final Mono p = deriveMonoFromResource(resource, resourceClosure); - p.subscribe(MonoUsingWhen.prepareSubscriberForResource(resource, + fromDirect(p).subscribe(MonoUsingWhen.prepareSubscriberForResource(resource, this.actual, this.asyncComplete, this.asyncError, diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoWhen.java index 0ea2aaae06..fa9ab34966 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,6 +95,8 @@ public void subscribe(CoreSubscriber actual) { return; } + Operators.toFluxOrMono(a); + WhenCoordinator parent = new WhenCoordinator(a, actual, n, delayError); actual.onSubscribe(parent); } @@ -104,7 +106,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.DELAY_ERROR) return delayError; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class WhenCoordinator implements InnerProducer, diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoZip.java b/reactor-core/src/main/java/reactor/core/publisher/MonoZip.java index dac6dacf1d..2f06e02bab 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoZip.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoZip.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -122,6 +122,10 @@ public void subscribe(CoreSubscriber actual) { return; } + for (int i = 0; i < n; i++) { + a[i] = Mono.fromDirect(a[i]); + } + actual.onSubscribe(new ZipCoordinator<>(a, actual, n, delayError, zipper)); } @@ -129,7 +133,7 @@ public void subscribe(CoreSubscriber actual) { public Object scanUnsafe(Attr key) { if (key == Attr.DELAY_ERROR) return delayError; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; - return null; + return SourceProducer.super.scanUnsafe(key); } static final class ZipCoordinator implements InnerProducer, diff --git a/reactor-core/src/main/java/reactor/core/publisher/Operators.java b/reactor-core/src/main/java/reactor/core/publisher/Operators.java index 65fbf9e282..3d60285c48 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Operators.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Operators.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -991,6 +991,57 @@ public static CorePublisher onLastAssembly(CorePublisher source) { } } + public static CorePublisher toFluxOrMono(Publisher publisher) { + if (publisher instanceof Mono) { + return Mono.fromDirect(publisher); + } + return Flux.from(publisher); + } + + public static void toFluxOrMono(Publisher[] sources) { + for (int i = 0; i < sources.length; i++) { + if (sources[i] != null) { + sources[i] = toFluxOrMono(sources[i]); + } + } + } + + static CoreSubscriber restoreContextOnSubscriberIfPublisherNonInternal( + Publisher publisher, CoreSubscriber subscriber) { + if (ContextPropagationSupport.shouldWrapPublisher(publisher)) { + return restoreContextOnSubscriber(publisher, subscriber); + } + return subscriber; + } + + static CoreSubscriber restoreContextOnSubscriberIfAutoCPEnabled( + Publisher publisher, CoreSubscriber subscriber) { + if (ContextPropagationSupport.shouldPropagateContextToThreadLocals()) { + return restoreContextOnSubscriber(publisher, subscriber); + } + return subscriber; + } + + static CoreSubscriber restoreContextOnSubscriber(Publisher publisher, CoreSubscriber subscriber) { + if (publisher instanceof Fuseable) { + return new FluxContextWriteRestoringThreadLocals.FuseableContextWriteRestoringThreadLocalsSubscriber<>( + subscriber, subscriber.currentContext()); + } else { + return new FluxContextWriteRestoringThreadLocals.ContextWriteRestoringThreadLocalsSubscriber<>( + subscriber, + subscriber.currentContext()); + } + } + + static CoreSubscriber[] restoreContextOnSubscribers( + Publisher publisher, CoreSubscriber[] subscribers) { + CoreSubscriber[] actualSubscribers = new CoreSubscriber[subscribers.length]; + for (int i = 0; i < subscribers.length; i++) { + actualSubscribers[i] = restoreContextOnSubscriber(publisher, subscribers[i]); + } + return actualSubscribers; + } + private static Throwable unwrapOnNextError(Throwable error) { return Exceptions.isBubbling(error) ? error : Exceptions.unwrap(error); } @@ -1476,24 +1527,14 @@ static int unboundedOrLimit(int prefetch, int lowTide) { Operators() { } - static final class CorePublisherAdapter implements CorePublisher, - OptimizableOperator { + static final class CorePublisherAdapter implements CorePublisher { final Publisher publisher; - @Nullable - final OptimizableOperator optimizableOperator; - CorePublisherAdapter(Publisher publisher) { this.publisher = publisher; - if (publisher instanceof OptimizableOperator) { - @SuppressWarnings("unchecked") - OptimizableOperator optimSource = (OptimizableOperator) publisher; - this.optimizableOperator = optimSource; - } - else { - this.optimizableOperator = null; - } + // note: if publisher is not CorePublisher it can't be an + // OptimizableOperator, which extends CorePublisher } @Override @@ -1505,21 +1546,6 @@ public void subscribe(CoreSubscriber subscriber) { public void subscribe(Subscriber s) { publisher.subscribe(s); } - - @Override - public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - return actual; - } - - @Override - public final CorePublisher source() { - return this; - } - - @Override - public final OptimizableOperator nextOptimizableSource() { - return optimizableOperator; - } } static final Fuseable.ConditionalSubscriber EMPTY_SUBSCRIBER = new Fuseable.ConditionalSubscriber() { @@ -2656,6 +2682,11 @@ final static class LiftFunction final Predicate filter; final String name; + // TODO: this leaks to the users of LiftFunction, encapsulation is broken + // consider: liftFunction.lifter.apply could go through encapsulation + // like: liftFunction.applyLifter() where what lifter.apply returns is wrapped + // unconditionally; otherwise -> all lift* operators need to be considered as + // NOT INTERNAL_PRODUCER sources final BiFunction, ? extends CoreSubscriber> lifter; diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelArraySource.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelArraySource.java index 3c4b66b513..ab40cc971d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelArraySource.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelArraySource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,6 @@ public void subscribe(CoreSubscriber[] subscribers) { } int n = subscribers.length; - for (int i = 0; i < n; i++) { Flux.from(sources[i]).subscribe(subscribers[i]); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelCollect.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelCollect.java index ff385e3a18..a903d52d68 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelCollect.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelCollect.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ final class ParallelCollect extends ParallelFlux implements Scannable, ParallelCollect(ParallelFlux source, Supplier initialCollection, BiConsumer collector) { - this.source = source; + this.source = ParallelFlux.from(source); this.initialCollection = initialCollection; this.collector = collector; } @@ -54,6 +54,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelConcatMap.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelConcatMap.java index 58bcff0661..8db76a6d6d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelConcatMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelConcatMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ final class ParallelConcatMap extends ParallelFlux implements Scannable Function> mapper, Supplier> queueSupplier, int prefetch, ErrorMode errorMode) { - this.source = source; + this.source = ParallelFlux.from(source); this.mapper = Objects.requireNonNull(mapper, "mapper"); this.queueSupplier = Objects.requireNonNull(queueSupplier, "queueSupplier"); this.prefetch = prefetch; @@ -64,6 +64,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.DELAY_ERROR) return errorMode != ErrorMode.IMMEDIATE; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelDoOnEach.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelDoOnEach.java index 03f1a6e134..eff7dabc37 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelDoOnEach.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelDoOnEach.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ final class ParallelDoOnEach extends ParallelFlux implements Scannable { @Nullable BiConsumer onError, @Nullable Consumer onComplete ) { - this.source = source; + this.source = ParallelFlux.from(source); this.onNext = onNext; this.onError = onError; @@ -98,6 +98,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelFilter.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelFilter.java index 1b960d0b3b..376bb583b0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelFilter.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ final class ParallelFilter extends ParallelFlux implements Scannable{ final Predicate predicate; ParallelFilter(ParallelFlux source, Predicate predicate) { - this.source = source; + this.source = ParallelFlux.from(source); this.predicate = predicate; } @@ -45,6 +45,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelFlatMap.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelFlatMap.java index 21e2316d88..314e812d0e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelFlatMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelFlatMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ final class ParallelFlatMap extends ParallelFlux implements Scannable{ boolean delayError, int maxConcurrency, Supplier> mainQueueSupplier, int prefetch, Supplier> innerQueueSupplier) { - this.source = source; + this.source = ParallelFlux.from(source); this.mapper = mapper; this.delayError = delayError; this.maxConcurrency = maxConcurrency; @@ -69,6 +69,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.DELAY_ERROR) return delayError; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelFlux.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelFlux.java index f225decbc7..e1e63e2fbb 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelFlux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelFlux.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,13 @@ */ public abstract class ParallelFlux implements CorePublisher { + static ParallelFlux from(ParallelFlux source) { + if (ContextPropagationSupport.shouldWrapPublisher(source)) { + return new ParallelFluxRestoringThreadLocals<>(source); + } + return source; + } + /** * Take a Publisher and prepare to consume it on multiple 'rails' (one per CPU core) * in a round-robin fashion. Equivalent to {@link Flux#parallel}. diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxHide.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxHide.java index abcd3df8fa..6fafe6718f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxHide.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxHide.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ final class ParallelFluxHide extends ParallelFlux implements Scannable{ final ParallelFlux source; ParallelFluxHide(ParallelFlux source) { - this.source = source; + this.source = ParallelFlux.from(source); } @Override @@ -51,6 +51,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxName.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxName.java index 8053ddc06f..9bbcfd526a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxName.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxName.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ static ParallelFlux createOrAppend(ParallelFlux source, String tagName ParallelFluxName(ParallelFlux source, @Nullable String name, @Nullable List> tags) { - this.source = source; + this.source = ParallelFlux.from(source); this.name = name; this.tagsWithDuplicates = tags; } @@ -113,6 +113,8 @@ public Object scanUnsafe(Attr key) { return SYNC; } + if (key == InternalProducerAttr.INSTANCE) return true; + return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxOnAssembly.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxOnAssembly.java index e0672b2507..a530efb857 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxOnAssembly.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxOnAssembly.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ final class ParallelFluxOnAssembly extends ParallelFlux * Create an assembly trace wrapping a {@link ParallelFlux}. */ ParallelFluxOnAssembly(ParallelFlux source, AssemblySnapshot stacktrace) { - this.source = source; + this.source = ParallelFlux.from(source); this.stacktrace = stacktrace; } @@ -97,6 +97,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.ACTUAL_METADATA) return !stacktrace.isCheckpoint; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxRestoringThreadLocals.java new file mode 100644 index 0000000000..946cd0cae2 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelFluxRestoringThreadLocals.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import reactor.core.CoreSubscriber; +import reactor.core.Scannable; + +class ParallelFluxRestoringThreadLocals extends ParallelFlux implements + Scannable { + + private final ParallelFlux source; + + ParallelFluxRestoringThreadLocals(ParallelFlux source) { + this.source = source; + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + @Override + public void subscribe(CoreSubscriber[] subscribers) { + CoreSubscriber[] actualSubscribers = + Operators.restoreContextOnSubscribers(source, subscribers); + + source.subscribe(actualSubscribers); + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return source; + if (key == Attr.PREFETCH) return getPrefetch(); + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; + return null; + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelGroup.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelGroup.java index b9f2733b44..5cda4f54d5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelGroup.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelGroup.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ final class ParallelGroup extends Flux> implements final ParallelFlux source; ParallelGroup(ParallelFlux source) { - this.source = source; + this.source = ParallelFlux.from(source); } @Override @@ -65,6 +65,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java index 2f186c2d29..83fa0b940b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ final class ParallelLift extends ParallelFlux implements Scannable { ParallelLift(ParallelFlux p, Operators.LiftFunction liftFunction) { - this.source = Objects.requireNonNull(p, "source"); + this.source = ParallelFlux.from(Objects.requireNonNull(p, "source")); this.liftFunction = liftFunction; } @@ -62,6 +62,8 @@ public Object scanUnsafe(Attr key) { if (key == Attr.LIFTER) { return liftFunction.name; } + // We don't control what the lifter does, so we play it safe. + if (key == InternalProducerAttr.INSTANCE) return false; return null; } @@ -81,6 +83,11 @@ public void subscribe(CoreSubscriber[] s) { int i = 0; while (i < subscribers.length) { + // As this is not an INTERNAL_PRODUCER, the subscribers should be protected + // in case of automatic context propagation. + // If a user directly subscribes with a set of rails, there is no + // protection against that, so a ThreadLocal restoring subscriber would + // need to be provided. subscribers[i] = Objects.requireNonNull(liftFunction.lifter.apply(source, s[i]), "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java index 4fd1025a3f..2d3ae6c28c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ final class ParallelLiftFuseable extends ParallelFlux ParallelLiftFuseable(ParallelFlux p, Operators.LiftFunction liftFunction) { - this.source = Objects.requireNonNull(p, "source"); + this.source = ParallelFlux.from(Objects.requireNonNull(p, "source")); this.liftFunction = liftFunction; } @@ -65,6 +65,8 @@ public Object scanUnsafe(Attr key) { if (key == Attr.LIFTER) { return liftFunction.name; } + // We don't control what the lifter does, so we play it safe. + if (key == InternalProducerAttr.INSTANCE) return false; return null; } @@ -85,6 +87,11 @@ public void subscribe(CoreSubscriber[] s) { int i = 0; while (i < subscribers.length) { CoreSubscriber actual = s[i]; + // As this is not an INTERNAL_PRODUCER, the subscribers should be protected + // in case of automatic context propagation. + // If a user directly subscribes with a set of rails, there is no + // protection against that, so a ThreadLocal restoring subscriber would + // need to be provided. CoreSubscriber converted = Objects.requireNonNull(liftFunction.lifter.apply(source, actual), "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelLog.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelLog.java index 60ae219317..bda6d4b54c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelLog.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelLog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ final class ParallelLog extends ParallelFlux implements Scannable { ParallelLog(ParallelFlux source, SignalPeek log ) { - this.source = source; + this.source = ParallelFlux.from(source); this.log = log; } @@ -81,6 +81,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelMap.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelMap.java index ee0a619914..7d978cc824 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ final class ParallelMap extends ParallelFlux implements Scannable { final Function mapper; ParallelMap(ParallelFlux source, Function mapper) { - this.source = source; + this.source = ParallelFlux.from(source); this.mapper = mapper; } @@ -46,6 +46,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeOrdered.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeOrdered.java index ee070166dc..84a4ed206a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeOrdered.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeOrdered.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ final class ParallelMergeOrdered extends Flux implements Scannable { if (prefetch <= 0) { throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch); } - this.source = source; + this.source = ParallelFlux.from(source); this.prefetch = prefetch; this.valueComparator = valueComparator; } @@ -57,6 +57,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return prefetch; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeReduce.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeReduce.java index a4ea01aa5d..0fca4881ed 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeReduce.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeReduce.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ final class ParallelMergeReduce extends Mono implements Scannable, Fuseabl ParallelMergeReduce(ParallelFlux source, BiFunction reducer) { - this.source = source; + this.source = ParallelFlux.from(source); this.reducer = reducer; } @@ -51,6 +51,7 @@ final class ParallelMergeReduce extends Mono implements Scannable, Fuseabl public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeSequential.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeSequential.java index 2664d05889..9a1b01cf81 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeSequential.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeSequential.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ final class ParallelMergeSequential extends Flux implements Scannable { if (prefetch <= 0) { throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch); } - this.source = source; + this.source = ParallelFlux.from(source); this.prefetch = prefetch; this.queueSupplier = queueSupplier; } @@ -57,6 +57,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeSort.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeSort.java index 1ff75a4cd6..26edb3b366 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeSort.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelMergeSort.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ final class ParallelMergeSort extends Flux implements Scannable { ParallelMergeSort(ParallelFlux> source, Comparator comparator) { - this.source = source; + this.source = ParallelFlux.from(source); this.comparator = comparator; } @@ -71,6 +71,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelPeek.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelPeek.java index 4b0bbda432..65b4ab4899 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelPeek.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelPeek.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ final class ParallelPeek extends ParallelFlux implements SignalPeek{ @Nullable LongConsumer onRequest, @Nullable Runnable onCancel ) { - this.source = source; + this.source = ParallelFlux.from(source); this.onNext = onNext; this.onAfterNext = onAfterNext; @@ -153,6 +153,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelReduceSeed.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelReduceSeed.java index 131f00b263..4bb7c04264 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelReduceSeed.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelReduceSeed.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ final class ParallelReduceSeed extends ParallelFlux implements ParallelReduceSeed(ParallelFlux source, Supplier initialSupplier, BiFunction reducer) { - this.source = source; + this.source = ParallelFlux.from(source); this.initialSupplier = initialSupplier; this.reducer = reducer; } @@ -55,6 +55,7 @@ public Object scanUnsafe(Scannable.Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelRunOn.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelRunOn.java index c58af4ae37..c006024dfe 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelRunOn.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelRunOn.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ final class ParallelRunOn extends ParallelFlux implements Scannable{ if (prefetch <= 0) { throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch); } - this.source = parent; + this.source = ParallelFlux.from(parent); this.scheduler = scheduler; this.prefetch = prefetch; this.queueSupplier = queueSupplier; @@ -57,6 +57,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelSource.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelSource.java index 8453552a7a..5a5016165f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelSource.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ final class ParallelSource extends ParallelFlux implements Scannable { if (prefetch <= 0) { throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch); } - this.source = source; + this.source = Operators.toFluxOrMono(source); this.parallelism = parallelism; this.prefetch = prefetch; this.queueSupplier = queueSupplier; @@ -76,6 +76,7 @@ public Object scanUnsafe(Scannable.Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelThen.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelThen.java index f8470b3312..dd8fce93bf 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelThen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelThen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ final class ParallelThen extends Mono implements Scannable, Fuseable { final ParallelFlux source; ParallelThen(ParallelFlux source) { - this.source = source; + this.source = ParallelFlux.from(source); } @Override @@ -42,6 +42,7 @@ final class ParallelThen extends Mono implements Scannable, Fuseable { public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkEmptyMulticast.java b/reactor-core/src/main/java/reactor/core/publisher/SinkEmptyMulticast.java index 46e3fa43c3..c50da3773c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkEmptyMulticast.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkEmptyMulticast.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -124,6 +124,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return isTerminated(subscribers); if (key == Attr.ERROR) return subscribers == TERMINATED_ERROR ? error : null; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } @@ -196,8 +197,10 @@ void remove(Inner ps) { //redefined in SinkOneMulticast @Override public void subscribe(final CoreSubscriber actual) { - Inner as = new VoidInner<>(actual, this); - actual.onSubscribe(as); + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + Inner as = new VoidInner<>(wrapped, this); + wrapped.onSubscribe(as); final int addedState = add(as); if (addedState == STATE_ADDED) { if (as.isCancelled()) { @@ -207,7 +210,7 @@ public void subscribe(final CoreSubscriber actual) { else if (addedState == STATE_ERROR) { Throwable ex = error; - actual.onError(ex); + wrapped.onError(ex); } else { as.complete(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java index 3c63f094b5..aaf439db5d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyBestEffort.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,6 +77,7 @@ public Stream inners() { public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return subscribers == TERMINATED; if (key == Attr.ERROR) return error; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } @@ -191,8 +192,11 @@ public Flux asFlux() { public void subscribe(CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe(null) is forbidden"); - DirectInner p = new DirectInner<>(actual, this); - actual.onSubscribe(p); + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + + DirectInner p = new DirectInner<>(wrapped, this); + wrapped.onSubscribe(p); if (p.isCancelled()) { return; @@ -206,10 +210,10 @@ public void subscribe(CoreSubscriber actual) { else { Throwable e = error; if (e != null) { - actual.onError(e); + wrapped.onError(e); } else { - actual.onComplete(); + wrapped.onComplete(); } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyEmitterProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyEmitterProcessor.java index ad815c50a4..96762282a0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyEmitterProcessor.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyEmitterProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -165,8 +165,12 @@ public Disposable subscribeTo(Publisher upstream) { @Override public void subscribe(CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe"); - EmitterInner inner = new EmitterInner<>(actual, this); - actual.onSubscribe(inner); + + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + + EmitterInner inner = new EmitterInner<>(wrapped, this); + wrapped.onSubscribe(inner); if (inner.isCancelled()) { return; @@ -373,6 +377,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return isTerminated(); if (key == Attr.ERROR) return getError(); if (key == Attr.CAPACITY) return getPrefetch(); + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyReplayProcessor.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyReplayProcessor.java index 9493e79738..6ba04bbf04 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyReplayProcessor.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyReplayProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -295,8 +295,11 @@ static SinkManyReplayProcessor createSizeAndTimeout(int size, @Override public void subscribe(CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe"); - FluxReplay.ReplaySubscription rs = new ReplayInner<>(actual, this); - actual.onSubscribe(rs); + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + + FluxReplay.ReplaySubscription rs = new ReplayInner<>(wrapped, this); + wrapped.onSubscribe(rs); if (add(rs)) { if (rs.isCancelled()) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java index d266bd3cd4..9396b1f10b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -183,6 +183,7 @@ public Object scanUnsafe(Attr key) { if (Attr.CANCELLED == key) return cancelled; if (Attr.TERMINATED == key) return done; if (Attr.ERROR == key) return error; + if (InternalProducerAttr.INSTANCE == key) return true; return null; } @@ -408,18 +409,20 @@ public Context currentContext() { @Override public void subscribe(CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe"); + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { this.hasDownstream = true; - actual.onSubscribe(this); - this.actual = actual; + wrapped.onSubscribe(this); + this.actual = wrapped; if (cancelled) { this.hasDownstream = false; } else { drain(null); } } else { - Operators.error(actual, new IllegalStateException("Sinks.many().unicast() sinks only allow a single Subscriber")); + Operators.error(wrapped, new IllegalStateException("Sinks.many().unicast() sinks only allow a single Subscriber")); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicastNoBackpressure.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicastNoBackpressure.java index 3c9d12f539..c1fd5d01d2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicastNoBackpressure.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicastNoBackpressure.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,13 +76,17 @@ public Flux asFlux() { public void subscribe(CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe"); + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + if (!STATE.compareAndSet(this, State.INITIAL, State.SUBSCRIBED)) { - Operators.reportThrowInSubscribe(actual, new IllegalStateException("Unicast Sinks.Many allows only a single Subscriber")); + Operators.reportThrowInSubscribe(wrapped, new IllegalStateException( + "Unicast Sinks.Many allows only a single Subscriber")); return; } - this.actual = actual; - actual.onSubscribe(this); + this.actual = wrapped; + wrapped.onSubscribe(this); } @Override @@ -190,6 +194,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.ACTUAL) return actual; if (key == Attr.TERMINATED) return state == State.TERMINATED; if (key == Attr.CANCELLED) return state == State.CANCELLED; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkOneMulticast.java b/reactor-core/src/main/java/reactor/core/publisher/SinkOneMulticast.java index 2dd03f7693..7c5709bf51 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkOneMulticast.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkOneMulticast.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package reactor.core.publisher; import java.time.Duration; -import java.util.Objects; import reactor.core.CoreSubscriber; import reactor.core.publisher.Sinks.EmitResult; @@ -75,14 +74,17 @@ public Object scanUnsafe(Attr key) { if (key == Attr.TERMINATED) return isTerminated(subscribers); if (key == Attr.ERROR) return subscribers == TERMINATED_ERROR ? error : null; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } @Override public void subscribe(final CoreSubscriber actual) { - NextInner as = new NextInner<>(actual, this); - actual.onSubscribe(as); + CoreSubscriber wrapped = + Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); + NextInner as = new NextInner<>(wrapped, this); + wrapped.onSubscribe(as); final int addState = add(as); if (addState == STATE_ADDED) { if (as.isCancelled()) { @@ -90,7 +92,7 @@ public void subscribe(final CoreSubscriber actual) { } } else if (addState == STATE_ERROR) { - actual.onError(error); + wrapped.onError(error); } else if (addState == STATE_EMPTY) { as.complete(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/SourceProducer.java b/reactor-core/src/main/java/reactor/core/publisher/SourceProducer.java index b00f302522..b1f2498092 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SourceProducer.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SourceProducer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,9 @@ interface SourceProducer extends Scannable, Publisher { @Override @Nullable default Object scanUnsafe(Attr key) { - if (key == Attr.PARENT) return Scannable.from(null); - if (key == Attr.ACTUAL) return Scannable.from(null); + if (key == Attr.PARENT) return null; + if (key == Attr.ACTUAL) return null; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java index 00736448ee..988bb0aacc 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java @@ -16,7 +16,12 @@ package reactor.core.publisher; +import java.io.File; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; @@ -25,23 +30,37 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; import io.micrometer.context.ContextRegistry; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.core.CorePublisher; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; import reactor.core.scheduler.Schedulers; import reactor.test.publisher.TestPublisher; import reactor.test.subscriber.TestSubscriber; import reactor.util.concurrent.Queues; import reactor.util.context.Context; +import reactor.util.function.Tuples; +import reactor.util.retry.Retry; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; public class AutomaticContextPropagationTest { @@ -165,7 +184,7 @@ void threadLocalsPresentInDoOnRequest() { } @Test - void threadLocalsPresentInDoAfterTerminate() throws InterruptedException { + void threadLocalsPresentInDoAfterTerminate() throws InterruptedException, TimeoutException { AtomicReference tlValue = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); @@ -181,113 +200,1263 @@ void threadLocalsPresentInDoAfterTerminate() throws InterruptedException { // Need to synchronize, as the doAfterTerminate operator can race with the // assertion. First, blockLast receives the completion signal, and only then, // the callback is triggered. - latch.await(10, TimeUnit.MILLISECONDS); + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } assertThat(tlValue.get()).isEqualTo("present"); } - @Test - void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { - AtomicReference requestTlValue = new AtomicReference<>(); - AtomicReference subscribeTlValue = new AtomicReference<>(); - AtomicReference firstNextTlValue = new AtomicReference<>(); - AtomicReference secondNextTlValue = new AtomicReference<>(); - AtomicReference cancelTlValue = new AtomicReference<>(); + @Test + void contextCapturePropagatedAutomaticallyToAllSignals() throws InterruptedException { + AtomicReference requestTlValue = new AtomicReference<>(); + AtomicReference subscribeTlValue = new AtomicReference<>(); + AtomicReference firstNextTlValue = new AtomicReference<>(); + AtomicReference secondNextTlValue = new AtomicReference<>(); + AtomicReference cancelTlValue = new AtomicReference<>(); + + CountDownLatch itemDelivered = new CountDownLatch(1); + CountDownLatch cancelled = new CountDownLatch(1); + + TestSubscriber subscriber = + TestSubscriber.builder().initialRequest(1).build(); + + REF.set("downstreamContext"); + + Flux.just(1, 2, 3) + .hide() + .doOnRequest(r -> requestTlValue.set(REF.get())) + .doOnNext(i -> firstNextTlValue.set(REF.get())) + .doOnSubscribe(s -> subscribeTlValue.set(REF.get())) + .doOnCancel(() -> { + cancelTlValue.set(REF.get()); + cancelled.countDown(); + }) + .delayElements(Duration.ofMillis(1)) + .contextWrite(Context.of(KEY, "upstreamContext")) + // disabling prefetching to observe cancellation + .publishOn(Schedulers.parallel(), 1) + .doOnNext(i -> { + secondNextTlValue.set(REF.get()); + itemDelivered.countDown(); + }) + .subscribeOn(Schedulers.boundedElastic()) + .contextCapture() + .subscribe(subscriber); + + itemDelivered.await(); + + subscriber.cancel(); + + cancelled.await(); + + assertThat(requestTlValue.get()).isEqualTo("upstreamContext"); + assertThat(subscribeTlValue.get()).isEqualTo("upstreamContext"); + assertThat(firstNextTlValue.get()).isEqualTo("upstreamContext"); + assertThat(cancelTlValue.get()).isEqualTo("upstreamContext"); + assertThat(secondNextTlValue.get()).isEqualTo("downstreamContext"); + } + + @Test + void prefetchingShouldMaintainThreadLocals() { + // We validate streams of items above default prefetch size + // (max concurrency of flatMap == Queues.SMALL_BUFFER_SIZE == 256) + // are able to maintain the context propagation to ThreadLocals + // in the presence of prefetching + int size = Queues.SMALL_BUFFER_SIZE * 10; + + Flux source = Flux.create(s -> { + for (int i = 0; i < size; i++) { + s.next(i); + } + s.complete(); + }); + + assertThat(REF.get()).isEqualTo("ref_init"); + + ArrayBlockingQueue innerThreadLocals = new ArrayBlockingQueue<>(size); + ArrayBlockingQueue outerThreadLocals = new ArrayBlockingQueue<>(size); + + source.publishOn(Schedulers.boundedElastic()) + .flatMap(i -> Mono.just(i) + .delayElement(Duration.ofMillis(1)) + .doOnNext(j -> innerThreadLocals.add(REF.get()))) + .contextWrite(ctx -> ctx.put(KEY, "present")) + .publishOn(Schedulers.parallel()) + .doOnNext(i -> outerThreadLocals.add(REF.get())) + .blockLast(); + + assertThat(innerThreadLocals).containsOnly("present").hasSize(size); + assertThat(outerThreadLocals).containsOnly("ref_init").hasSize(size); + } + + @Test + void fluxApiUsesContextPropagationConstantFunction() { + Flux source = Flux.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(FluxContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("flux's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) + ); + } + + @Test + void monoApiUsesContextPropagationConstantFunction() { + Mono source = Mono.empty(); + assertThat(source.contextCapture()) + .isInstanceOfSatisfying(MonoContextWriteRestoringThreadLocals.class, + fcw -> assertThat(fcw.doOnContext) + .as("mono's capture function") + .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE)); + } + + @Nested + class NonReactorFluxOrMono { + + private ExecutorService executorService; + + @BeforeEach + void enableAutomaticContextPropagation() { + executorService = Executors.newFixedThreadPool(3); + } + + @AfterEach + void cleanupThreadLocals() { + executorService.shutdownNow(); + } + + // Scaffold methods + + private ThreadSwitchingFlux threadSwitchingFlux() { + return new ThreadSwitchingFlux<>("Hello", executorService); + } + + private ThreadSwitchingMono threadSwitchingMono() { + return new ThreadSwitchingMono<>("Hello", executorService); + } + + void assertThreadLocalsPresentInFlux(Supplier> chainSupplier) { + assertThreadLocalsPresentInFlux(chainSupplier, false); + } + + void assertThreadLocalsPresentInFlux(Supplier> chainSupplier, + boolean skipCoreSubscriber) { + assertThreadLocalsPresent(chainSupplier.get()); + assertThatNoException().isThrownBy(() -> + assertThatThreadLocalsPresentDirectRawSubscribe(chainSupplier.get())); + if (!skipCoreSubscriber) { + assertThatNoException().isThrownBy(() -> + assertThatThreadLocalsPresentDirectCoreSubscribe(chainSupplier.get())); + } + } + + void assertThreadLocalsPresentInMono(Supplier> chainSupplier) { + assertThreadLocalsPresentInMono(chainSupplier, false); + } + + void assertThreadLocalsPresentInMono(Supplier> chainSupplier, + boolean skipCoreSubscriber) { + assertThreadLocalsPresent(chainSupplier.get()); + assertThatNoException().isThrownBy(() -> + assertThatThreadLocalsPresentDirectRawSubscribe(chainSupplier.get())); + if (!skipCoreSubscriber) { + assertThatNoException().isThrownBy(() -> + assertThatThreadLocalsPresentDirectCoreSubscribe(chainSupplier.get())); + } + } + + void assertThreadLocalsPresent(Flux chain) { + AtomicReference tlInOnNext = new AtomicReference<>(); + AtomicReference tlInOnComplete = new AtomicReference<>(); + AtomicReference tlInOnError = new AtomicReference<>(); + + AtomicBoolean hadNext = new AtomicBoolean(false); + AtomicBoolean hadError = new AtomicBoolean(false); + + chain.doOnEach(signal -> { + if (signal.isOnNext()) { + tlInOnNext.set(REF.get()); + hadNext.set(true); + } else if (signal.isOnError()) { + tlInOnError.set(REF.get()); + hadError.set(true); + } else if (signal.isOnComplete()) { + tlInOnComplete.set(REF.get()); + } + }) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + if (hadNext.get()) { + assertThat(tlInOnNext.get()).isEqualTo("present"); + } + if (hadError.get()) { + assertThat(tlInOnError.get()).isEqualTo("present"); + } else { + assertThat(tlInOnComplete.get()).isEqualTo("present"); + } + } + + void assertThreadLocalsPresent(Mono chain) { + AtomicReference tlInOnNext = new AtomicReference<>(); + AtomicReference tlInOnComplete = new AtomicReference<>(); + AtomicReference tlInOnError = new AtomicReference<>(); + + AtomicBoolean hadNext = new AtomicBoolean(false); + AtomicBoolean hadError = new AtomicBoolean(false); + + chain.doOnEach(signal -> { + if (signal.isOnNext()) { + tlInOnNext.set(REF.get()); + hadNext.set(true); + } else if (signal.isOnError()) { + tlInOnError.set(REF.get()); + hadError.set(true); + } else if (signal.isOnComplete()) { + tlInOnComplete.set(REF.get()); + } + }) + .contextWrite(Context.of(KEY, "present")) + .block(); + + if (hadNext.get()) { + assertThat(tlInOnNext.get()).isEqualTo("present"); + } + if (hadError.get()) { + assertThat(tlInOnError.get()).isEqualTo("present"); + } else { + assertThat(tlInOnComplete.get()).isEqualTo("present"); + } + } + + void assertThatThreadLocalsPresentDirectCoreSubscribe( + CorePublisher source) throws InterruptedException, TimeoutException { + assertThatThreadLocalsPresentDirectCoreSubscribe(source, () -> {}); + } + + void assertThatThreadLocalsPresentDirectCoreSubscribe( + CorePublisher source, Runnable asyncAction) throws InterruptedException, TimeoutException { + AtomicReference valueInOnNext = new AtomicReference<>(); + AtomicReference valueInOnComplete = new AtomicReference<>(); + AtomicReference valueInOnError = new AtomicReference<>(); + AtomicReference error = new AtomicReference<>(); + AtomicBoolean complete = new AtomicBoolean(); + AtomicBoolean hadNext = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + + CoreSubscriberWithContext subscriberWithContext = + new CoreSubscriberWithContext<>( + valueInOnNext, valueInOnComplete, valueInOnError, + error, latch, hadNext, complete); + + source.subscribe(subscriberWithContext); + + executorService.submit(asyncAction); + + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } + + if (hadNext.get()) { + assertThat(valueInOnNext.get()).isEqualTo("present"); + } + if (error.get() == null) { + assertThat(valueInOnComplete.get()).isEqualTo("present"); + assertThat(complete).isTrue(); + } else { + assertThat(valueInOnError.get()).isEqualTo("present"); + } + } + + // We force the use of subscribe(Subscriber) override instead of + // subscribe(CoreSubscriber), and we can observe that for such a case we + // are able to wrap the Subscriber and restore ThreadLocal values for the + // signals received downstream. + void assertThatThreadLocalsPresentDirectRawSubscribe( + Publisher source) throws InterruptedException, TimeoutException { + assertThatThreadLocalsPresentDirectRawSubscribe(source, () -> {}); + } + + void assertThatThreadLocalsPresentDirectRawSubscribe( + Publisher source, Runnable asyncAction) throws InterruptedException, TimeoutException { + AtomicReference valueInOnNext = new AtomicReference<>(); + AtomicReference valueInOnComplete = new AtomicReference<>(); + AtomicReference valueInOnError = new AtomicReference<>(); + AtomicReference error = new AtomicReference<>(); + AtomicBoolean hadNext = new AtomicBoolean(); + AtomicBoolean complete = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + + CoreSubscriberWithContext subscriberWithContext = + new CoreSubscriberWithContext<>( + valueInOnNext, valueInOnComplete, valueInOnError, + error, latch, hadNext, complete); + + source.subscribe(subscriberWithContext); + + executorService.submit(asyncAction); + + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } + + if (hadNext.get()) { + assertThat(valueInOnNext.get()).isEqualTo("present"); + } + if (error.get() == null) { + assertThat(valueInOnComplete.get()).isEqualTo("present"); + assertThat(complete).isTrue(); + } else { + assertThat(valueInOnError.get()).isEqualTo("present"); + } + } + + // Fundamental tests for Flux + + @Test + void fluxSubscribe() { + assertThreadLocalsPresentInFlux(this::threadSwitchingFlux, true); + } + + @Test + void internalFluxFlatMapSubscribe() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("hello") + .flatMap(item -> threadSwitchingFlux())); + } + + @Test + void internalFluxSubscribeNoFusion() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("hello") + .hide() + .flatMap(item -> threadSwitchingFlux())); + } + + @Test + void directFluxSubscribeAsCoreSubscriber() throws InterruptedException, TimeoutException { + AtomicReference valueInOnNext = new AtomicReference<>(); + AtomicReference valueInOnComplete = new AtomicReference<>(); + AtomicReference valueInOnError = new AtomicReference<>(); + AtomicReference error = new AtomicReference<>(); + AtomicBoolean hadNext = new AtomicBoolean(); + AtomicBoolean complete = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + + Flux flux = threadSwitchingFlux(); + + CoreSubscriberWithContext subscriberWithContext = + new CoreSubscriberWithContext<>( + valueInOnNext, valueInOnComplete, valueInOnError, + error, latch, hadNext, complete); + + flux.subscribe(subscriberWithContext); + + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } + + assertThat(error.get()).isNull(); + assertThat(complete.get()).isTrue(); + + // We can't do anything here. subscribe(CoreSubscriber) is abstract in + // CoreSubscriber interface and we have no means to intercept the calls to + // restore ThreadLocals. + assertThat(valueInOnNext.get()).isEqualTo("ref_init"); + assertThat(valueInOnComplete.get()).isEqualTo("ref_init"); + } + + // Fundamental tests for Mono + + @Test + void monoSubscribe() { + assertThreadLocalsPresentInMono(this::threadSwitchingMono, true); + } + + @Test + void internalMonoFlatMapSubscribe() { + assertThreadLocalsPresentInMono(() -> + Mono.just("hello") + .flatMap(item -> threadSwitchingMono())); + } + + @Test + void directMonoSubscribeAsCoreSubscriber() throws InterruptedException, TimeoutException { + AtomicReference valueInOnNext = new AtomicReference<>(); + AtomicReference valueInOnComplete = new AtomicReference<>(); + AtomicReference valueInOnError = new AtomicReference<>(); + AtomicReference error = new AtomicReference<>(); + AtomicBoolean complete = new AtomicBoolean(); + AtomicBoolean hadNext = new AtomicBoolean(); + CountDownLatch latch = new CountDownLatch(1); + + Mono mono = new ThreadSwitchingMono<>("Hello", executorService); + + CoreSubscriberWithContext subscriberWithContext = + new CoreSubscriberWithContext<>( + valueInOnNext, valueInOnComplete, valueInOnError, + error, latch, hadNext, complete); + + mono.subscribe(subscriberWithContext); + + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } + + assertThat(error.get()).isNull(); + assertThat(complete.get()).isTrue(); + + // We can't do anything here. subscribe(CoreSubscriber) is abstract in + // CoreSubscriber interface and we have no means to intercept the calls to + // restore ThreadLocals. + assertThat(valueInOnNext.get()).isEqualTo("ref_init"); + assertThat(valueInOnComplete.get()).isEqualTo("ref_init"); + } + + // Flux tests + + @Test + void fluxCreate() { + Supplier> fluxSupplier = + () -> Flux.create(sink -> executorService.submit(() -> { + sink.next("Hello"); + sink.complete(); + })); + + assertThreadLocalsPresentInFlux(fluxSupplier); + } + + @Test + void fluxMap() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingFlux().map(String::toUpperCase)); + } + + @Test + void fluxIgnoreThenSwitchThread() { + assertThreadLocalsPresentInMono(() -> Flux.just("Bye").then(threadSwitchingMono())); + } + + @Test + void fluxSwitchThreadThenIgnore() { + assertThreadLocalsPresentInMono(() -> threadSwitchingFlux().then(Mono.just("Hi"))); + } + + @Test + void fluxDeferContextual() { + assertThreadLocalsPresentInFlux(() -> + Flux.deferContextual(ctx -> threadSwitchingFlux())); + } + + @Test + void fluxFirstWithSignalArray() { + assertThreadLocalsPresentInFlux(() -> + Flux.firstWithSignal(threadSwitchingFlux())); + assertThreadLocalsPresentInFlux(() -> + Flux.firstWithSignal(threadSwitchingFlux()).or(threadSwitchingFlux())); + } + + @Test + void fluxFirstWithSignalIterable() { + assertThreadLocalsPresentInFlux(() -> + Flux.firstWithSignal(Collections.singletonList(threadSwitchingFlux()))); + assertThreadLocalsPresentInFlux(() -> + Flux.firstWithSignal(Stream.of(threadSwitchingFlux(), threadSwitchingFlux()).collect(Collectors.toList()))); + } + + @Test + void fluxRetryWhen() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux().retryWhen(Retry.max(1))); + } + + @Test + void fluxRetryWhenSwitchingThread() { + assertThreadLocalsPresentInFlux(() -> + Flux.error(new RuntimeException("Oops")) + .retryWhen(Retry.from(f -> threadSwitchingFlux()))); + } + + @Test + void fluxWindowUntil() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux().windowUntil(s -> true) + .flatMap(Function.identity())); + } + + @Test + void switchOnFirst() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .switchOnFirst((s, f) -> f.map(String::toUpperCase))); + } + + @Test + void switchOnFirstFuseable() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .filter("Hello"::equals) + .switchOnFirst((s, f) -> f.map(String::toUpperCase))); + } + + @Test + void switchOnFirstSwitchThread() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .switchOnFirst((s, f) -> threadSwitchingFlux())); + } + + @Test + void switchOnFirstFuseableSwitchThread() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .filter("Hello"::equals) + .switchOnFirst((s, f) -> threadSwitchingFlux())); + } + + @Test + void fluxWindowTimeout() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .windowTimeout(1, Duration.ofDays(1), true)); + } + + @Test + void fluxMergeComparing() { + assertThreadLocalsPresentInFlux(() -> + Flux.mergeComparing(Flux.empty(), threadSwitchingFlux())); + } + + @Test + void fluxFirstWithValueArray() { + assertThreadLocalsPresentInFlux(() -> + Flux.firstWithValue(Flux.empty(), threadSwitchingFlux())); + } + + @Test + void fluxFirstWithValueIterable() { + assertThreadLocalsPresentInFlux(() -> + Flux.firstWithValue( + Stream.of(Flux.empty(), threadSwitchingFlux()) + .collect(Collectors.toList()))); + } + + @Test + void fluxConcatArray() { + assertThreadLocalsPresentInFlux(() -> + Flux.concat(Mono.empty(), threadSwitchingFlux())); + } + + @Test + void fluxConcatIterable() { + assertThreadLocalsPresentInFlux(() -> + Flux.concat( + Stream.of(Flux.empty(), threadSwitchingFlux()).collect(Collectors.toList()))); + } + + @Test + void fluxGenerate() { + assertThreadLocalsPresentInFlux(() -> Flux.generate(sink -> { + sink.next("Hello"); + // the generator is checked if any signal was delivered by the consumer + // so we perform asynchronous completion only + executorService.submit(sink::complete); + })); + } + + @Test + void fluxCombineLatest() { + assertThreadLocalsPresentInFlux(() -> + Flux.combineLatest( + Flux.just(""), threadSwitchingFlux(), (s1, s2) -> s2)); + } + + @Test + void fluxUsing() { + assertThreadLocalsPresentInFlux(() -> + Flux.using(() -> 0, i -> threadSwitchingFlux(), i -> {})); + } + + @Test + void fluxZip() { + assertThreadLocalsPresentInFlux(() -> + Flux.zip(Flux.just(""), threadSwitchingFlux())); + } + + @Test + void fluxZipIterable() { + assertThreadLocalsPresentInFlux(() -> + Flux.zip(Stream.of(Flux.just(""), threadSwitchingFlux()).collect(Collectors.toList()), + obj -> Tuples.of((String) obj[0], (String) obj[1]))); + } + + // Mono tests + + @Test + void monoCreate() { + assertThreadLocalsPresentInMono(() -> + Mono.create(sink -> { + executorService.submit(() -> { + sink.success("Hello"); + }); + })); + } + + @Test + void monoSwitchThreadIgnoreThen() { + assertThreadLocalsPresentInMono(() -> + threadSwitchingMono().then(Mono.just("Bye"))); + } + + @Test + void monoIgnoreThenSwitchThread() { + assertThreadLocalsPresentInMono(() -> + Mono.just("Bye").then(threadSwitchingMono())); + } + + @Test + void monoSwitchThreadDelayUntil() { + assertThreadLocalsPresentInMono(() -> + threadSwitchingMono().delayUntil(s -> Mono.delay(Duration.ofMillis(1)))); + } + + @Test + void monoDelayUntilSwitchingThread() { + assertThreadLocalsPresentInMono(() -> + Mono.just("Hello").delayUntil(s -> threadSwitchingMono())); + } + + @Test + void monoIgnoreSwitchingThread() { + assertThreadLocalsPresentInMono(() -> + Mono.ignoreElements(threadSwitchingMono())); + } + + @Test + void monoDeferContextual() { + assertThreadLocalsPresentInMono(() -> + Mono.deferContextual(ctx -> threadSwitchingMono())); + } + + @Test + void monoDefer() { + assertThreadLocalsPresentInMono(() -> + Mono.defer(this::threadSwitchingMono)); + } + + @Test + void monoFirstWithSignalArray() { + assertThreadLocalsPresentInMono(() -> + Mono.firstWithSignal(threadSwitchingMono())); + + assertThreadLocalsPresentInMono(() -> + Mono.firstWithSignal(threadSwitchingMono()) + .or(threadSwitchingMono())); + } + + @Test + void monoFirstWithSignalIterable() { + assertThreadLocalsPresentInMono(() -> + Mono.firstWithSignal(Collections.singletonList(threadSwitchingMono()))); + + assertThreadLocalsPresentInMono(() -> + Mono.firstWithSignal( + Stream.of(threadSwitchingMono(), threadSwitchingMono()) + .collect(Collectors.toList()))); + } + + @Test + void monoFromFluxSingle() { + assertThreadLocalsPresentInMono(() -> + threadSwitchingFlux().single()); + } + + @Test + void monoRetryWhen() { + assertThreadLocalsPresentInMono(() -> + threadSwitchingMono().retryWhen(Retry.max(1))); + } + + @Test + void monoRetryWhenSwitchingThread() { + assertThreadLocalsPresentInMono(() -> + Mono.error(new RuntimeException("Oops")) + .retryWhen(Retry.from(f -> threadSwitchingMono()))); + } + + @Test + void monoUsing() { + assertThreadLocalsPresentInMono(() -> + Mono.using(() -> "Hello", + seed -> threadSwitchingMono(), + seed -> {}, + false)); + } + + @Test + void monoFirstWithValueArray() { + assertThreadLocalsPresentInMono(() -> + Mono.firstWithValue(Mono.empty(), threadSwitchingMono())); + } + + @Test + void monoFirstWithValueIterable() { + assertThreadLocalsPresentInMono(() -> + Mono.firstWithValue( + Stream.of(Mono.empty(), threadSwitchingMono()) + .collect(Collectors.toList()))); + } + + @Test + void monoZip() { + assertThreadLocalsPresentInMono(() -> + Mono.zip(Mono.just(""), threadSwitchingMono())); + } + + @Test + void monoZipIterable() { + assertThreadLocalsPresentInMono(() -> + Mono.zip( + Stream.of(Mono.just(""), threadSwitchingMono()) + .collect(Collectors.toList()), + obj -> Tuples.of((String) obj[0], (String) obj[1]))); + } + + @Test + void monoSequenceEqual() { + assertThreadLocalsPresentInMono(() -> + Mono.sequenceEqual(Mono.just("Hello"), threadSwitchingMono())); + } + + @Test + void monoWhen() { + assertThreadLocalsPresentInMono(() -> + Mono.when(Mono.empty(), threadSwitchingMono())); + } + + @Test + void monoUsingWhen() { + assertThreadLocalsPresentInMono(() -> + Mono.usingWhen(Mono.just("Hello"), s -> threadSwitchingMono(), + s -> Mono.empty())); + } + + // ParallelFlux tests + + @Test + void parallelFluxFromMonoToMono() { + assertThreadLocalsPresentInMono(() -> + Mono.from(ParallelFlux.from(threadSwitchingMono()))); + } + + @Test + void parallelFluxFromMonoToFlux() { + assertThreadLocalsPresentInFlux(() -> + Flux.from(ParallelFlux.from(threadSwitchingMono()))); + } + + @Test + void parallelFluxFromFluxToMono() { + assertThreadLocalsPresentInMono(() -> + Mono.from(ParallelFlux.from(threadSwitchingFlux()))); + } + + @Test + void parallelFluxFromFluxToFlux() { + assertThreadLocalsPresentInFlux(() -> + Flux.from(ParallelFlux.from(threadSwitchingFlux()))); + } + + @Test + void parallelFluxLift() { + assertThreadLocalsPresentInFlux(() -> { + ParallelFlux parallelFlux = ParallelFlux.from(Flux.just("Hello")); + + Publisher lifted = + Operators.liftPublisher((pub, sub) -> new CoreSubscriber() { + @Override + public void onSubscribe(Subscription s) { + executorService.submit(() -> sub.onSubscribe(s)); + } + + @Override + public void onNext(String s) { + executorService.submit(() -> sub.onNext(s)); + } + + @Override + public void onError(Throwable t) { + executorService.submit(() -> sub.onError(t)); + } + + @Override + public void onComplete() { + executorService.submit(sub::onComplete); + } + }) + .apply(parallelFlux); + + return ((ParallelFlux) lifted).sequential(); + }); + } + + @Test + void parallelFluxLiftFuseable() { + assertThreadLocalsPresentInFlux(() -> { + ParallelFlux> parallelFlux = + ParallelFlux.from(Flux.just("Hello")) + .collect(ArrayList::new, ArrayList::add); + + Publisher> lifted = + Operators., ArrayList>liftPublisher((pub, sub) -> new CoreSubscriber>() { + @Override + public void onSubscribe(Subscription s) { + executorService.submit(() -> sub.onSubscribe(s)); + } + + @Override + public void onNext(ArrayList s) { + executorService.submit(() -> sub.onNext(s)); + } + + @Override + public void onError(Throwable t) { + executorService.submit(() -> sub.onError(t)); + } + + @Override + public void onComplete() { + executorService.submit(sub::onComplete); + } + }) + .apply(parallelFlux); + + return ((ParallelFlux) lifted).sequential(); + }); + } + + @Test + void parallelFluxFromThreadSwitchingMono() { + assertThreadLocalsPresentInFlux(() -> + ParallelFlux.from(threadSwitchingMono()).sequential()); + } + + @Test + void parallelFluxFromThreadSwitchingFlux() { + assertThreadLocalsPresentInFlux(() -> + ParallelFlux.from(threadSwitchingFlux()).sequential()); + } + + @Test + void threadSwitchingParallelFluxSequential() { + AtomicReference value = new AtomicReference<>(); + new ThreadSwitchingParallelFlux<>("Hello", executorService) + .sequential() + .doOnNext(i -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void threadSwitchingParallelFluxThen() { + assertThreadLocalsPresentInMono(() -> + new ThreadSwitchingParallelFlux<>("Hello", executorService) + .then()); + } + + @Test + void threadSwitchingParallelFluxOrdered() { + assertThreadLocalsPresentInFlux(() -> + new ThreadSwitchingParallelFlux<>("Hello", executorService) + .ordered(Comparator.naturalOrder())); + } + + @Test + void threadSwitchingParallelFluxReduce() { + AtomicReference value = new AtomicReference<>(); + new ThreadSwitchingParallelFlux<>("Hello", executorService) + .reduce((s1, s2) -> s2) + .doOnNext(i -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .block(); + + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void threadSwitchingParallelFluxReduceSeed() { + AtomicReference value = new AtomicReference<>(); + new ThreadSwitchingParallelFlux<>("Hello", executorService) + .reduce(ArrayList::new, (l, s) -> { + value.set(REF.get()); + l.add(s); + return l; + }) + .sequential() + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void threadSwitchingParallelFluxGroup() { + AtomicReference value = new AtomicReference<>(); + new ThreadSwitchingParallelFlux<>("Hello", executorService) + .groups() + .doOnNext(i -> value.set(REF.get())) + .flatMap(Flux::last) + .contextWrite(Context.of(KEY, "present")) + .blockLast(); + + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void threadSwitchingParallelFluxSort() { + assertThreadLocalsPresentInFlux(() -> + new ThreadSwitchingParallelFlux<>("Hello", executorService) + .sorted(Comparator.naturalOrder())); + } + + // Sinks tests + + @Test + void sink() throws InterruptedException, TimeoutException { + AtomicReference value = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Sinks.One sink = Sinks.one(); + + sink.asMono() + .doOnNext(i -> { + value.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService.submit(() -> sink.tryEmitValue(1)); + + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } + + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void sinkDirect() throws InterruptedException, TimeoutException { + Sinks.One sink1 = Sinks.one(); + assertThatThreadLocalsPresentDirectCoreSubscribe(sink1.asMono(), + () -> sink1.tryEmitValue("Hello")); + + Sinks.One sink2 = Sinks.one(); + assertThatThreadLocalsPresentDirectRawSubscribe(sink2.asMono(), + () -> sink2.tryEmitValue("Hello")); + } + + @Test + void sinksEmpty() throws InterruptedException, TimeoutException { + AtomicReference value = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Sinks.Empty spec = Sinks.empty(); + + spec.asMono() + .doOnSuccess(ignored -> { + value.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService.submit(spec::tryEmitEmpty); + + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } + + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void sinksEmptyDirect() throws InterruptedException, TimeoutException { + Sinks.Empty empty1 = Sinks.empty(); + assertThatThreadLocalsPresentDirectCoreSubscribe(empty1.asMono(), empty1::tryEmitEmpty); + + Sinks.Empty empty2 = Sinks.empty(); + assertThatThreadLocalsPresentDirectRawSubscribe(empty2.asMono(), empty2::tryEmitEmpty); + } + + @Test + void sinkManyUnicast() throws InterruptedException, TimeoutException { + AtomicReference value = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Sinks.ManySpec spec = Sinks.many(); + + Sinks.Many many = spec.unicast() + .onBackpressureBuffer(); + many.asFlux() + .doOnNext(i -> { + value.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); - CountDownLatch itemDelivered = new CountDownLatch(1); - CountDownLatch cancelled = new CountDownLatch(1); + executorService.submit(() -> many.tryEmitNext("Hello")); - TestSubscriber subscriber = - TestSubscriber.builder().initialRequest(1).build(); + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } - REF.set("downstreamContext"); + assertThat(value.get()).isEqualTo("present"); + } - Flux.just(1, 2, 3) - .hide() - .doOnRequest(r -> requestTlValue.set(REF.get())) - .doOnNext(i -> firstNextTlValue.set(REF.get())) - .doOnSubscribe(s -> subscribeTlValue.set(REF.get())) - .doOnCancel(() -> { - cancelTlValue.set(REF.get()); - cancelled.countDown(); - }) - .delayElements(Duration.ofMillis(1)) - .contextWrite(Context.of(KEY, "upstreamContext")) - // disabling prefetching to observe cancellation - .publishOn(Schedulers.parallel(), 1) - .doOnNext(i -> { - System.out.println(REF.get()); - secondNextTlValue.set(REF.get()); - itemDelivered.countDown(); - }) - .subscribeOn(Schedulers.boundedElastic()) - .contextCapture() - .subscribe(subscriber); + @Test + void sinkManyUnicastDirect() throws InterruptedException, TimeoutException { + Sinks.Many many1 = Sinks.many().unicast() + .onBackpressureBuffer(); + + assertThatThreadLocalsPresentDirectCoreSubscribe(many1.asFlux(), () -> { + many1.tryEmitNext("Hello"); + many1.tryEmitComplete(); + }); + + Sinks.Many many2 = Sinks.many().unicast() + .onBackpressureBuffer(); + + assertThatThreadLocalsPresentDirectRawSubscribe(many2.asFlux(), () -> { + many2.tryEmitNext("Hello"); + many2.tryEmitComplete(); + }); + } - itemDelivered.await(); + @Test + void sinkManyUnicastNoBackpressure() throws InterruptedException, + TimeoutException { + AtomicReference value = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); - subscriber.cancel(); + Sinks.ManySpec spec = Sinks.many(); - cancelled.await(); + Sinks.Many many = spec.unicast().onBackpressureError(); + many.asFlux() + .doOnNext(i -> { + value.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); - assertThat(requestTlValue.get()).isEqualTo("upstreamContext"); - assertThat(subscribeTlValue.get()).isEqualTo("upstreamContext"); - assertThat(firstNextTlValue.get()).isEqualTo("upstreamContext"); - assertThat(cancelTlValue.get()).isEqualTo("upstreamContext"); - assertThat(secondNextTlValue.get()).isEqualTo("downstreamContext"); - } + executorService.submit(() -> many.tryEmitNext("Hello")); - @Test - void prefetchingShouldMaintainThreadLocals() { - // We validate streams of items above default prefetch size - // (max concurrency of flatMap == Queues.SMALL_BUFFER_SIZE == 256) - // are able to maintain the context propagation to ThreadLocals - // in the presence of prefetching - int size = Queues.SMALL_BUFFER_SIZE * 10; + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } - Flux source = Flux.create(s -> { - for (int i = 0; i < size; i++) { - s.next(i); + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void sinkManyMulticastAllOrNothing() throws InterruptedException, + TimeoutException { + AtomicReference value = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Sinks.ManySpec spec = Sinks.many(); + + Sinks.Many many = spec.multicast().directAllOrNothing(); + many.asFlux() + .doOnNext(i -> { + value.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService.submit(() -> many.tryEmitNext("Hello")); + + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); } - s.complete(); - }); - assertThat(REF.get()).isEqualTo("ref_init"); + assertThat(value.get()).isEqualTo("present"); + } - ArrayBlockingQueue innerThreadLocals = new ArrayBlockingQueue<>(size); - ArrayBlockingQueue outerThreadLocals = new ArrayBlockingQueue<>(size); + @Test + void sinkManyMulticastBuffer() throws InterruptedException, TimeoutException { + AtomicReference value = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); - source.publishOn(Schedulers.boundedElastic()) - .flatMap(i -> Mono.just(i) - .delayElement(Duration.ofMillis(1)) - .doOnNext(j -> innerThreadLocals.add(REF.get()))) - .contextWrite(ctx -> ctx.put(KEY, "present")) - .publishOn(Schedulers.parallel()) - .doOnNext(i -> outerThreadLocals.add(REF.get())) - .blockLast(); + Sinks.ManySpec spec = Sinks.many(); - assertThat(innerThreadLocals).containsOnly("present").hasSize(size); - assertThat(outerThreadLocals).containsOnly("ref_init").hasSize(size); - } + Sinks.Many many = spec.multicast().onBackpressureBuffer(); + many.asFlux() + .doOnNext(i -> { + value.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); - @Test - void fluxApiUsesContextPropagationConstantFunction() { - Flux source = Flux.empty(); - assertThat(source.contextCapture()) - .isInstanceOfSatisfying(FluxContextWriteRestoringThreadLocals.class, - fcw -> assertThat(fcw.doOnContext) - .as("flux's capture function") - .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE) - ); - } + executorService.submit(() -> many.tryEmitNext("Hello")); - @Test - void monoApiUsesContextPropagationConstantFunction() { - Mono source = Mono.empty(); - assertThat(source.contextCapture()) - .isInstanceOfSatisfying(MonoContextWriteRestoringThreadLocals.class, - fcw -> assertThat(fcw.doOnContext) - .as("mono's capture function") - .isSameAs(ContextPropagation.WITH_GLOBAL_REGISTRY_NO_PREDICATE)); + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } + + assertThat(value.get()).isEqualTo("present"); + } + + @Test + void sinkManyMulticastBestEffort() throws InterruptedException, TimeoutException { + AtomicReference value = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + Sinks.ManySpec spec = Sinks.many(); + + Sinks.Many many = spec.multicast().directBestEffort(); + many.asFlux() + .doOnNext(i -> { + value.set(REF.get()); + latch.countDown(); + }) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService.submit(() -> many.tryEmitNext("Hello")); + + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } + + assertThat(value.get()).isEqualTo("present"); + } + + // Other + + List> getAllClassesInClasspathRecursively(File directory) throws Exception { + List> classes = new ArrayList<>(); + + for (File file : directory.listFiles()) { + if (file.isDirectory()) { + classes.addAll(getAllClassesInClasspathRecursively(file)); + } else if (file.getName().endsWith(".class") ) { + String path = file.getPath(); + path = path.replace("./build/classes/java/main/reactor/", ""); + String pkg = path.substring(0, path.lastIndexOf("/") + 1).replace("/", + "."); + String name = path.substring(path.lastIndexOf("/") + 1).replace(".class", ""); + try { + classes.add(Class.forName("reactor." + pkg + name)); + } + catch (ClassNotFoundException ex) { + System.out.println("Ignoring " + pkg + name); + } catch (NoClassDefFoundError err) { + System.out.println("Ignoring " + pkg + name); + } + } + } + + return classes; + } + + @Test + @Disabled("Used to find Publishers that can switch threads") + void printInterestingClasses() throws Exception { + List> allClasses = + getAllClassesInClasspathRecursively(new File("./build/classes/java/main/reactor/")); + + System.out.println("Classes that are Publisher, but not SourceProducer, " + + "ConnectableFlux, ParallelFlux, GroupedFlux, MonoFromFluxOperator, " + + "FluxFromMonoOperator:"); + for (Class c : allClasses) { + if (Publisher.class.isAssignableFrom(c) && !SourceProducer.class.isAssignableFrom(c) + && !ConnectableFlux.class.isAssignableFrom(c) + && !ParallelFlux.class.isAssignableFrom(c) + && !GroupedFlux.class.isAssignableFrom(c) + && !MonoFromFluxOperator.class.isAssignableFrom(c) + && !FluxFromMonoOperator.class.isAssignableFrom(c)) { + if (Flux.class.isAssignableFrom(c) && !FluxOperator.class.isAssignableFrom(c)) { + System.out.println(c.getName()); + } + if (Mono.class.isAssignableFrom(c) && !MonoOperator.class.isAssignableFrom(c)) { + System.out.println(c.getName()); + } + } + } + + System.out.println("Classes that are Fuseable and Publisher but not Mono or Flux, ?"); + for (Class c : allClasses) { + if (Fuseable.class.isAssignableFrom(c) && Publisher.class.isAssignableFrom(c) + && !Mono.class.isAssignableFrom(c) + && !Flux.class.isAssignableFrom(c)) { + System.out.println(c.getName()); + } + } + } + + private class CoreSubscriberWithContext implements CoreSubscriber { + + private final AtomicReference valueInOnNext; + private final AtomicReference valueInOnComplete; + private final AtomicReference valueInOnError; + private final AtomicReference error; + private final CountDownLatch latch; + private final AtomicBoolean complete; + private final AtomicBoolean hadNext; + + public CoreSubscriberWithContext( + AtomicReference valueInOnNext, + AtomicReference valueInOnComplete, + AtomicReference valueInOnError, + AtomicReference error, + CountDownLatch latch, + AtomicBoolean hadNext, + AtomicBoolean complete) { + this.valueInOnNext = valueInOnNext; + this.valueInOnComplete = valueInOnComplete; + this.valueInOnError = valueInOnError; + this.error = error; + this.latch = latch; + this.hadNext = hadNext; + this.complete = complete; + } + + @Override + public Context currentContext() { + return Context.of(KEY, "present"); + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + hadNext.set(true); + valueInOnNext.set(REF.get()); + } + + @Override + public void onError(Throwable t) { + error.set(t); + valueInOnError.set(REF.get()); + latch.countDown(); + } + + @Override + public void onComplete() { + complete.set(true); + valueInOnComplete.set(REF.get()); + latch.countDown(); + } + } } @Nested @@ -326,6 +1495,41 @@ void fluxFromPublisher() throws InterruptedException, ExecutionException { executorService.shutdownNow(); } + @Test + void fluxFlatMapToPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Flux.just("hello") + .flatMap(s -> nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + @Test void monoFromPublisher() throws InterruptedException, ExecutionException { ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -480,6 +1684,77 @@ void monoFromFuture() throws ExecutionException, InterruptedException { executorService.shutdownNow(); } + + @Test + void fluxMerge() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Flux.merge(Flux.empty(), nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + + @Test + void parallelFlux() throws ExecutionException, InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + ParallelFlux.from(nonReactorPublisher) + .doOnNext(i -> value.set(REF.get())) + .sequential() + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } } @Nested diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsTest.java index 3d7b2b4ab1..7df3888683 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsTest.java @@ -39,10 +39,9 @@ public void scanOperator(){ @Test public void scanSubscriber(){ CoreSubscriber actual = new LambdaSubscriber<>(null, e -> {}, null, null); - FluxContextWriteRestoringThreadLocals - .ContextWriteRestoringThreadLocalsSubscriber test = - new FluxContextWriteRestoringThreadLocals - .ContextWriteRestoringThreadLocalsSubscriber<>( + FluxContextWriteRestoringThreadLocals.ContextWriteRestoringThreadLocalsSubscriber + test = + new FluxContextWriteRestoringThreadLocals.ContextWriteRestoringThreadLocalsSubscriber<>( actual, Context.empty() ); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingFlux.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingFlux.java new file mode 100644 index 0000000000..90bb0ad95a --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingFlux.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +public class ThreadSwitchingFlux extends Flux implements Subscription, Runnable { + + private final ExecutorService executorService; + private final T item; + private CoreSubscriber actual; + AtomicBoolean done = new AtomicBoolean(); + + public ThreadSwitchingFlux(T item, ExecutorService executorService) { + this.item = item; + this.executorService = executorService; + } + + @Override + public void subscribe(CoreSubscriber actual) { + this.actual = actual; + this.executorService.submit(this); + } + + @Override + public void run() { + this.actual.onSubscribe(this); + } + + private void deliver() { + if (done.compareAndSet(false, true)) { + this.actual.onNext(this.item); + this.executorService.submit(this.actual::onComplete); + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + if (!done.get()) { + this.executorService.submit(this::deliver); + } + } + } + + @Override + public void cancel() { + done.set(true); + } +} \ No newline at end of file diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingMono.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingMono.java new file mode 100644 index 0000000000..9b52dcea52 --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingMono.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +public class ThreadSwitchingMono extends Mono implements Subscription, Runnable { + + private final ExecutorService executorService; + private final T item; + private CoreSubscriber actual; + AtomicBoolean done = new AtomicBoolean(); + + public ThreadSwitchingMono(T item, ExecutorService executorService) { + this.item = item; + this.executorService = executorService; + } + + @Override + public void subscribe(CoreSubscriber actual) { + this.actual = actual; + this.executorService.submit(this); + } + + @Override + public void run() { + this.actual.onSubscribe(this); + } + + private void deliver() { + if (done.compareAndSet(false, true)) { + this.actual.onNext(this.item); + this.executorService.submit(this.actual::onComplete); + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + if (!done.get()) { + this.executorService.submit(this::deliver); + } + } + } + + @Override + public void cancel() { + done.set(true); + } +} \ No newline at end of file diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingParallelFlux.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingParallelFlux.java new file mode 100644 index 0000000000..d7557d1a8a --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingParallelFlux.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +public class ThreadSwitchingParallelFlux extends ParallelFlux implements + Subscription, Runnable { + + private final T item; + private final ExecutorService executorService; + AtomicBoolean done = new AtomicBoolean(); + CoreSubscriber[] actual; + + public ThreadSwitchingParallelFlux(T item, ExecutorService executorService) { + this.item = item; + this.executorService = executorService; + } + + @Override + public int parallelism() { + return 1; + } + + @Override + public void subscribe(CoreSubscriber[] subscribers) { + if (!validate(subscribers)) { + return; + } + + this.actual = subscribers; + executorService.submit(this); + } + + @Override + public void run() { + actual[0].onSubscribe(this); + } + + private void deliver() { + if (done.compareAndSet(false, true)) { + this.actual[0].onNext(this.item); + this.executorService.submit(this.actual[0]::onComplete); + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + if (!done.get()) { + this.executorService.submit(this::deliver); + } + } + } + + @Override + public void cancel() { + done.set(true); + } +} From 953f222b8d7bc594e149c1b1045fada119bbc7b3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 3 Oct 2023 17:42:09 +0300 Subject: [PATCH 185/312] fixes nightly build Signed-off-by: Oleh Dokuka --- .github/workflows/nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 843adf8f11..60e7bc2070 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -145,4 +145,4 @@ jobs: - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 name: other tests with: - arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file + arguments: check -x :reactor-core:test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file From 681c0d92d6828c2d35ab0150c0de6ecd9af68d28 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 3 Oct 2023 17:43:49 +0300 Subject: [PATCH 186/312] fixes nightly build Signed-off-by: Oleh Dokuka --- .github/workflows/nightly.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 60e7bc2070..7d3c118d5f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -143,6 +143,12 @@ jobs: distribution: 'temurin' java-version: 8 - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 + if: ${{ matrix.branch == 'main' }} + name: other tests + with: + arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default + - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 + if: ${{ matrix.branch == '3.5.x' }} name: other tests with: - arguments: check -x :reactor-core:test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file + arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file From e671573446db6caa51f2ec2b7adfab8982cb39ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 5 Oct 2023 10:54:58 +0200 Subject: [PATCH 187/312] Handling 1.0.0 of context-propagation (#3609) context-propagation 1.0.0 fails due to no method with signature ThreadLocalAccessor#restore(Object) is present prior to 1.0.1. This change calls setValue(Object) instead to not break at runtime. For versions older than 1.0.3 we also produce a warning because the API has not supported scoped thread local states properly. --- .../core/publisher/ContextPropagation.java | 43 ++++++++++++++++++- .../publisher/ContextPropagationSupport.java | 16 +++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index f331b6a5c2..fb66fd76d3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -82,7 +82,10 @@ static ContextSnapshot.Scope setThreadLocals(Object context) { Object value = ((ContextAccessor) contextAccessor).readValue((C) context, key); previousValues = setThreadLocal(key, value, threadLocalAccessor, previousValues); } - return ReactorScopeImpl.from(previousValues, registry); + if (ContextPropagationSupport.isContextPropagation101Available()) { + return ReactorScopeImpl.from(previousValues, registry); + } + return ReactorScopeImpl100.from(previousValues, registry); } } @@ -491,4 +494,42 @@ public static ContextSnapshot.Scope from(@Nullable Map previousV }); } } + + private static class ReactorScopeImpl100 implements ContextSnapshot.Scope { + + private final Map previousValues; + + private final ContextRegistry contextRegistry; + + private ReactorScopeImpl100(Map previousValues, + ContextRegistry contextRegistry) { + this.previousValues = previousValues; + this.contextRegistry = contextRegistry; + } + + @Override + public void close() { + for (ThreadLocalAccessor accessor : this.contextRegistry.getThreadLocalAccessors()) { + if (this.previousValues.containsKey(accessor.key())) { + Object previousValue = this.previousValues.get(accessor.key()); + resetThreadLocalValue(accessor, previousValue); + } + } + } + + @SuppressWarnings("unchecked") + private void resetThreadLocalValue(ThreadLocalAccessor accessor, @Nullable V previousValue) { + if (previousValue != null) { + ((ThreadLocalAccessor) accessor).setValue(previousValue); + } + else { + accessor.reset(); + } + } + + public static ContextSnapshot.Scope from(@Nullable Map previousValues, ContextRegistry registry) { + return (previousValues != null ? new ReactorScopeImpl100(previousValues, registry) : () -> { + }); + } + } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java index c705899a40..9c51d5a4bd 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagationSupport.java @@ -28,24 +28,36 @@ final class ContextPropagationSupport { // Ultimately the long term solution should be provided by Reactor Core. static final boolean isContextPropagationOnClasspath; static final boolean isContextPropagation103OnClasspath; + static final boolean isContextPropagation101OnClasspath; static boolean propagateContextToThreadLocals = false; static { boolean contextPropagation = false; boolean contextPropagation103 = false; + boolean contextPropagation101 = false; try { Class.forName("io.micrometer.context.ContextRegistry"); contextPropagation = true; + Class.forName("io.micrometer.context.ThreadLocalAccessor").getDeclaredMethod("restore", Object.class); + contextPropagation101 = true; Class.forName("io.micrometer.context.ContextSnapshotFactory"); contextPropagation103 = true; } catch (ClassNotFoundException notFound) { + } catch (NoSuchMethodException notFound) { } catch (LinkageError linkageErr) { } catch (Throwable err) { LOGGER.error("Unexpected exception while detecting ContextPropagation feature." + " The feature is considered disabled due to this:", err); } isContextPropagationOnClasspath = contextPropagation; + isContextPropagation101OnClasspath = contextPropagation101; isContextPropagation103OnClasspath = contextPropagation103; + + if (isContextPropagationOnClasspath && !isContextPropagation103OnClasspath) { + LOGGER.warn("context-propagation version below 1.0.3 can cause memory leaks" + + " when working with scope-based ThreadLocalAccessors, please " + + "upgrade!"); + } } /** @@ -57,6 +69,10 @@ static boolean isContextPropagationAvailable() { return isContextPropagationOnClasspath; } + static boolean isContextPropagation101Available() { + return isContextPropagation101OnClasspath; + } + static boolean isContextPropagation103Available() { return isContextPropagation103OnClasspath; } From 43b706134ee72804624a1d3364fa60c8cef378b4 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:06:44 +0300 Subject: [PATCH 188/312] ensures `addCap` always returns value with flag (#3610) Signed-off-by: Oleh Dokuka --- .../reactor/core/publisher/FluxCreate.java | 2 +- .../core/publisher/FluxCreateTest.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java b/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java index fcd01bc9d0..ccc3bed950 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java @@ -654,7 +654,7 @@ static long addCap(BaseSink instance, long toAdd) { s = instance.requested; r = s & Long.MAX_VALUE; if (r == Long.MAX_VALUE) { - return Long.MAX_VALUE; + return s; } u = Operators.addCap(r, toAdd); if (REQUESTED.compareAndSet(instance, s, u | (s & Long.MIN_VALUE))) { diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java index d2fdb92f3f..ac51cd763b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java @@ -16,6 +16,7 @@ package reactor.core.publisher; +import java.time.Duration; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -57,6 +58,26 @@ class FluxCreateTest { + @Test + //https://github.com/reactor/reactor-core/issues/3569 + void ensuresRequestMaxPlusOneDoesNotFailOnNoRequestConsumer() { + Flux.create(sink -> { + sink.next("1"); + sink.next("2"); + sink.next("3"); + sink.complete(); + }) + .as(StepVerifier::create) + .expectNext("1") + .thenRequest(1) + .expectNext("2") + .thenRequest(1) + .expectNext("3") + .thenRequest(1) + .expectComplete() + .verify(Duration.ofMillis(1000)); + } + @Test //https://github.com/reactor/reactor-core/issues/1949 void ensuresConcurrentRequestAndSettingOnRequestAlwaysDeliversDemand() throws ExecutionException, InterruptedException { From 9152893cf0f308c1270f0bdca2047f9715ed31ba Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:06:44 +0300 Subject: [PATCH 189/312] ensures `addCap` always returns value with flag (#3610) Signed-off-by: Oleh Dokuka (cherry picked from commit 43b706134ee72804624a1d3364fa60c8cef378b4) --- .../reactor/core/publisher/FluxCreate.java | 2 +- .../core/publisher/FluxCreateTest.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java b/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java index 27564cfc37..d859259ef8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxCreate.java @@ -652,7 +652,7 @@ static long addCap(BaseSink instance, long toAdd) { s = instance.requested; r = s & Long.MAX_VALUE; if (r == Long.MAX_VALUE) { - return Long.MAX_VALUE; + return s; } u = Operators.addCap(r, toAdd); if (REQUESTED.compareAndSet(instance, s, u | (s & Long.MIN_VALUE))) { diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java index d2fdb92f3f..ac51cd763b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxCreateTest.java @@ -16,6 +16,7 @@ package reactor.core.publisher; +import java.time.Duration; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -57,6 +58,26 @@ class FluxCreateTest { + @Test + //https://github.com/reactor/reactor-core/issues/3569 + void ensuresRequestMaxPlusOneDoesNotFailOnNoRequestConsumer() { + Flux.create(sink -> { + sink.next("1"); + sink.next("2"); + sink.next("3"); + sink.complete(); + }) + .as(StepVerifier::create) + .expectNext("1") + .thenRequest(1) + .expectNext("2") + .thenRequest(1) + .expectNext("3") + .thenRequest(1) + .expectComplete() + .verify(Duration.ofMillis(1000)); + } + @Test //https://github.com/reactor/reactor-core/issues/1949 void ensuresConcurrentRequestAndSettingOnRequestAlwaysDeliversDemand() throws ExecutionException, InterruptedException { From a9d922d370b7e6e6149bc9c2d0a1034d176d0053 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+olegdokuka@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:02:47 +0300 Subject: [PATCH 190/312] backports ci build speed improvements (#3597) Signed-off-by: Oleh Dokuka Signed-off-by: OlegDokuka --- .github/workflows/ci.yml | 2 +- .github/workflows/nightly.yml | 2 +- reactor-core/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 925a011a10..ec5c759633 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,4 +80,4 @@ jobs: - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 name: other tests with: - arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon + arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -Pjcstress.mode=sanity diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3fe01dc4b5..66a15dbfdd 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -61,4 +61,4 @@ jobs: - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 name: other tests with: - arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true \ No newline at end of file + arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 83e7fa062a..6d3f85d02f 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -272,7 +272,7 @@ blockHoundTest { } jcstress { - mode = 'quick' //quick, default, tough + mode = project.hasProperty('jcstress.mode') ? project.getProperty('jcstress.mode') : 'quick'//quick, default, tough jcstressDependency 'org.openjdk.jcstress:jcstress-core:0.16' heapPerFork = 512 } From a887af84072827eec23628d53ecf5a4a97db014b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:06:11 +0300 Subject: [PATCH 191/312] provides extra check for contextualName presence (#3611) * provides extra check for contextualName presence this check allows keeping user provided configuration Signed-off-by: Oleh Dokuka * fixes Signed-off-by: Oleh Dokuka --------- Signed-off-by: Oleh Dokuka --- .../MicrometerObservationListener.java | 15 ++++-- .../MicrometerObservationListenerTest.java | 51 +++++++++++++++++-- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java index 29f05e7935..f8d1e5c791 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/MicrometerObservationListener.java @@ -97,21 +97,26 @@ final class MicrometerObservationListener implements SignalListener { //while doOnSubscription matches the moment where the Publisher acknowledges said subscription //NOTE: we don't use the `DocumentedObservation` features to create the Observation, even for the ANONYMOUS case, //because the discovered tags could be more than the documented defaults - tapObservation = defaultObservation(configuration, observationSupplier) - .contextualName(configuration.sequenceName) + tapObservation = supplyOrCreateObservation(configuration, observationSupplier) .lowCardinalityKeyValues(configuration.commonKeyValues); } - private static Observation defaultObservation( + private static Observation supplyOrCreateObservation( MicrometerObservationListenerConfiguration configuration, @Nullable Function observationSupplier) { if (observationSupplier != null) { final Observation observation = observationSupplier.apply(configuration.registry); if (observation != null) { - return observation; + if (observation.getContext().getContextualName() != null) { + return observation; + } + else { + return observation.contextualName(configuration.sequenceName); + } } } - return Observation.createNotStarted(configuration.sequenceName, configuration.registry); + return Observation.createNotStarted(configuration.sequenceName, configuration.registry) + .contextualName(configuration.sequenceName); } @Override diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java index a0cf2ed107..148be1d3ba 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/MicrometerObservationListenerTest.java @@ -68,12 +68,53 @@ public long monotonicTime() { }; configuration = new MicrometerObservationListenerConfiguration( "testName", - KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + KeyValues.of("testTag1", "testTagValue1", "testTag2", "testTagValue2"), registry, false); subscriberContext = Context.of("contextKey", "contextValue"); } + @ParameterizedTestWithName + @ValueSource(booleans = {true, false}) + void whenStartedFluxWithDefaultNameWithCustomObservationSupplier(boolean automatic) { + if (automatic) { + Hooks.enableAutomaticContextPropagation(); + } + configuration = new MicrometerObservationListenerConfiguration( + MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), + //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) + KeyValues.of("testTag1", "testTagValue1", "testTag2", "testTagValue2"), + registry, + false); + + MicrometerObservationListener listener = + new MicrometerObservationListener<>(subscriberContext, configuration, + or -> Observation.createNotStarted("user.name", or) + .lowCardinalityKeyValue("userType", "admin") + .highCardinalityKeyValue("userId", "testId") + .contextualName("getting-user-name")); + + assertThat(listener.valued).as("valued").isFalse(); + assertThat(listener.tapObservation) + .as("subscribeToTerminalObservation field") + .isNotNull(); + assertThat(registry).as("before start").doesNotHaveAnyObservation(); + + listener.doFirst(); // forces observation start + + assertThat(registry) + .hasSingleObservationThat() + .hasNameEqualTo("user.name") + .hasContextualNameEqualTo("getting-user-name") + .as("subscribeToTerminalObservation") + .hasBeenStarted() + .isNotStopped() + .hasLowCardinalityKeyValue("testTag1", "testTagValue1") + .hasLowCardinalityKeyValue("testTag2", "testTagValue2") + .hasLowCardinalityKeyValue("userType", "admin") + .hasHighCardinalityKeyValue("userId", "testId"); + } + @ParameterizedTestWithName @ValueSource(booleans = {true, false}) void whenStartedFluxWithDefaultName(boolean automatic) { @@ -83,7 +124,7 @@ void whenStartedFluxWithDefaultName(boolean automatic) { configuration = new MicrometerObservationListenerConfiguration( MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) - KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + KeyValues.of("testTag1", "testTagValue1", "testTag2", "testTagValue2"), registry, false); @@ -116,7 +157,7 @@ void whenStartedFluxWithCustomName(boolean automatic) { configuration = new MicrometerObservationListenerConfiguration( "testName", //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) - KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + KeyValues.of("testTag1", "testTagValue1", "testTag2", "testTagValue2"), registry, false); @@ -150,7 +191,7 @@ void whenStartedMono(boolean automatic) { configuration = new MicrometerObservationListenerConfiguration( MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromMono (which is tested separately) - KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + KeyValues.of("testTag1", "testTagValue1", "testTag2", "testTagValue2"), registry, true); @@ -644,7 +685,7 @@ void observationWithEmptyContextHasNoParent(boolean automatic) { configuration = new MicrometerObservationListenerConfiguration( MicrometerObservationListenerDocumentation.ANONYMOUS.getName(), //note: "type" key is added by MicrometerObservationListenerConfiguration#fromFlux (which is tested separately) - KeyValues.of("testTag1", "testTagValue1","testTag2", "testTagValue2"), + KeyValues.of("testTag1", "testTagValue1", "testTag2", "testTagValue2"), registry, false); From 759375e40e4146398444ced83bd25909f1e9d6f6 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:47:03 +0300 Subject: [PATCH 192/312] ensures that proper `index` is used during `onNext` check (#3614) * ensures that proper index is used during onNext check Signed-off-by: Oleh Dokuka * adds proper index zeroing Signed-off-by: Oleh Dokuka --------- Signed-off-by: Oleh Dokuka --- .../core/publisher/FluxBufferTimeout.java | 7 +- .../core/publisher/FluxBufferTimeoutTest.java | 67 +++++++++++++------ 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java index 9b2508255a..dcc6f7228f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java @@ -694,9 +694,11 @@ public Object scanUnsafe(Attr key) { @Override public void onNext(final T value) { int index; + boolean flush; for(;;){ index = this.index + 1; - if(INDEX.compareAndSet(this, index - 1, index)){ + flush = index % batchSize == 0; + if(INDEX.compareAndSet(this, index - 1, flush ? 0 : index)){ break; } } @@ -715,8 +717,7 @@ public void onNext(final T value) { nextCallback(value); - if (this.index % batchSize == 0) { - this.index = 0; + if (flush) { if (timespanRegistration != null) { timespanRegistration.dispose(); timespanRegistration = null; diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutTest.java index 9d703b4daa..e750a5df96 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutTest.java @@ -24,11 +24,13 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; @@ -310,31 +312,54 @@ public void scanSubscriberCancelled() { @Test public void flushShouldNotRaceWithNext() { - Set seen = new HashSet<>(); - Consumer> consumer = integers -> { - for (Integer i : integers) { - if (!seen.add(i)) { - throw new IllegalStateException("Duplicate! " + i); + for (int i = 0; i < 100; i++) { + AtomicInteger caller = new AtomicInteger(); + AtomicBoolean stop = new AtomicBoolean(); + Set seen = new HashSet<>(); + Consumer> consumer = integers -> { + RuntimeException ex = new RuntimeException(integers.toString()); + if (caller.getAndIncrement() == 0) { + for (Integer value : integers) { + if (!seen.add(value)) { + throw new IllegalStateException("Duplicate! " + value); + } + } + + if (caller.decrementAndGet() != 0) { + stop.set(true); + throw ex; + } } + else { + stop.set(true); + throw ex; + } + }; + CoreSubscriber> actual = + new LambdaSubscriber<>(consumer, null, null, null); + + FluxBufferTimeout.BufferTimeoutSubscriber> test = + new FluxBufferTimeout.BufferTimeoutSubscriber>( + actual, + 3, + 1000, + TimeUnit.MILLISECONDS, + Schedulers.boundedElastic() + .createWorker(), + ArrayList::new); + test.onSubscribe(Operators.emptySubscription()); + + AtomicInteger counter = new AtomicInteger(); + for (int j = 0; j < 500; j++) { + RaceTestUtils.race(() -> test.onNext(counter.getAndIncrement()), test.flushTask); + Assertions.assertThat(stop).isFalse(); } - }; - CoreSubscriber> actual = new LambdaSubscriber<>(consumer, null, null, null); - - FluxBufferTimeout.BufferTimeoutSubscriber> test = new FluxBufferTimeout.BufferTimeoutSubscriber>( - actual, 3, 1000, TimeUnit.MILLISECONDS, Schedulers.boundedElastic().createWorker(), ArrayList::new); - test.onSubscribe(Operators.emptySubscription()); - - AtomicInteger counter = new AtomicInteger(); - for (int i = 0; i < 500; i++) { - RaceTestUtils.race( - () -> test.onNext(counter.getAndIncrement()), - () -> test.flushCallback(null) - ); - } - test.onComplete(); + test.onComplete(); - assertThat(seen.size()).isEqualTo(500); + assertThat(seen.size()).as(() -> seen.size() + " " + seen.toString()) + .isEqualTo(500); + } } //see https://github.com/reactor/reactor-core/issues/1247 From c6b7f8778bd398ba38f019f6e43977cafb0a68e5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 10 Oct 2023 12:51:47 +0300 Subject: [PATCH 193/312] [release] Prepare and release 3.5.11 Signed-off-by: Oleh Dokuka --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- settings.gradle | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 533ac11810..a27f4e22de 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.10" - testCompile "io.projectreactor:reactor-test:3.5.10" + compile "io.projectreactor:reactor-core:3.5.11" + testCompile "io.projectreactor:reactor-test:3.5.11" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.11-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.11-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.12-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.12-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.10" + // implementation "io.projectreactor:reactor-tools:3.5.11" } ``` diff --git a/gradle.properties b/gradle.properties index f80d25a9de..f9a378b262 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.11-SNAPSHOT -bomVersion=2022.0.11 -metricsMicrometerVersion=1.0.11-SNAPSHOT +version=3.5.11 +bomVersion=2022.0.12 +metricsMicrometerVersion=1.0.11 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index de455fcd2c..7ce8cdbe83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,10 +14,10 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.11" +micrometer = "1.10.12" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.0.10" -contextPropagation="1.0.5" +micrometerTracingTest="1.0.11" +contextPropagation="1.0.6" kotlin = "1.5.32" reactiveStreams = "1.0.4" diff --git a/settings.gradle b/settings.gradle index 9d6b531ee1..c078c6d8ad 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,10 +28,10 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.10.12-SNAPSHOT') + version('micrometer', '1.10.13-SNAPSHOT') version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") - version('micrometerTracingTest', "1.0.11-SNAPSHOT") - version('contextPropagation', "1.0.6-SNAPSHOT") + version('micrometerTracingTest', "1.0.12-SNAPSHOT") + version('contextPropagation', "1.0.7-SNAPSHOT") } } } From 7dff05d5629dc29f0e6dd6d2aaafe65bcc99f9ae Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 10 Oct 2023 13:42:31 +0300 Subject: [PATCH 194/312] [release] Next development version 3.5.12-SNAPSHOT Signed-off-by: Oleh Dokuka --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index f9a378b262..9865d37b2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.11 +version=3.5.12-SNAPSHOT bomVersion=2022.0.12 -metricsMicrometerVersion=1.0.11 +metricsMicrometerVersion=1.0.12-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ce8cdbe83..0343893154 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.10" -baselinePerfCore = "3.5.10" +baseline-core-api = "3.5.11" +baselinePerfCore = "3.5.11" baselinePerfExtra = "3.5.1" # Other shared versions From 420203cf0af233eb69efce0b9c93f2e32c9256f8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 10 Oct 2023 16:51:12 +0300 Subject: [PATCH 195/312] [release] Prepare and release 3.6.0-RC1 Signed-off-by: Oleh Dokuka --- README.md | 6 +++--- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7a13235eef..b4b24c4352 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0-M3" - testCompile "io.projectreactor:reactor-test:3.6.0-M3" + compile "io.projectreactor:reactor-core:3.6.0-RC1" + testCompile "io.projectreactor:reactor-test:3.6.0-RC1" // Alternatively, use the following for latest snapshot artifacts in this line // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0-M3" + // implementation "io.projectreactor:reactor-tools:3.6.0-RC1" } ``` diff --git a/gradle.properties b/gradle.properties index 643808d631..f6dbd1a578 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-SNAPSHOT -bomVersion=2023.0.0-M3 -metricsMicrometerVersion=1.1.0-SNAPSHOT +version=3.6.0-RC1 +bomVersion=2023.0.0-RC1 +metricsMicrometerVersion=1.1.0-RC1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8cedfe1716..d675dc73a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,10 +14,10 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-M3" +micrometer = "1.12.0-RC1" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.0-M2" -contextPropagation="1.0.6" +micrometerTracingTest="1.2.0-RC1" +contextPropagation="1.1.0-RC1" kotlin = "1.5.32" reactiveStreams = "1.0.4" From 6b3fcc521242eb1a2d95095cd4c314703bbee152 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 10 Oct 2023 22:12:41 +0300 Subject: [PATCH 196/312] [release] Next development version 3.6.0-SNAPSHOT Signed-off-by: Oleh Dokuka --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index f6dbd1a578..47f6da2b71 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-RC1 +version=3.6.0-SNAPSHOT bomVersion=2023.0.0-RC1 -metricsMicrometerVersion=1.1.0-RC1 +metricsMicrometerVersion=1.1.0-SNAPSHOT From 9da514c6065a42a0201269b532eb2aef46052c5e Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:28:29 +0300 Subject: [PATCH 197/312] ensures SchedulerTask uses isShutdown instead of isDisposed #3623 (#3623) Signed-off-by: Oleh Dokuka --- .../ThreadPerTaskBoundedElasticScheduler.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java b/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java index 209a2ac679..a8d0b777e7 100644 --- a/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java +++ b/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java @@ -826,6 +826,10 @@ public boolean isDisposed() { return isShutdown(this.wipAndRefCnt) && numberOfEnqueuedTasks() == 0; } + boolean isShutdown() { + return isShutdown(this.wipAndRefCnt); + } + @Override public Object scanUnsafe(Attr key) { if (Attr.TERMINATED == key) return isDisposed(); @@ -1037,7 +1041,7 @@ public void run() { previousState = isInstant ? markInitial(this) : markRescheduled(this); boolean isDisposed = isDisposed(previousState); - boolean isShutdown = holder.isDisposed(); + boolean isShutdown = holder.isShutdown(); if (isInstant) { if (!isDisposed && !isShutdown) { @@ -1305,8 +1309,11 @@ static void markCompleted(SchedulerTask disposable) { @Override public String toString() { - return "SchedulerTask(" + hashCode() +"){" + "carrier=" + carrier + ", " + - "scheduledFuture=" + scheduledFuture + "state= " + Integer.toBinaryString(get()) + '}'; + return (isPeriodic() ? this.fixedRatePeriod == 0 ? "InstantPeriodic" : + "Periodic" : + "") + + "SchedulerTask(" + hashCode() +"){" + "carrier=" + carrier + ", " + + "scheduledFuture=" + scheduledFuture + ", state= " + Integer.toBinaryString(get()) + '}'; } } From d1557e049f14e4c1b4c50cc04e1ad7062f3aae61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 31 Oct 2023 13:39:52 +0100 Subject: [PATCH 198/312] Revised documentation about context propagation (#3617) --- .../asciidoc/advanced-contextPropagation.adoc | 141 +++++++++++++++--- docs/asciidoc/advancedFeatures.adoc | 2 +- docs/asciidoc/coreFeatures.adoc | 2 +- 3 files changed, 120 insertions(+), 25 deletions(-) diff --git a/docs/asciidoc/advanced-contextPropagation.adoc b/docs/asciidoc/advanced-contextPropagation.adoc index 3a2fc76ebb..acc5639bad 100644 --- a/docs/asciidoc/advanced-contextPropagation.adoc +++ b/docs/asciidoc/advanced-contextPropagation.adoc @@ -5,52 +5,110 @@ Since 3.5.0, Reactor-Core embeds support for the `io.micrometer:context-propagat This library is intended as a means to easily adapt between various implementations of the concept of a Context, of which `ContextView`/`Context` is an example, and between `ThreadLocal` variables as well. -`ReactorContextAccessor` allows the Context-Propagation library to understand Reactor `Context` and `Contextview`. +`ReactorContextAccessor` allows the Context-Propagation library to understand Reactor +`Context` and `ContextView`. It implements the SPI and is loaded via `java.util.ServiceLoader`. No user action is required, other than having a dependency on both reactor-core and `io.micrometer:context-propagation`. The `ReactorContextAccessor` class is public but shouldn't generally be accessed by user code. -On top of that, Reactor-Core 3.5.0 also modifies the behavior of a couple key operators as well as introduces the `contextCapture` operator -to transparently deal with `ContextSnapshot`s if the library is available at runtime. +Reactor-Core supports two modes of operation with `io.micrometer:context-propagation`: -== `contextCapture` Operator +- the **default** (limited) mode, +- and the **automatic** mode, enabled via `Hooks.enableAutomaticContextPropagation()`. + Please note that this mode applies only to new subscriptions, so it is recommended to +enable this hook when the application starts. + +Their key differences are discussed in the context of either <> to Reactor `Context`, or <> that +reflects the contents of the `Context` of currently attached `Subscriber` for reading. + +[[context-writing]] +== Writing to `Context` + +Depending on the individual application, you might either have to store already populated +`ThreadLocal` state as entries in the `Context`, or might only need to directly populate +the `Context`. + +=== `contextWrite` Operator + +When the values meant to be accessed as `ThreadLocal` are not (or do not need to be) +present at the time of subscription, they can immediately be stored in the `Context`: + +==== +[source,java] +---- +// assuming TL is known to Context-Propagation as key TLKEY. +static final ThreadLocal TL = new ThreadLocal<>(); + +// in the main Thread, TL is not set + +Mono.deferContextual(ctx -> + Mono.delay(Duration.ofSeconds(1)) + // we're now in another thread, TL is not explicitly set + .map(v -> "delayed ctx[" + TLKEY + "]=" + ctx.getOrDefault(TLKEY, "not found") + ", TL=" + TL.get())) +.contextWrite(ctx -> ctx.put(TLKEY, "HELLO")) +.block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" in default mode + // returns "delayed ctx[TLKEY]=HELLO, TL=HELLO" in automatic mode +---- +==== + +=== `contextCapture` Operator This operator can be used when one needs to capture `ThreadLocal` value(s) at subscription time and reflect these values in the Reactor `Context` for the benefit of upstream operators. -It relies on the `context-propagation` library and notably the registered `ThreadLocalAccessor`(s) to discover relevant ThreadLocal values. -This is a convenient alternative to `contextWrite` which uses the `context-propagation` API to obtain a `ContextSnapshot` and then uses that snapshot to populate the Reactor `Context`. +In contrast to the manual `contextWrite` operator, `contextCapture` uses the +`context-propagation` API to obtain a `ContextSnapshot` and then uses that snapshot +to populate the Reactor `Context`. -As a result, if there were any ThreadLocal values during subscription phase, for which there is a registered `ThreadLocalAccessor`, their values would now be stored in the Reactor `Context` and visible +As a result, if there were any `ThreadLocal` values during subscription phase, for which there is a registered `ThreadLocalAccessor`, their values would now be stored in the Reactor `Context` and visible at runtime in upstream operators. ==== [source,java] ---- -//assuming TL is known to Context-Propagation as key TLKEY. +// assuming TL is known to Context-Propagation as key TLKEY. static final ThreadLocal TL = new ThreadLocal<>(); -//in the main thread, TL is set to "HELLO" +// in the main Thread, TL is set to "HELLO" TL.set("HELLO"); Mono.deferContextual(ctx -> Mono.delay(Duration.ofSeconds(1)) - //we're now in another thread, TL is not set - .map(v -> "delayed ctx[" + TLKEY + "]=" + ctx.getOrDefault(TLKEY, "not found") + ", TL=" + TL.get()) -) -.contextCapture() -.block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" + // we're now in another thread, TL is not explicitly set + .map(v -> "delayed ctx[" + TLKEY + "]=" + ctx.getOrDefault(TLKEY, "not found") + ", TL=" + TL.get())) +.contextCapture() // can be skipped in automatic mode when a blocking operator follows +.block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" in default mode + // returns "delayed ctx[TLKEY]=HELLO, TL=HELLO" in automatic mode ---- ==== -== Operators that transparently restore a snapshot: `handle` and `tap` -Both `Flux` and `Mono` variants of `handle` and `tap` will have their behavior slightly modified -if the Context-Propagation library is available at runtime. +NOTE: In the **automatic** mode, blocking operators, such as `Flux#blockFirst()`, +`Flux#blockLast()`, `Flux#toIterable()`, `Mono#block()`, `Mono#blockOptional()`, and +relevant overloads, all perform `contextCapture()` transparently, so in most cases it is +not necessary to add it. + +[[context-accessing]] +== Accessing `ThreadLocal` state + +Starting from Reactor-Core 3.5.0, `ThreadLocal` state is restored in a limited set +of operators. We call this behaviour the **default** (limited) mode. In 3.5.3, a new +mode was added, the **automatic** mode, which provides access to `ThreadLocal` values +throughout the reactive chain. -Namely, if their downstream `ContextView` is not empty they will assume a context capture has occurred -(either manually or via the `contextCapture()` operator) and will attempt to restore `ThreadLocal`s from -that snapshot transparently. +Reactor-Core performs `ThreadLocal` state restoration using the values +stored in `Context` and `ThreadLocalAccessor` instances registered in `ContextRegistry` +that match by key. + +=== Default mode operators for snapshot restoration: `handle` and `tap` + +In the **default** mode, both `Flux` and `Mono` variants of `handle` and `tap` will have +their behavior slightly modified if the Context-Propagation library is available at runtime. + +Namely, if their downstream `ContextView` is not empty they will assume a context +capture has occurred (either manually or via the `contextCapture()` operator) and will attempt to restore ThreadLocals from that snapshot transparently. Any ThreadLocals for keys that are missing in the `ContextView` are left untouched. These operators will ensure restoration is performed around the user-provided code, respectively: - - `handle` will wrap the `BiConsumer` in one which restores `ThreadLocal`s - - `tap` variants will wrap the `SignalListener` into one that has the same kind of wrapping around each method (this includes the `addToContext` method) + +- `handle` will wrap the `BiConsumer` in one which restores `ThreadLocal`s +- `tap` variants will wrap the `SignalListener` into one that has the same kind of wrapping around each method (this includes the `addToContext` method) The intent is to have a minimalistic set of operators transparently perform restoration. As a result we chose operators with rather general and broad applications (one with transformative capabilities, one with side-effect capabilities) @@ -72,4 +130,41 @@ Mono.delay(Duration.ofSeconds(1)) .contextCapture() .block(); // prints "null" and returns "handled delayed TL=HELLO" ---- -==== \ No newline at end of file +==== + +=== Automatic mode + +In the **automatic** mode, all operators restore `ThreadLocal` state across `Thread` +boundaries. In contrast, in the **default** mode only selected operators do so. + +`Hooks.enableAutomaticContextPropagation()` can be called upon application start to +enable the **automatic** mode. Please note that this mode applies only to new subscriptions, +so it is recommended to enable this hook when the application starts. + +It is not an easy task to achieve, as the Reactive Streams specification makes reactive +chains `Thread`-agnostic. However, Reactor-Core does its best to control sources of +`Thread` switches and perform snapshot restoration based on the Reactor `Context`, +which is treated as the source of truth for `ThreadLocal` state. + +WARNING: While the **default** mode limits the `ThreadLocal` state only to the user code +executed as arguments to the chosen operators, the **automatic** mode allows +`ThreadLocal` state to cross operator boundaries. This requires proper cleanup to avoid +leaking the state to unrelated code which reuses the same `Thread`. This requires to +treat absent keys in the `Context` for registered instances of `ThreadLocalAccessor` as +signals to clear the corresponding `ThreadLocal` state. This is especially important for +an empty `Context`, which clears all state for registered `ThreadLocalAccessor` instances. + +== Which mode should I choose? + +Both **default** and **automatic** modes have an impact on performance. Accessing +`ThreadLocal` variables can impact a reactive pipeline significantly. If the highest +scalability and performance is the goal, more verbose approaches for logging and +explicit argument passing can be considered instead of relying on `ThreadLocal` state. If +access to established libraries in the space of Observability, such as Micrometer and +SLF4J, which use `ThreadLocal` state for convenience to provide meaningful production +grade features is an understood compromise, the choice of the mode is yet another +compromise to make. The **automatic** mode, depending on the flow of your application and +the amount of operators used, can be either better or worse than the **default** mode. The +only recommendation that can be given is to measure how your application behaves and what +scalability and performance characteristics you obtain when presented with a load you +expect. \ No newline at end of file diff --git a/docs/asciidoc/advancedFeatures.adoc b/docs/asciidoc/advancedFeatures.adoc index 88743410ed..353ab1405f 100644 --- a/docs/asciidoc/advancedFeatures.adoc +++ b/docs/asciidoc/advancedFeatures.adoc @@ -1080,7 +1080,7 @@ public void contextForLibraryReactivePut() { ---- ==== -include::advanced-contextPropagation.adoc[leveloffset=1] +include::advanced-contextPropagation.adoc[leveloffset=2] [[cleanup]] == Dealing with Objects that Need Cleanup diff --git a/docs/asciidoc/coreFeatures.adoc b/docs/asciidoc/coreFeatures.adoc index 836e3c0fdb..2314256d19 100644 --- a/docs/asciidoc/coreFeatures.adoc +++ b/docs/asciidoc/coreFeatures.adoc @@ -317,7 +317,7 @@ the part of the chain after them. * Changes the `Thread` from which the *whole chain* of operators subscribes * Picks one thread from the `Scheduler` -- NOTE: Only the closest `subscribeOn` call in the downstream chain effectively +NOTE: Only the closest `subscribeOn` call in the downstream chain effectively schedules subscription and request signals to the source or operators that can intercept them (`doFirst`, `doOnRequest`). Using multiple `subscribeOn` calls will introduce unnecessary Thread switches that have no value. From 69ab66dc1e877872bae18ac53094efb932c15c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 31 Oct 2023 18:01:46 +0100 Subject: [PATCH 199/312] InternalOperator automatic context propagation (#3625) This change revisits InternalMonoOperator and InternalFluxOperator implementations and wraps their async sources with a context restoring Subscriber implementation. This is a follow-up for #3549 --- .../core/publisher/ConnectableLift.java | 2 +- .../publisher/ConnectableLiftFuseable.java | 2 +- .../core/publisher/FluxBufferBoundary.java | 4 +- .../core/publisher/FluxBufferWhen.java | 4 +- .../reactor/core/publisher/FluxConcatMap.java | 6 +- .../publisher/FluxConcatMapNoPrefetch.java | 4 +- .../core/publisher/FluxDelaySubscription.java | 4 +- .../reactor/core/publisher/FluxExpand.java | 6 +- .../core/publisher/FluxFilterWhen.java | 4 +- .../reactor/core/publisher/FluxGroupJoin.java | 8 +- .../java/reactor/core/publisher/FluxJoin.java | 8 +- .../java/reactor/core/publisher/FluxLift.java | 4 +- .../core/publisher/FluxLiftFuseable.java | 6 +- .../core/publisher/FluxMergeSequential.java | 2 +- .../core/publisher/FluxOnErrorResume.java | 6 +- .../core/publisher/FluxPublishMulticast.java | 7 +- .../core/publisher/FluxRepeatWhen.java | 2 +- .../reactor/core/publisher/FluxRetry.java | 7 +- .../reactor/core/publisher/FluxSample.java | 4 +- .../core/publisher/FluxSampleFirst.java | 4 +- .../core/publisher/FluxSampleTimeout.java | 4 +- .../core/publisher/FluxSkipUntilOther.java | 4 +- .../core/publisher/FluxSwitchIfEmpty.java | 4 +- .../reactor/core/publisher/FluxSwitchMap.java | 4 +- .../publisher/FluxSwitchMapNoPrefetch.java | 4 +- .../core/publisher/FluxTakeUntilOther.java | 4 +- .../reactor/core/publisher/FluxTimeout.java | 11 +- .../core/publisher/FluxWindowBoundary.java | 4 +- .../core/publisher/FluxWindowWhen.java | 6 +- .../core/publisher/FluxWithLatestFrom.java | 4 +- .../reactor/core/publisher/GroupedLift.java | 2 +- .../core/publisher/GroupedLiftFuseable.java | 2 +- .../publisher/MonoCacheInvalidateWhen.java | 5 +- .../core/publisher/MonoDelaySubscription.java | 4 +- .../core/publisher/MonoFilterWhen.java | 4 +- .../reactor/core/publisher/MonoFlatMap.java | 4 +- .../core/publisher/MonoFlatMapMany.java | 4 +- .../java/reactor/core/publisher/MonoLift.java | 4 +- .../core/publisher/MonoLiftFuseable.java | 5 +- .../core/publisher/MonoPublishMulticast.java | 6 +- .../core/publisher/MonoSwitchIfEmpty.java | 4 +- .../core/publisher/MonoTakeUntilOther.java | 4 +- .../reactor/core/publisher/MonoTimeout.java | 6 +- .../reactor/core/publisher/Operators.java | 2 +- .../reactor/core/publisher/ParallelLift.java | 14 +- .../core/publisher/ParallelLiftFuseable.java | 13 +- .../reactor/core/publisher/SignalLogger.java | 3 +- .../AutomaticContextPropagationTest.java | 806 ++++++++++++++---- .../core/publisher/ThreadSwitchingFlux.java | 15 +- .../core/publisher/ThreadSwitchingMono.java | 15 +- 50 files changed, 797 insertions(+), 268 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java index 35d7e86bc7..9523098638 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java @@ -69,7 +69,7 @@ public String stepName() { @Override public final CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { CoreSubscriber input = - liftFunction.lifter.apply(source, actual); + liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java index b73831c3b9..60b748292d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java @@ -71,7 +71,7 @@ public String stepName() { @Override public final CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { CoreSubscriber input = - liftFunction.lifter.apply(source, actual); + liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferBoundary.java b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferBoundary.java index 4f81dc94d2..c0c4a04e50 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferBoundary.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferBoundary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ final class FluxBufferBoundary> Publisher other, Supplier bufferSupplier) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); this.bufferSupplier = Objects.requireNonNull(bufferSupplier, "bufferSupplier"); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java index 8f92516842..b9379073a1 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java @@ -65,7 +65,7 @@ final class FluxBufferWhen> Supplier bufferSupplier, Supplier> queueSupplier) { super(source); - this.start = Objects.requireNonNull(start, "start"); + this.start = Operators.toFluxOrMono(Objects.requireNonNull(start, "start")); this.end = Objects.requireNonNull(end, "end"); this.bufferSupplier = Objects.requireNonNull(bufferSupplier, "bufferSupplier"); this.queueSupplier = Objects.requireNonNull(queueSupplier, "queueSupplier"); @@ -360,7 +360,7 @@ void open(OPEN token) { BufferWhenCloseSubscriber bc = new BufferWhenCloseSubscriber<>(this, idx); subscribers.add(bc); - p.subscribe(bc); + Operators.toFluxOrMono(p).subscribe(bc); } void openComplete(BufferWhenOpenSubscriber os) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMap.java index 9dde7207dd..b07d8e87e2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -448,7 +448,7 @@ void drain() { } else { active = true; - p.subscribe(inner); + Operators.toFluxOrMono(p).subscribe(inner); } } } @@ -805,7 +805,7 @@ void drain() { } else { active = true; - p.subscribe(inner); + Operators.toFluxOrMono(p).subscribe(inner); } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMapNoPrefetch.java b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMapNoPrefetch.java index 222f60fc23..59ba901353 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMapNoPrefetch.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMapNoPrefetch.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,7 +203,7 @@ public void onNext(T t) { return; } - p.subscribe(inner); + Operators.toFluxOrMono(p).subscribe(inner); } catch (Throwable e) { Context ctx = actual.currentContext(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxDelaySubscription.java b/reactor-core/src/main/java/reactor/core/publisher/FluxDelaySubscription.java index 080e58670d..8a6a2c1df7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxDelaySubscription.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxDelaySubscription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ final class FluxDelaySubscription extends InternalFluxOperator FluxDelaySubscription(Flux source, Publisher other) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxExpand.java b/reactor-core/src/main/java/reactor/core/publisher/FluxExpand.java index d12f95b370..fe9fea4723 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxExpand.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxExpand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,8 +119,8 @@ public void onNext(T t) { Publisher p; try { - p = Objects.requireNonNull(expander.apply(t), - "The expander returned a null Publisher"); + p = Operators.toFluxOrMono(Objects.requireNonNull(expander.apply(t), + "The expander returned a null Publisher")); } catch (Throwable ex) { Exceptions.throwIfFatal(ex); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFilterWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFilterWhen.java index a7b57c89b8..3d2bec6dac 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFilterWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFilterWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -279,7 +279,7 @@ void drain() { FilterWhenInner inner = new FilterWhenInner(this, !(p instanceof Mono)); if (CURRENT.compareAndSet(this,null, inner)) { state = STATE_RUNNING; - p.subscribe(inner); + Operators.toFluxOrMono(p).subscribe(inner); break; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxGroupJoin.java b/reactor-core/src/main/java/reactor/core/publisher/FluxGroupJoin.java index d1f4b30356..c88650b462 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxGroupJoin.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxGroupJoin.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ final class FluxGroupJoin Supplier> queueSupplier, Supplier> processorQueueSupplier) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); this.leftEnd = Objects.requireNonNull(leftEnd, "leftEnd"); this.rightEnd = Objects.requireNonNull(rightEnd, "rightEnd"); this.processorQueueSupplier = Objects.requireNonNull(processorQueueSupplier, "processorQueueSupplier"); @@ -336,7 +336,7 @@ void drain() { new LeftRightEndSubscriber(this, true, idx); cancellations.add(end); - p.subscribe(end); + Operators.toFluxOrMono(p).subscribe(end); ex = error; if (ex != null) { @@ -404,7 +404,7 @@ else if (mode == RIGHT_VALUE) { new LeftRightEndSubscriber(this, false, idx); cancellations.add(end); - p.subscribe(end); + Operators.toFluxOrMono(p).subscribe(end); ex = error; if (ex != null) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxJoin.java b/reactor-core/src/main/java/reactor/core/publisher/FluxJoin.java index d85e16bb1f..b01678a6fc 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxJoin.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxJoin.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,7 @@ final class FluxJoin extends Function> rightEnd, BiFunction resultSelector) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); this.leftEnd = Objects.requireNonNull(leftEnd, "leftEnd"); this.rightEnd = Objects.requireNonNull(rightEnd, "rightEnd"); this.resultSelector = Objects.requireNonNull(resultSelector, "resultSelector"); @@ -289,7 +289,7 @@ void drain() { new LeftRightEndSubscriber(this, true, idx); cancellations.add(end); - p.subscribe(end); + Operators.toFluxOrMono(p).subscribe(end); ex = error; if (ex != null) { @@ -366,7 +366,7 @@ else if (mode == RIGHT_VALUE) { new LeftRightEndSubscriber(this, false, idx); cancellations.add(end); - p.subscribe(end); + Operators.toFluxOrMono(p).subscribe(end); ex = error; if (ex != null) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxLift.java b/reactor-core/src/main/java/reactor/core/publisher/FluxLift.java index ce9da76ac2..f127974310 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ public Object scanUnsafe(Attr key) { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { CoreSubscriber input = - liftFunction.lifter.apply(source, actual); + liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxLiftFuseable.java index f5eff2cf85..a3aedc88eb 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ public Object scanUnsafe(Attr key) { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { CoreSubscriber input = - liftFunction.lifter.apply(source, actual); + liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); @@ -65,6 +65,6 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act input = new FluxHide.SuppressFuseableSubscriber<>(input); } //otherwise QS is not required or user already made a compatible conversion - return input; + return Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, input); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxMergeSequential.java b/reactor-core/src/main/java/reactor/core/publisher/FluxMergeSequential.java index 50aa3ce830..46c4899e97 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxMergeSequential.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxMergeSequential.java @@ -205,7 +205,7 @@ public void onNext(T t) { Publisher publisher; try { - publisher = Objects.requireNonNull(mapper.apply(t), "publisher"); + publisher = Operators.toFluxOrMono(Objects.requireNonNull(mapper.apply(t), "publisher")); } catch (Throwable ex) { onError(Operators.onOperatorError(s, ex, t, actual.currentContext())); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxOnErrorResume.java b/reactor-core/src/main/java/reactor/core/publisher/FluxOnErrorResume.java index 28a4e60764..a09c3aa08c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxOnErrorResume.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxOnErrorResume.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,8 +91,8 @@ public void onError(Throwable t) { Publisher p; try { - p = Objects.requireNonNull(nextFactory.apply(t), - "The nextFactory returned a null Publisher"); + p = Operators.toFluxOrMono(Objects.requireNonNull(nextFactory.apply(t), + "The nextFactory returned a null Publisher")); } catch (Throwable e) { Throwable _e = Operators.onOperatorError(e, actual.currentContext()); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxPublishMulticast.java b/reactor-core/src/main/java/reactor/core/publisher/FluxPublishMulticast.java index 6bd338f16a..21ce7db642 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxPublishMulticast.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxPublishMulticast.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,8 +78,9 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act queueSupplier, actual.currentContext()); - Publisher out = Objects.requireNonNull(transform.apply(multicast), - "The transform returned a null Publisher"); + Publisher out = Operators.toFluxOrMono(Objects.requireNonNull( + transform.apply(multicast), + "The transform returned a null Publisher")); if (out instanceof Fuseable) { out.subscribe(new CancelFuseableMulticaster<>(actual, multicast)); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java index 30726f9ed7..2d240732b6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java @@ -76,7 +76,7 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act return null; } - p.subscribe(other); + Operators.toFluxOrMono(p).subscribe(other); if (!main.cancelled) { return main; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRetry.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRetry.java index ec96045a55..4efa85dc8c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRetry.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRetry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.reactivestreams.Publisher; import reactor.core.CorePublisher; import reactor.core.CoreSubscriber; @@ -114,7 +115,9 @@ void resubscribe() { produced(c); } - source.subscribe(this); + // Not wrapping source, but just the subscriber due to requirements + // in TailCallSubscribeTest#retry + source.subscribe(Operators.restoreContextOnSubscriberIfPublisherNonInternal(source, this)); } while (WIP.decrementAndGet(this) != 0); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSample.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSample.java index 01ffa0fd34..84e8b63fac 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSample.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSample.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ final class FluxSample extends InternalFluxOperator { FluxSample(Flux source, Publisher other) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSampleFirst.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSampleFirst.java index c346f75002..a3027c86d0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSampleFirst.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSampleFirst.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -191,7 +191,7 @@ public void onNext(T t) { SampleFirstOther other = new SampleFirstOther<>(this); if (Operators.replace(OTHER, this, other)) { - p.subscribe(other); + Operators.toFluxOrMono(p).subscribe(other); } } else { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSampleTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSampleTimeout.java index 58d2fcc952..ccd51f7200 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSampleTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSampleTimeout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -207,7 +207,7 @@ public void onNext(T t) { SampleTimeoutOther os = new SampleTimeoutOther<>(this, t, idx); if (Operators.replace(OTHER, this, os)) { - p.subscribe(os); + Operators.toFluxOrMono(p).subscribe(os); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSkipUntilOther.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSkipUntilOther.java index e1865725f2..1aa25886af 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSkipUntilOther.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSkipUntilOther.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ final class FluxSkipUntilOther extends InternalFluxOperator { FluxSkipUntilOther(Flux source, Publisher other) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchIfEmpty.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchIfEmpty.java index da13faedb7..a94a412a3c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchIfEmpty.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchIfEmpty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ final class FluxSwitchIfEmpty extends InternalFluxOperator { FluxSwitchIfEmpty(Flux source, Publisher other) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMap.java index e703b43b31..39c3f8a7cb 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -233,7 +233,7 @@ public void onNext(T t) { if (INNER.compareAndSet(this, si, innerSubscriber)) { ACTIVE.getAndIncrement(this); - p.subscribe(innerSubscriber); + Operators.toFluxOrMono(p).subscribe(innerSubscriber); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMapNoPrefetch.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMapNoPrefetch.java index 404f86eab5..06620047f8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMapNoPrefetch.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMapNoPrefetch.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2021-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -215,7 +215,7 @@ void subscribeInner(T nextElement, SwitchMapInner nextInner, int nextIndex return; } - p.subscribe(nextInner); + Operators.toFluxOrMono(p).subscribe(nextInner); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTakeUntilOther.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTakeUntilOther.java index 86f7a50644..2c999c18f9 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTakeUntilOther.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTakeUntilOther.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ final class FluxTakeUntilOther extends InternalFluxOperator { FluxTakeUntilOther(Flux source, Publisher other) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/FluxTimeout.java index d139f5fde2..d1482d4c8b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxTimeout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,8 @@ final class FluxTimeout extends InternalFluxOperator { Function> itemTimeout, String timeoutDescription) { super(source); - this.firstTimeout = Objects.requireNonNull(firstTimeout, "firstTimeout"); + this.firstTimeout = Operators.toFluxOrMono(Objects.requireNonNull(firstTimeout, + "firstTimeout")); this.itemTimeout = Objects.requireNonNull(itemTimeout, "itemTimeout"); this.other = null; @@ -69,9 +70,9 @@ final class FluxTimeout extends InternalFluxOperator { Function> itemTimeout, Publisher other) { super(source); - this.firstTimeout = Objects.requireNonNull(firstTimeout, "firstTimeout"); + this.firstTimeout = Operators.toFluxOrMono(Objects.requireNonNull(firstTimeout, "firstTimeout")); this.itemTimeout = Objects.requireNonNull(itemTimeout, "itemTimeout"); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); this.timeoutDescription = null; } @@ -199,7 +200,7 @@ public void onNext(T t) { return; } - p.subscribe(ts); + Operators.toFluxOrMono(p).subscribe(ts); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowBoundary.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowBoundary.java index f3a1954c5d..ec05189340 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowBoundary.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowBoundary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ final class FluxWindowBoundary extends InternalFluxOperator> { FluxWindowBoundary(Flux source, Publisher other, Supplier> processorQueueSupplier) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); this.processorQueueSupplier = Objects.requireNonNull(processorQueueSupplier, "processorQueueSupplier"); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowWhen.java index cc92466e16..16d136d30a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWindowWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWindowWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ final class FluxWindowWhen extends InternalFluxOperator> { Function> end, Supplier> processorQueueSupplier) { super(source); - this.start = Objects.requireNonNull(start, "start"); + this.start = Operators.toFluxOrMono(Objects.requireNonNull(start, "start")); this.end = Objects.requireNonNull(end, "end"); this.processorQueueSupplier = Objects.requireNonNull(processorQueueSupplier, "processorQueueSupplier"); @@ -308,7 +308,7 @@ void drainLoop() { if (resources.add(cl)) { OPEN_WINDOW_COUNT.getAndIncrement(this); - p.subscribe(cl); + Operators.toFluxOrMono(p).subscribe(cl); } continue; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxWithLatestFrom.java b/reactor-core/src/main/java/reactor/core/publisher/FluxWithLatestFrom.java index 8b4b2eb4af..8693622717 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxWithLatestFrom.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxWithLatestFrom.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ final class FluxWithLatestFrom extends InternalFluxOperator { Publisher other, BiFunction combiner) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); this.combiner = Objects.requireNonNull(combiner, "combiner"); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java b/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java index a74c73cd50..4f82af4696 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java @@ -80,7 +80,7 @@ public String stepName() { @Override public void subscribe(CoreSubscriber actual) { CoreSubscriber input = - liftFunction.lifter.apply(source, actual); + liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java index 3b1cbbfa0e..4eaf1f0618 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java @@ -82,7 +82,7 @@ public String stepName() { @Override public void subscribe(CoreSubscriber actual) { CoreSubscriber input = - liftFunction.lifter.apply(source, actual); + liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoCacheInvalidateWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoCacheInvalidateWhen.java index 0c9ea70dce..8d03641aca 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoCacheInvalidateWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoCacheInvalidateWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2021-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -282,6 +282,9 @@ void cacheLoad(T value) { for (@SuppressWarnings("unchecked") CacheMonoSubscriber inner : SUBSCRIBERS.getAndSet(this, COORDINATOR_DONE)) { inner.complete(value); } + // even though the trigger can deliver values on different threads, + // it's not causing any delivery to downstream, so we don't need to + // wrap it invalidateTrigger.subscribe(new TriggerSubscriber(this.main)); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoDelaySubscription.java b/reactor-core/src/main/java/reactor/core/publisher/MonoDelaySubscription.java index 09697964f9..461b8d62b2 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoDelaySubscription.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoDelaySubscription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ final class MonoDelaySubscription extends InternalMonoOperator MonoDelaySubscription(Mono source, Publisher other) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java index 7865886443..093c54c25a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -143,7 +143,7 @@ public void onNext(T t) { } else { FilterWhenInner inner = new FilterWhenInner<>(this, !(p instanceof Mono), t); - p.subscribe(inner); + Operators.toFluxOrMono(p).subscribe(inner); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMap.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMap.java index 8d30a5d688..9f85b847c3 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMap.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -162,7 +162,7 @@ public void onNext(T t) { } try { - m.subscribe(new FlatMapInner<>(this)); + Mono.fromDirect(m).subscribe(new FlatMapInner<>(this)); } catch (Throwable e) { actual.onError(Operators.onOperatorError(this, e, t, diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMapMany.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMapMany.java index fa671d6c40..08d6d1dd9a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMapMany.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMapMany.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -192,7 +192,7 @@ public void onNext(T t) { return; } - p.subscribe(new FlatMapManyInner<>(this, actual)); + Operators.toFluxOrMono(p).subscribe(new FlatMapManyInner<>(this, actual)); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoLift.java b/reactor-core/src/main/java/reactor/core/publisher/MonoLift.java index b47b672131..697804dbf4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ final class MonoLift extends InternalMonoOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { CoreSubscriber input = - liftFunction.lifter.apply(source, actual); + liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoLiftFuseable.java index 7bc535a244..93da526312 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,8 +55,7 @@ public Object scanUnsafe(Attr key) { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - - CoreSubscriber input = liftFunction.lifter.apply(source, actual); + CoreSubscriber input = liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoPublishMulticast.java b/reactor-core/src/main/java/reactor/core/publisher/MonoPublishMulticast.java index 02f471eae8..ccf5999e04 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoPublishMulticast.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoPublishMulticast.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,8 +53,8 @@ final class MonoPublishMulticast extends InternalMonoOperator implem public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { MonoPublishMulticaster multicast = new MonoPublishMulticaster<>(actual.currentContext()); - Mono out = Objects.requireNonNull(transform.apply(fromDirect(multicast)), - "The transform returned a null Mono"); + Mono out = fromDirect( + Objects.requireNonNull(transform.apply(fromDirect(multicast)), "The transform returned a null Mono")); if (out instanceof Fuseable) { out.subscribe(new FluxPublishMulticast.CancelFuseableMulticaster<>(actual, multicast)); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoSwitchIfEmpty.java b/reactor-core/src/main/java/reactor/core/publisher/MonoSwitchIfEmpty.java index 42eb454489..e485f220da 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoSwitchIfEmpty.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoSwitchIfEmpty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ final class MonoSwitchIfEmpty extends InternalMonoOperator { MonoSwitchIfEmpty(Mono source, Mono other) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = fromDirect(Objects.requireNonNull(other, "other")); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTakeUntilOther.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTakeUntilOther.java index 5962d7ef34..0e3a448944 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTakeUntilOther.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTakeUntilOther.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ final class MonoTakeUntilOther extends InternalMonoOperator { MonoTakeUntilOther(Mono source, Publisher other) { super(source); - this.other = Objects.requireNonNull(other, "other"); + this.other = Operators.toFluxOrMono(Objects.requireNonNull(other, "other")); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/MonoTimeout.java index 24ccfe8912..97d0569b4d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoTimeout.java @@ -48,7 +48,7 @@ final class MonoTimeout extends InternalMonoOperator { Publisher firstTimeout, String timeoutDescription) { super(source); - this.firstTimeout = Objects.requireNonNull(firstTimeout, "firstTimeout"); + this.firstTimeout = Mono.fromDirect(Objects.requireNonNull(firstTimeout, "firstTimeout")); this.other = null; this.timeoutDescription = timeoutDescription; } @@ -57,8 +57,8 @@ final class MonoTimeout extends InternalMonoOperator { Publisher firstTimeout, Publisher other) { super(source); - this.firstTimeout = Objects.requireNonNull(firstTimeout, "firstTimeout"); - this.other = Objects.requireNonNull(other, "other"); + this.firstTimeout = Mono.fromDirect(Objects.requireNonNull(firstTimeout, "firstTimeout")); + this.other = Mono.fromDirect(Objects.requireNonNull(other, "other")); this.timeoutDescription = null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Operators.java b/reactor-core/src/main/java/reactor/core/publisher/Operators.java index 3d60285c48..0763de1185 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Operators.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Operators.java @@ -2701,7 +2701,7 @@ static final LiftFunction liftScannable( } BiFunction, ? extends CoreSubscriber> - effectiveLifter = (pub, sub) -> lifter.apply(Scannable.from(pub), sub); + effectiveLifter = (pub, sub) -> lifter.apply(Scannable.from(pub), restoreContextOnSubscriberIfAutoCPEnabled(pub, sub)); return new LiftFunction<>(effectiveFilter, effectiveLifter, lifter.toString()); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java index 83fa0b940b..29375a0535 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java @@ -62,9 +62,9 @@ public Object scanUnsafe(Attr key) { if (key == Attr.LIFTER) { return liftFunction.name; } - // We don't control what the lifter does, so we play it safe. - if (key == InternalProducerAttr.INSTANCE) return false; - + if (key == InternalProducerAttr.INSTANCE) { + return true; + } return null; } @@ -83,13 +83,9 @@ public void subscribe(CoreSubscriber[] s) { int i = 0; while (i < subscribers.length) { - // As this is not an INTERNAL_PRODUCER, the subscribers should be protected - // in case of automatic context propagation. - // If a user directly subscribes with a set of rails, there is no - // protection against that, so a ThreadLocal restoring subscriber would - // need to be provided. subscribers[i] = - Objects.requireNonNull(liftFunction.lifter.apply(source, s[i]), + Objects.requireNonNull(liftFunction.lifter.apply(source, + Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, s[i])), "Lifted subscriber MUST NOT be null"); i++; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java index 2d3ae6c28c..c345fbb93d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java @@ -65,9 +65,9 @@ public Object scanUnsafe(Attr key) { if (key == Attr.LIFTER) { return liftFunction.name; } - // We don't control what the lifter does, so we play it safe. - if (key == InternalProducerAttr.INSTANCE) return false; - + if (key == InternalProducerAttr.INSTANCE) { + return true; + } return null; } @@ -87,13 +87,8 @@ public void subscribe(CoreSubscriber[] s) { int i = 0; while (i < subscribers.length) { CoreSubscriber actual = s[i]; - // As this is not an INTERNAL_PRODUCER, the subscribers should be protected - // in case of automatic context propagation. - // If a user directly subscribes with a set of rails, there is no - // protection against that, so a ThreadLocal restoring subscriber would - // need to be provided. CoreSubscriber converted = - Objects.requireNonNull(liftFunction.lifter.apply(source, actual), + Objects.requireNonNull(liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)), "Lifted subscriber MUST NOT be null"); Objects.requireNonNull(converted, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/SignalLogger.java b/reactor-core/src/main/java/reactor/core/publisher/SignalLogger.java index c335128644..ccfe47fdc0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SignalLogger.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SignalLogger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package reactor.core.publisher; +import java.time.Instant; import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java index 988bb0aacc..61e45b7057 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java @@ -18,6 +18,7 @@ import java.io.File; import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -39,6 +40,7 @@ import java.util.stream.Stream; import io.micrometer.context.ContextRegistry; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -48,6 +50,8 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.CorePublisher; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; @@ -59,6 +63,7 @@ import reactor.util.function.Tuples; import reactor.util.retry.Retry; +import static org.assertj.core.api.Assertions.anyOf; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; @@ -317,7 +322,7 @@ class NonReactorFluxOrMono { @BeforeEach void enableAutomaticContextPropagation() { - executorService = Executors.newFixedThreadPool(3); + executorService = Executors.newSingleThreadExecutor(); } @AfterEach @@ -342,11 +347,9 @@ void assertThreadLocalsPresentInFlux(Supplier> chainSupplier) { void assertThreadLocalsPresentInFlux(Supplier> chainSupplier, boolean skipCoreSubscriber) { assertThreadLocalsPresent(chainSupplier.get()); - assertThatNoException().isThrownBy(() -> - assertThatThreadLocalsPresentDirectRawSubscribe(chainSupplier.get())); + assertThatThreadLocalsPresentDirectRawSubscribe(chainSupplier.get()); if (!skipCoreSubscriber) { - assertThatNoException().isThrownBy(() -> - assertThatThreadLocalsPresentDirectCoreSubscribe(chainSupplier.get())); + assertThatThreadLocalsPresentDirectCoreSubscribe(chainSupplier.get()); } } @@ -357,11 +360,9 @@ void assertThreadLocalsPresentInMono(Supplier> chainSupplier) { void assertThreadLocalsPresentInMono(Supplier> chainSupplier, boolean skipCoreSubscriber) { assertThreadLocalsPresent(chainSupplier.get()); - assertThatNoException().isThrownBy(() -> - assertThatThreadLocalsPresentDirectRawSubscribe(chainSupplier.get())); + assertThatThreadLocalsPresentDirectRawSubscribe(chainSupplier.get()); if (!skipCoreSubscriber) { - assertThatNoException().isThrownBy(() -> - assertThatThreadLocalsPresentDirectCoreSubscribe(chainSupplier.get())); + assertThatThreadLocalsPresentDirectCoreSubscribe(chainSupplier.get()); } } @@ -371,26 +372,37 @@ void assertThreadLocalsPresent(Flux chain) { AtomicReference tlInOnError = new AtomicReference<>(); AtomicBoolean hadNext = new AtomicBoolean(false); - AtomicBoolean hadError = new AtomicBoolean(false); + AtomicReference error = new AtomicReference<>(); - chain.doOnEach(signal -> { - if (signal.isOnNext()) { - tlInOnNext.set(REF.get()); - hadNext.set(true); - } else if (signal.isOnError()) { - tlInOnError.set(REF.get()); - hadError.set(true); - } else if (signal.isOnComplete()) { - tlInOnComplete.set(REF.get()); - } - }) - .contextWrite(Context.of(KEY, "present")) - .blockLast(); + try { + chain.doOnEach(signal -> { + if (signal.isOnNext()) { + tlInOnNext.set(REF.get()); + hadNext.set(true); + } + else if (signal.isOnError()) { + tlInOnError.set(REF.get()); + error.set(signal.getThrowable()); + } + else if (signal.isOnComplete()) { + tlInOnComplete.set(REF.get()); + } + }) + .contextWrite(Context.of(KEY, "present")) + .blockLast(Duration.ofMillis(5000)); + } catch (Exception e) { + if (e instanceof IllegalStateException) { + throw e; + } + assertThat(e).satisfiesAnyOf( + exception -> assertThat(exception).isEqualTo(error.get()), + exception -> assertThat(exception).hasCause(error.get())); + } if (hadNext.get()) { assertThat(tlInOnNext.get()).isEqualTo("present"); } - if (hadError.get()) { + if (error.get() != null) { assertThat(tlInOnError.get()).isEqualTo("present"); } else { assertThat(tlInOnComplete.get()).isEqualTo("present"); @@ -406,17 +418,20 @@ void assertThreadLocalsPresent(Mono chain) { AtomicBoolean hadError = new AtomicBoolean(false); chain.doOnEach(signal -> { - if (signal.isOnNext()) { - tlInOnNext.set(REF.get()); - hadNext.set(true); - } else if (signal.isOnError()) { - tlInOnError.set(REF.get()); - hadError.set(true); - } else if (signal.isOnComplete()) { - tlInOnComplete.set(REF.get()); - } - }) + if (signal.isOnNext()) { + tlInOnNext.set(REF.get()); + hadNext.set(true); + } + else if (signal.isOnError()) { + tlInOnError.set(REF.get()); + hadError.set(true); + } + else if (signal.isOnComplete()) { + tlInOnComplete.set(REF.get()); + } + }) .contextWrite(Context.of(KEY, "present")) + .onErrorComplete() .block(); if (hadNext.get()) { @@ -430,42 +445,36 @@ void assertThreadLocalsPresent(Mono chain) { } void assertThatThreadLocalsPresentDirectCoreSubscribe( - CorePublisher source) throws InterruptedException, TimeoutException { + CorePublisher source) { assertThatThreadLocalsPresentDirectCoreSubscribe(source, () -> {}); } void assertThatThreadLocalsPresentDirectCoreSubscribe( - CorePublisher source, Runnable asyncAction) throws InterruptedException, TimeoutException { - AtomicReference valueInOnNext = new AtomicReference<>(); - AtomicReference valueInOnComplete = new AtomicReference<>(); - AtomicReference valueInOnError = new AtomicReference<>(); - AtomicReference error = new AtomicReference<>(); - AtomicBoolean complete = new AtomicBoolean(); - AtomicBoolean hadNext = new AtomicBoolean(); - CountDownLatch latch = new CountDownLatch(1); + CorePublisher source, Runnable asyncAction) { + assertThatNoException().isThrownBy(() -> { + CoreSubscriberWithContext subscriberWithContext = new CoreSubscriberWithContext<>(); - CoreSubscriberWithContext subscriberWithContext = - new CoreSubscriberWithContext<>( - valueInOnNext, valueInOnComplete, valueInOnError, - error, latch, hadNext, complete); + source.subscribe(subscriberWithContext); - source.subscribe(subscriberWithContext); + executorService.submit(asyncAction) + .get(100, TimeUnit.MILLISECONDS); - executorService.submit(asyncAction); - - if (!latch.await(100, TimeUnit.MILLISECONDS)) { - throw new TimeoutException("timed out"); - } + if (!subscriberWithContext.latch.await(500, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } - if (hadNext.get()) { - assertThat(valueInOnNext.get()).isEqualTo("present"); - } - if (error.get() == null) { - assertThat(valueInOnComplete.get()).isEqualTo("present"); - assertThat(complete).isTrue(); - } else { - assertThat(valueInOnError.get()).isEqualTo("present"); - } + if (subscriberWithContext.hadNext.get()) { + assertThat(subscriberWithContext.valueInOnNext.get()).isEqualTo( + "present"); + } + if (subscriberWithContext.error.get() == null) { + assertThat(subscriberWithContext.valueInOnComplete.get()).isEqualTo("present"); + assertThat(subscriberWithContext.complete).isTrue(); + } + else { + assertThat(subscriberWithContext.valueInOnError.get()).isEqualTo("present"); + } + }); } // We force the use of subscribe(Subscriber) override instead of @@ -473,42 +482,35 @@ void assertThatThreadLocalsPresentDirectCoreSubscribe( // are able to wrap the Subscriber and restore ThreadLocal values for the // signals received downstream. void assertThatThreadLocalsPresentDirectRawSubscribe( - Publisher source) throws InterruptedException, TimeoutException { + Publisher source) { assertThatThreadLocalsPresentDirectRawSubscribe(source, () -> {}); } void assertThatThreadLocalsPresentDirectRawSubscribe( - Publisher source, Runnable asyncAction) throws InterruptedException, TimeoutException { - AtomicReference valueInOnNext = new AtomicReference<>(); - AtomicReference valueInOnComplete = new AtomicReference<>(); - AtomicReference valueInOnError = new AtomicReference<>(); - AtomicReference error = new AtomicReference<>(); - AtomicBoolean hadNext = new AtomicBoolean(); - AtomicBoolean complete = new AtomicBoolean(); - CountDownLatch latch = new CountDownLatch(1); + Publisher source, Runnable asyncAction) { + assertThatNoException().isThrownBy(() -> { + CoreSubscriberWithContext subscriberWithContext = new CoreSubscriberWithContext<>(); - CoreSubscriberWithContext subscriberWithContext = - new CoreSubscriberWithContext<>( - valueInOnNext, valueInOnComplete, valueInOnError, - error, latch, hadNext, complete); + source.subscribe(subscriberWithContext); - source.subscribe(subscriberWithContext); + executorService.submit(asyncAction) + .get(100, TimeUnit.MILLISECONDS); - executorService.submit(asyncAction); - - if (!latch.await(100, TimeUnit.MILLISECONDS)) { - throw new TimeoutException("timed out"); - } + if (!subscriberWithContext.latch.await(500, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out"); + } - if (hadNext.get()) { - assertThat(valueInOnNext.get()).isEqualTo("present"); - } - if (error.get() == null) { - assertThat(valueInOnComplete.get()).isEqualTo("present"); - assertThat(complete).isTrue(); - } else { - assertThat(valueInOnError.get()).isEqualTo("present"); - } + if (subscriberWithContext.hadNext.get()) { + assertThat(subscriberWithContext.valueInOnNext.get()).isEqualTo("present"); + } + if (subscriberWithContext.error.get() == null) { + assertThat(subscriberWithContext.valueInOnComplete.get()).isEqualTo("present"); + assertThat(subscriberWithContext.complete).isTrue(); + } + else { + assertThat(subscriberWithContext.valueInOnError.get()).isEqualTo("present"); + } + }); } // Fundamental tests for Flux @@ -528,42 +530,30 @@ void internalFluxFlatMapSubscribe() { @Test void internalFluxSubscribeNoFusion() { assertThreadLocalsPresentInFlux(() -> - Flux.just("hello") - .hide() + threadSwitchingFlux() .flatMap(item -> threadSwitchingFlux())); } @Test void directFluxSubscribeAsCoreSubscriber() throws InterruptedException, TimeoutException { - AtomicReference valueInOnNext = new AtomicReference<>(); - AtomicReference valueInOnComplete = new AtomicReference<>(); - AtomicReference valueInOnError = new AtomicReference<>(); - AtomicReference error = new AtomicReference<>(); - AtomicBoolean hadNext = new AtomicBoolean(); - AtomicBoolean complete = new AtomicBoolean(); - CountDownLatch latch = new CountDownLatch(1); - Flux flux = threadSwitchingFlux(); - CoreSubscriberWithContext subscriberWithContext = - new CoreSubscriberWithContext<>( - valueInOnNext, valueInOnComplete, valueInOnError, - error, latch, hadNext, complete); + CoreSubscriberWithContext subscriberWithContext = new CoreSubscriberWithContext<>(); flux.subscribe(subscriberWithContext); - if (!latch.await(100, TimeUnit.MILLISECONDS)) { + if (!subscriberWithContext.latch.await(100, TimeUnit.MILLISECONDS)) { throw new TimeoutException("timed out"); } - assertThat(error.get()).isNull(); - assertThat(complete.get()).isTrue(); + assertThat(subscriberWithContext.error.get()).isNull(); + assertThat(subscriberWithContext.complete.get()).isTrue(); // We can't do anything here. subscribe(CoreSubscriber) is abstract in // CoreSubscriber interface and we have no means to intercept the calls to // restore ThreadLocals. - assertThat(valueInOnNext.get()).isEqualTo("ref_init"); - assertThat(valueInOnComplete.get()).isEqualTo("ref_init"); + assertThat(subscriberWithContext.valueInOnNext.get()).isEqualTo("ref_init"); + assertThat(subscriberWithContext.valueInOnComplete.get()).isEqualTo("ref_init"); } // Fundamental tests for Mono @@ -581,36 +571,34 @@ void internalMonoFlatMapSubscribe() { } @Test - void directMonoSubscribeAsCoreSubscriber() throws InterruptedException, TimeoutException { - AtomicReference valueInOnNext = new AtomicReference<>(); - AtomicReference valueInOnComplete = new AtomicReference<>(); - AtomicReference valueInOnError = new AtomicReference<>(); - AtomicReference error = new AtomicReference<>(); - AtomicBoolean complete = new AtomicBoolean(); - AtomicBoolean hadNext = new AtomicBoolean(); - CountDownLatch latch = new CountDownLatch(1); + void internalMonoFlatMapSubscribeNoFusion() { + assertThreadLocalsPresentInMono(() -> + Mono.just("hello") + .hide() + .flatMap(item -> threadSwitchingMono())); + } + @Test + void directMonoSubscribeAsCoreSubscriber() throws InterruptedException, TimeoutException { Mono mono = new ThreadSwitchingMono<>("Hello", executorService); CoreSubscriberWithContext subscriberWithContext = - new CoreSubscriberWithContext<>( - valueInOnNext, valueInOnComplete, valueInOnError, - error, latch, hadNext, complete); + new CoreSubscriberWithContext<>(); mono.subscribe(subscriberWithContext); - if (!latch.await(100, TimeUnit.MILLISECONDS)) { + if (!subscriberWithContext.latch.await(100, TimeUnit.MILLISECONDS)) { throw new TimeoutException("timed out"); } - assertThat(error.get()).isNull(); - assertThat(complete.get()).isTrue(); + assertThat(subscriberWithContext.error.get()).isNull(); + assertThat(subscriberWithContext.complete.get()).isTrue(); // We can't do anything here. subscribe(CoreSubscriber) is abstract in // CoreSubscriber interface and we have no means to intercept the calls to // restore ThreadLocals. - assertThat(valueInOnNext.get()).isEqualTo("ref_init"); - assertThat(valueInOnComplete.get()).isEqualTo("ref_init"); + assertThat(subscriberWithContext.valueInOnNext.get()).isEqualTo("ref_init"); + assertThat(subscriberWithContext.valueInOnComplete.get()).isEqualTo("ref_init"); } // Flux tests @@ -676,6 +664,20 @@ void fluxRetryWhenSwitchingThread() { .retryWhen(Retry.from(f -> threadSwitchingFlux()))); } + @Test + void fluxRepeatWhen() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .repeatWhen(s -> Flux.just(1))); + } + + @Test + void fluxRepeatWhenSwitchingThread() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("Hello") + .repeatWhen(s -> threadSwitchingFlux())); + } + @Test void fluxWindowUntil() { assertThreadLocalsPresentInFlux(() -> @@ -748,9 +750,11 @@ void fluxConcatArray() { @Test void fluxConcatIterable() { - assertThreadLocalsPresentInFlux(() -> + assertThreadLocalsPresent( Flux.concat( Stream.of(Flux.empty(), threadSwitchingFlux()).collect(Collectors.toList()))); + + // Direct subscription } @Test @@ -789,6 +793,343 @@ void fluxZipIterable() { obj -> Tuples.of((String) obj[0], (String) obj[1]))); } + @Test + void fluxBufferBoundary() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("Hello").delayElements(Duration.ofMillis(20)) + .buffer(threadSwitchingFlux())); + } + + @Test + void fluxBufferWhen() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("hello").delayElements(Duration.ofMillis(20)) + .bufferWhen(threadSwitchingFlux(), x -> Flux.empty())); + } + + @Test + void fluxConcatMap() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .concatMap(s -> threadSwitchingFlux(), 1)); + } + + @Test + void fluxConcatMapNoPrefetch() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("hello").hide() + .concatMap(s -> threadSwitchingFlux())); + } + + @Test + void fluxDelaySubscription() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("hello") + .delaySubscription(threadSwitchingFlux())); + } + + @Test + void fluxExpand() { + AtomicBoolean done = new AtomicBoolean(false); + // We don't validate direct subscription via CoreSubscriber with Context in + // this case as it can happen that the drain loop is in the main thread + // and won't restore TLs from the Context when contextWrite operator is + // missing along the way in the chain. + assertThreadLocalsPresent( + Flux.just("hello").expand(s -> { + if (done.get()) { + return Flux.empty(); + } else { + done.set(true); + return threadSwitchingFlux(); + } + })); + } + + @Test + void fluxFilterWhen() { + // We don't validate direct subscription via CoreSubscriber with Context in + // this case as it can happen that the drain loop is in the main thread + // and won't restore TLs from the Context when contextWrite operator is + // missing along the way in the chain. + assertThreadLocalsPresent( + Flux.just("hello") + .filterWhen(s -> new ThreadSwitchingFlux<>(Boolean.TRUE, executorService))); + } + + @Test + void fluxGroupJoinFlattened() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("hello").groupJoin(threadSwitchingFlux(), + l -> Flux.never(), r -> Flux.never(), + (s, f) -> f.map(i -> s)).flatMap(Function.identity())); + } + + @Test + void fluxGroupJoin() { + assertThreadLocalsPresent( + Flux.just("hello").groupJoin(threadSwitchingFlux(), + l -> Flux.never(), r -> Flux.never(), + (s, f) -> f.map(i -> s))); + + // works only with contextWrite because the group is delivered using the + // signal from the left hand side + } + + @Test + void fluxGroupJoinSubscribed() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("hello").groupJoin(threadSwitchingFlux(), + l -> Flux.never(), r -> Flux.never(), + (s, f) -> f.map(i -> s)) + .flatMap(Function.identity())); + } + + @Disabled("Only contextWrite/contextCapture usages are supported") + @Test + void fluxJustRawSubscribe() { + assertThatNoException().isThrownBy(() -> + assertThatThreadLocalsPresentDirectRawSubscribe(Flux.just("hello")) + ); + } + + @Test + void fluxJoin() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("hello").join(threadSwitchingFlux(), l -> Flux.never(), + r -> Flux.never(), (s1, s2) -> s1 + s2)); + } + + @Test + void fluxLift() { + assertThreadLocalsPresentInFlux(() -> { + Flux flux = Flux.just("Hello").hide(); + + Publisher lifted = + Operators.liftPublisher((pub, sub) -> new CoreSubscriber() { + @Override + public void onSubscribe(Subscription s) { + executorService.submit(() -> sub.onSubscribe(s)); + } + + @Override + public void onNext(String s) { + executorService.submit(() -> sub.onNext(s)); + } + + @Override + public void onError(Throwable t) { + executorService.submit(() -> sub.onError(t)); + } + + @Override + public void onComplete() { + executorService.submit(sub::onComplete); + } + + @Override + public Context currentContext() { + return sub.currentContext(); + } + }) + .apply(flux); + + return (Flux) lifted; + }); + } + + @Test + void fluxLiftFuseable() { + assertThreadLocalsPresentInFlux(() -> { + Flux flux = Flux.just("Hello"); + + Publisher lifted = + Operators.liftPublisher((pub, sub) -> new CoreSubscriber() { + @Override + public void onSubscribe(Subscription s) { + executorService.submit(() -> sub.onSubscribe(s)); + } + + @Override + public void onNext(String s) { + executorService.submit(() -> sub.onNext(s)); + } + + @Override + public void onError(Throwable t) { + executorService.submit(() -> sub.onError(t)); + } + + @Override + public void onComplete() { + executorService.submit(sub::onComplete); + } + }) + .apply(flux); + + return (Flux) lifted; + }); + } + + @Test + void fluxFlatMapSequential() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .flatMapSequential(s -> threadSwitchingFlux())); + } + + @Test + void fluxOnErrorResume() { + assertThreadLocalsPresentInFlux(() -> + Flux.error(new RuntimeException("Oops")) + .onErrorResume(t -> threadSwitchingFlux())); + } + + @Test + void fluxPublishMulticast() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("Hello") + .publish(s -> threadSwitchingFlux())); + } + + @Test + void fluxSkipUntilOther() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .skipUntilOther(threadSwitchingFlux())); + } + + @Test + void fluxSample() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("Hello").concatWith(Flux.never()) + .sample(threadSwitchingFlux())); + } + + @Test + void fluxSampleFirst() { + // We don't validate direct subscription via CoreSubscriber with Context in + // this case as it can happen that the drain loop is in the main thread + // and won't restore TLs from the Context when contextWrite operator is + // missing along the way in the chain. + assertThreadLocalsPresent( + Flux.just("Hello").concatWith(Flux.never()) + .sampleFirst(s -> new ThreadSwitchingFlux<>(new RuntimeException("oops"), executorService))); + } + + @Test + void fluxSampleTimeout() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux().concatWith(Mono.delay(Duration.ofMillis(10)).map(l -> "").concatWith(Mono.empty())) + .sampleTimeout(s -> threadSwitchingFlux())); + } + + @Test + void fluxSwitchIfEmpty() { + assertThreadLocalsPresentInFlux(() -> + Flux.empty() + .switchIfEmpty(threadSwitchingFlux())); + } + + @Test + void fluxSwitchMapNoPrefetch() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .switchMap(s -> threadSwitchingFlux())); + } + + @Test + void fluxSwitchMap() { + assertThreadLocalsPresentInFlux(() -> + threadSwitchingFlux() + .switchMap(s -> threadSwitchingFlux(), 1)); + } + + @Test + void fluxTakeUntilOther() { + // We don't validate direct subscription via CoreSubscriber with Context in + // this case as it can happen that the drain loop is in the main thread + // and won't restore TLs from the Context when contextWrite operator is + // missing along the way in the chain. + assertThreadLocalsPresent( + Flux.concat(Flux.just("Hello"), Flux.never()) + .takeUntilOther(threadSwitchingFlux())); + } + + @Test + void fluxTimeoutFirst() { + assertThreadLocalsPresentInFlux(() -> + Flux.never() + .timeout(threadSwitchingFlux())); + } + + @Test + void fluxTimeoutOther() { + assertThreadLocalsPresentInFlux(() -> + Flux.never() + .timeout(threadSwitchingFlux(), i -> Flux.never(), threadSwitchingFlux())); + } + + @Test + void fluxWindowBoundary() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("Hello").delayElements(Duration.ofMillis(20)) + .window(threadSwitchingFlux())); + } + + @Test + void fluxWindowBoundaryFlattened() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("Hello").delayElements(Duration.ofMillis(20)) + .window(threadSwitchingFlux()) + .flatMap(Function.identity())); + } + + @Test + @Disabled("Publisher delivering the window has no notion of Context so nothing " + + "can be restored in onNext") + void fluxWindowWhen() { + assertThreadLocalsPresent( + threadSwitchingFlux() + .windowWhen(threadSwitchingFlux(), s -> threadSwitchingFlux())); + } + + @Test + @Disabled("Publisher delivering the window has no notion of Context so nothing " + + "can be restored in onNext") + void fluxDelayedWindowWhen() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("Hello").delayElements(Duration.ofMillis(100)) + .windowWhen(threadSwitchingFlux(), s -> threadSwitchingFlux())); + } + + @Test + @Disabled("Publisher completing the window has no notion of Context so nothing " + + "can be restored in onComplete") + void fluxWindowWhenFlatMapped() { + assertThreadLocalsPresentInFlux(() -> + Flux.just("Hello").delayElements(Duration.ofMillis(100)) + .windowWhen(threadSwitchingFlux(), s -> threadSwitchingFlux()) + .flatMap(Function.identity())); + } + + @Test + void fluxWithLatestFrom() { + // We don't validate direct subscription via CoreSubscriber with Context in + // this case as it can happen that the drain loop is in the main thread + // and won't restore TLs from the Context when contextWrite operator is + // missing along the way in the chain. + assertThreadLocalsPresent( + Flux.just("Hello") + .withLatestFrom(threadSwitchingFlux(), (s1, s2) -> s1)); + } + + @Test + void continuationBrokenByThreadSwitch() { + assertThreadLocalsPresentInFlux(() -> + Flux.concat(Mono.empty(), threadSwitchingMono().retry())); + } + // Mono tests @Test @@ -940,6 +1281,140 @@ void monoUsingWhen() { s -> Mono.empty())); } + @Test + void monoFlatMapMany() { + assertThreadLocalsPresentInFlux(() -> + Mono.just("hello") + .hide() + .flatMapMany(item -> threadSwitchingFlux())); + } + + @Test + void monoFlatMapManyFuseable() { + assertThreadLocalsPresentInFlux(() -> + Mono.just("hello") + .flatMapMany(item -> threadSwitchingFlux())); + } + + @Test + void monoDelaySubscription() { + assertThreadLocalsPresentInMono(() -> + Mono.just("Hello").delaySubscription(threadSwitchingMono())); + } + + @Test + void monoFilterWhen() { + assertThreadLocalsPresentInMono(() -> + Mono.just("Hello").hide() + .filterWhen(s -> new ThreadSwitchingMono<>(Boolean.TRUE, executorService))); + } + + @Test + void monoLift() { + assertThreadLocalsPresentInMono(() -> { + Mono mono = Mono.just("Hello").hide(); + + Publisher lifted = + Operators.liftPublisher((pub, sub) -> new CoreSubscriber() { + @Override + public void onSubscribe(Subscription s) { + executorService.submit(() -> sub.onSubscribe(s)); + } + + @Override + public void onNext(String s) { + executorService.submit(() -> sub.onNext(s)); + } + + @Override + public void onError(Throwable t) { + executorService.submit(() -> sub.onError(t)); + } + + @Override + public void onComplete() { + executorService.submit(sub::onComplete); + } + }) + .apply(mono); + + return (Mono) lifted; + }); + } + + @Test + void monoLiftFuseable() { + assertThreadLocalsPresentInMono(() -> { + Mono mono = Mono.just("Hello"); + + Publisher lifted = + Operators.liftPublisher((pub, sub) -> new CoreSubscriber() { + @Override + public void onSubscribe(Subscription s) { + executorService.submit(() -> sub.onSubscribe(s)); + } + + @Override + public void onNext(String s) { + executorService.submit(() -> sub.onNext(s)); + } + + @Override + public void onError(Throwable t) { + executorService.submit(() -> sub.onError(t)); + } + + @Override + public void onComplete() { + executorService.submit(sub::onComplete); + } + }) + .apply(mono); + + return (Mono) lifted; + }); + } + + @Test + void monoOnErrorResume() { + assertThreadLocalsPresentInMono(() -> + Mono.error(new RuntimeException("oops")) + .onErrorResume(e -> threadSwitchingMono())); + } + + @Test + void monoPublishMulticast() { + assertThreadLocalsPresentInMono(() -> + Mono.just("Hello") + .publish(s -> threadSwitchingMono())); + } + + @Test + void monoSwitchIfEmpty() { + assertThreadLocalsPresentInMono(() -> + Mono.empty() + .switchIfEmpty(threadSwitchingMono())); + } + + @Test + void monoTakeUntilOther() { + assertThreadLocalsPresentInMono(() -> + Mono.delay(Duration.ofDays(1)).then(Mono.just("Hello")) + .takeUntilOther(threadSwitchingMono())); + } + + @Test + void monoTimeoutFirst() { + assertThreadLocalsPresentInMono(() -> + Mono.never().timeout(threadSwitchingMono())); + } + + @Test + void monoTimeoutFallback() { + assertThreadLocalsPresentInMono(() -> + Mono.never().timeout(threadSwitchingMono(), threadSwitchingMono())); + } + // ParallelFlux tests @Test @@ -1004,7 +1479,7 @@ void parallelFluxLiftFuseable() { assertThreadLocalsPresentInFlux(() -> { ParallelFlux> parallelFlux = ParallelFlux.from(Flux.just("Hello")) - .collect(ArrayList::new, ArrayList::add); + .collect(ArrayList::new, ArrayList::add); Publisher> lifted = Operators., ArrayList>liftPublisher((pub, sub) -> new CoreSubscriber>() { @@ -1147,7 +1622,7 @@ void sink() throws InterruptedException, TimeoutException { } @Test - void sinkDirect() throws InterruptedException, TimeoutException { + void sinkDirect() throws InterruptedException, TimeoutException, ExecutionException { Sinks.One sink1 = Sinks.one(); assertThatThreadLocalsPresentDirectCoreSubscribe(sink1.asMono(), () -> sink1.tryEmitValue("Hello")); @@ -1402,29 +1877,22 @@ void printInterestingClasses() throws Exception { private class CoreSubscriberWithContext implements CoreSubscriber { - private final AtomicReference valueInOnNext; - private final AtomicReference valueInOnComplete; - private final AtomicReference valueInOnError; - private final AtomicReference error; - private final CountDownLatch latch; - private final AtomicBoolean complete; - private final AtomicBoolean hadNext; - - public CoreSubscriberWithContext( - AtomicReference valueInOnNext, - AtomicReference valueInOnComplete, - AtomicReference valueInOnError, - AtomicReference error, - CountDownLatch latch, - AtomicBoolean hadNext, - AtomicBoolean complete) { - this.valueInOnNext = valueInOnNext; - this.valueInOnComplete = valueInOnComplete; - this.valueInOnError = valueInOnError; - this.error = error; - this.latch = latch; - this.hadNext = hadNext; - this.complete = complete; + final AtomicReference valueInOnNext; + final AtomicReference valueInOnComplete; + final AtomicReference valueInOnError; + final AtomicReference error; + final CountDownLatch latch; + final AtomicBoolean complete; + final AtomicBoolean hadNext; + + public CoreSubscriberWithContext() { + this.valueInOnNext = new AtomicReference<>(); + this.valueInOnComplete = new AtomicReference<>(); + this.valueInOnError = new AtomicReference<>(); + this.error = new AtomicReference<>(); + this.complete = new AtomicBoolean(); + this.hadNext = new AtomicBoolean(); + this.latch = new CountDownLatch(1); } @Override @@ -1530,6 +1998,42 @@ void fluxFlatMapToPublisher() throws InterruptedException, ExecutionException { executorService.shutdownNow(); } + @Test + void monoFlatMapToPublisher() throws InterruptedException, ExecutionException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + AtomicReference value = new AtomicReference<>(); + + TestPublisher testPublisher = TestPublisher.create(); + Publisher nonReactorPublisher = testPublisher; + + Mono.just("hello") + .hide() + .flatMapMany(s -> nonReactorPublisher) + .doOnNext(s -> value.set(REF.get())) + .contextWrite(Context.of(KEY, "present")) + .subscribe(); + + executorService + .submit(() -> testPublisher.emit("test").complete()) + .get(); + + testPublisher.assertWasSubscribed(); + testPublisher.assertWasNotCancelled(); + testPublisher.assertWasRequested(); + assertThat(value.get()).isEqualTo("present"); + + // validate there are no leftovers for other tasks to be attributed to + // previous values + executorService.submit(() -> value.set(REF.get())).get(); + + assertThat(value.get()).isEqualTo("ref_init"); + + // validate the current Thread does not have the value set either + assertThat(REF.get()).isEqualTo("ref_init"); + + executorService.shutdownNow(); + } + @Test void monoFromPublisher() throws InterruptedException, ExecutionException { ExecutorService executorService = Executors.newSingleThreadExecutor(); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingFlux.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingFlux.java index 90bb0ad95a..4978bdae48 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingFlux.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingFlux.java @@ -26,11 +26,19 @@ public class ThreadSwitchingFlux extends Flux implements Subscription, Run private final ExecutorService executorService; private final T item; + private final Throwable error; private CoreSubscriber actual; AtomicBoolean done = new AtomicBoolean(); public ThreadSwitchingFlux(T item, ExecutorService executorService) { this.item = item; + this.error = null; + this.executorService = executorService; + } + + public ThreadSwitchingFlux(Throwable error, ExecutorService executorService) { + this.item = null; + this.error = error; this.executorService = executorService; } @@ -47,7 +55,12 @@ public void run() { private void deliver() { if (done.compareAndSet(false, true)) { - this.actual.onNext(this.item); + if (this.item != null) { + this.actual.onNext(this.item); + } + if (this.error != null) { + this.actual.onError(this.error); + } this.executorService.submit(this.actual::onComplete); } } diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingMono.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingMono.java index 9b52dcea52..9dd458c7d5 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingMono.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingMono.java @@ -26,11 +26,19 @@ public class ThreadSwitchingMono extends Mono implements Subscription, Run private final ExecutorService executorService; private final T item; + private final Throwable error; private CoreSubscriber actual; AtomicBoolean done = new AtomicBoolean(); public ThreadSwitchingMono(T item, ExecutorService executorService) { this.item = item; + this.error = null; + this.executorService = executorService; + } + + public ThreadSwitchingMono(ExecutorService executorService, Throwable error) { + this.item = null; + this.error = error; this.executorService = executorService; } @@ -47,7 +55,12 @@ public void run() { private void deliver() { if (done.compareAndSet(false, true)) { - this.actual.onNext(this.item); + if (this.item != null) { + this.actual.onNext(this.item); + } + if (this.error != null) { + this.actual.onError(this.error); + } this.executorService.submit(this.actual::onComplete); } } From 8f240352557761fc7df2490456c9f5bf6dbfcecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 2 Nov 2023 14:40:46 +0100 Subject: [PATCH 200/312] JCStress: Await Scheduler dispose and increase timeouts (#3630) Resolves #3626 --- .../scheduler/BasicSchedulersStressTest.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/reactor-core/src/jcstress/java/reactor/core/scheduler/BasicSchedulersStressTest.java b/reactor-core/src/jcstress/java/reactor/core/scheduler/BasicSchedulersStressTest.java index aad18a61ef..8c0dac28f2 100644 --- a/reactor-core/src/jcstress/java/reactor/core/scheduler/BasicSchedulersStressTest.java +++ b/reactor-core/src/jcstress/java/reactor/core/scheduler/BasicSchedulersStressTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import org.openjdk.jcstress.annotations.Actor; import org.openjdk.jcstress.annotations.Arbiter; @@ -29,6 +28,7 @@ import org.openjdk.jcstress.annotations.State; import org.openjdk.jcstress.infra.results.IIZ_Result; import org.openjdk.jcstress.infra.results.Z_Result; +import reactor.core.Disposable; public abstract class BasicSchedulersStressTest { @@ -43,11 +43,15 @@ private static boolean canScheduleTask(Scheduler scheduler) { if (scheduler.isDisposed()) { return false; } - scheduler.schedule(latch::countDown); + Disposable disposable = scheduler.schedule(latch::countDown); boolean taskDone = false; try { - taskDone = latch.await(100, TimeUnit.MILLISECONDS); - } catch (InterruptedException ignored) { + taskDone = latch.await(1, TimeUnit.SECONDS); + } catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + if (((SchedulerTask) disposable).future.isCancelled()) { + throw new RuntimeException("Future cancelled " + disposable); } return taskDone; } @@ -78,7 +82,7 @@ public void arbiter(Z_Result r) { // At this stage, at least one actor called scheduler.start(), // so we should be able to execute a task. r.r1 = canScheduleTask(scheduler); - scheduler.dispose(); + scheduler.disposeGracefully().block(Duration.ofMillis(500)); } } @@ -88,7 +92,7 @@ public void arbiter(Z_Result r) { public static class ParallelSchedulerStartDisposeStressTest { private final ParallelScheduler scheduler = - new ParallelScheduler(4, Thread::new); + new ParallelScheduler(2, Thread::new); { scheduler.init(); @@ -109,7 +113,7 @@ public void arbiter(Z_Result r) { // At this stage, at least one actor called scheduler.start(), // so we should be able to execute a task. r.r1 = canScheduleTask(scheduler); - scheduler.dispose(); + scheduler.disposeGracefully().block(Duration.ofMillis(500));; } } @@ -169,7 +173,7 @@ public static class ParallelSchedulerDisposeGracefullyStressTest { private final CountDownLatch latch = new CountDownLatch(2); private final ParallelScheduler scheduler = - new ParallelScheduler(10, Thread::new); + new ParallelScheduler(2, Thread::new); { scheduler.init(); @@ -263,7 +267,7 @@ public void arbiter(IIZ_Result r) { public static class ParallelSchedulerDisposeGracefullyAndDisposeStressTest { private final ParallelScheduler scheduler = - new ParallelScheduler(10, Thread::new); + new ParallelScheduler(2, Thread::new); { scheduler.init(); From e2cd68997ec2ba6f3dfe0c61ad1fc4b9dc1a2655 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Thu, 2 Nov 2023 19:17:23 +0200 Subject: [PATCH 201/312] updates to java 21 GA temurin (#3631) Signed-off-by: Oleh Dokuka --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/nightly.yml | 8 ++++---- .github/workflows/publish.yml | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12c17d6d48..13636d307a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -79,7 +79,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -112,7 +112,7 @@ jobs: # uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 # with: # distribution: 'temurin' -# java-version: 21-ea +# java-version: 21 # - name: Setup JDK 8 # uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 # with: @@ -144,7 +144,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -176,7 +176,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7d3c118d5f..a7ace8aa3f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -32,7 +32,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -62,7 +62,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -99,7 +99,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -136,7 +136,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c72dc49cb6..c5381bfba4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,7 +35,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -75,7 +75,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -112,7 +112,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -150,7 +150,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: @@ -190,7 +190,7 @@ jobs: uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: distribution: 'temurin' - java-version: 21-ea + java-version: 21 - name: Setup JDK 8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: From 373349ca02c5c10694e9e6c1f4aeafcb01b4aa93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 7 Nov 2023 11:16:37 +0100 Subject: [PATCH 202/312] updates to gradle 8.4 (#3632) --- gradle/asciidoc.gradle | 31 +++++++++++++---------- gradle/javadoc.gradle | 2 +- gradle/releaser.gradle | 4 +-- gradle/setup.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 8 ++++-- reactor-core/build.gradle | 8 +++--- reactor-test/build.gradle | 4 +-- reactor-tools/build.gradle | 4 +-- 10 files changed, 37 insertions(+), 28 deletions(-) diff --git a/gradle/asciidoc.gradle b/gradle/asciidoc.gradle index 7e76f88615..148497df49 100644 --- a/gradle/asciidoc.gradle +++ b/gradle/asciidoc.gradle @@ -34,7 +34,7 @@ configure(rootProject) { asciidoctor { dependsOn "generateObservabilityDocs" - inputs.dir("$buildDir/generatedMetricsDocs/") // force the task to consider changes in this folder, making it not UP-TO-DATE + inputs.dir(layout.buildDirectory.dir("generatedMetricsDocs/").get().asFile) // force the task to consider changes in this folder, making it not UP-TO-DATE sourceDir "docs/asciidoc/" sources { include "index.asciidoc" @@ -47,7 +47,7 @@ configure(rootProject) { } } - outputDir file("$buildDir/docs/asciidoc/html") + outputDir layout.buildDirectory.dir("docs/asciidoc/html").get().asFile logDocuments = true attributes stylesdir: "stylesheets/", stylesheet: 'reactor.css', @@ -65,7 +65,7 @@ configure(rootProject) { include "index.asciidoc" } baseDirFollowsSourceDir() - outputDir file("$buildDir/docs/asciidoc/pdf") + outputDir layout.buildDirectory.dir("docs/asciidoc/pdf").get().asFile logDocuments = true attributes 'source-highlighter': 'rouge' } @@ -103,38 +103,43 @@ configure(rootProject) { task generateMeterListenerDocs(type: JavaExec) { mainClass.set("io.micrometer.docs.DocsGeneratorCommand") classpath configurations.adoc - args project.rootDir.getAbsolutePath(), ".*MicrometerMeterListenerDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/meterListener").toAbsolutePath().toString() + args project.rootDir.getAbsolutePath(), + ".*MicrometerMeterListenerDocumentation.*.java", + project.rootProject.layout.buildDirectory.dir("generatedMetricsDocs/meterListener").get().asFile.absolutePath } task generateTimedSchedulerDocs(type: JavaExec) { mainClass.set("io.micrometer.docs.DocsGeneratorCommand") classpath configurations.adoc - args project.rootDir.getAbsolutePath(), ".*TimedSchedulerMeterDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/timedScheduler").toAbsolutePath().toString() + args project.rootDir.getAbsolutePath(), ".*TimedSchedulerMeterDocumentation.*.java", + project.rootProject.layout.buildDirectory.dir("generatedMetricsDocs/timedScheduler").get().asFile.absolutePath } task generateObservationDocs(type: JavaExec) { mainClass.set("io.micrometer.docs.DocsGeneratorCommand") classpath configurations.adoc - args project.rootDir.getAbsolutePath(), ".*MicrometerObservationListenerDocumentation.*.java", project.rootProject.buildDir.toPath().resolve("generatedMetricsDocs/observation").toAbsolutePath().toString() + args project.rootDir.getAbsolutePath(), + ".*MicrometerObservationListenerDocumentation.*.java", + project.rootProject.layout.buildDirectory.dir("generatedMetricsDocs/observation").get().asFile.absolutePath } task polishGeneratedMetricsDocs(type: Copy) { mustRunAfter "generateMeterListenerDocs" mustRunAfter "generateTimedSchedulerDocs" mustRunAfter "generateObservationDocs" - from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/meterListener/") { + from(project.rootProject.layout.buildDirectory.get().asFile.toString() + "/generatedMetricsDocs/meterListener/") { include "_*.adoc" rename '_(.*).adoc', 'meterListener_$1.adoc' } - from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/timedScheduler/") { + from(project.rootProject.layout.buildDirectory.get().asFile.toString() + "/generatedMetricsDocs/timedScheduler/") { include "_*.adoc" rename '_(.*).adoc', 'timedScheduler_$1.adoc' } - from(project.rootProject.buildDir.toString() + "/generatedMetricsDocs/observation/") { + from(project.rootProject.layout.buildDirectory.get().asFile.toString() + "/generatedMetricsDocs/observation/") { include "_*.adoc" rename '_(.*).adoc', 'observation_$1.adoc' } - into project.rootProject.buildDir.toString() + "/documentedMetrics" + into project.rootProject.layout.buildDirectory.get().asFile.toString() + "/documentedMetrics" filter { String line -> line.startsWith('[[observability-metrics]]') || line.startsWith('=== Observability - Metrics') || @@ -145,9 +150,9 @@ configure(rootProject) { filter { String line -> line.startsWith("====") ? line.replaceFirst("====", "=") : line } doLast { //since these are the files that get explicitly included in asciidoc, smoke test they exist - assert file(project.rootProject.buildDir.toString() + "/documentedMetrics/meterListener_metrics.adoc").exists() - assert file(project.rootProject.buildDir.toString() + "/documentedMetrics/timedScheduler_metrics.adoc").exists() - assert file(project.rootProject.buildDir.toString() + "/documentedMetrics/observation_metrics.adoc").exists() + assert file(project.rootProject.layout.buildDirectory.get().asFile.toString() + "/documentedMetrics/meterListener_metrics.adoc").exists() + assert file(project.rootProject.layout.buildDirectory.get().asFile.toString() + "/documentedMetrics/timedScheduler_metrics.adoc").exists() + assert file(project.rootProject.layout.buildDirectory.get().asFile.toString() + "/documentedMetrics/observation_metrics.adoc").exists() } } diff --git a/gradle/javadoc.gradle b/gradle/javadoc.gradle index b38ba27f77..cf2191c6d9 100644 --- a/gradle/javadoc.gradle +++ b/gradle/javadoc.gradle @@ -46,7 +46,7 @@ javadoc { classpath += sourceSets.main.compileClasspath maxMemory = "1024m" - destinationDir = new File(project.buildDir, "docs/javadoc") + destinationDir = project.layout.buildDirectory.dir("docs/javadoc").get().asFile if (JavaVersion.current().isJava8Compatible()) { options.addStringOption("Xdoclint:none", "-quiet") diff --git a/gradle/releaser.gradle b/gradle/releaser.gradle index c5c36f089c..23dcf9e485 100644 --- a/gradle/releaser.gradle +++ b/gradle/releaser.gradle @@ -51,7 +51,7 @@ task copyReadme(type: Copy, group: "releaser helpers", description: "copies the from(rootProject.rootDir) { include "README.md" } - into rootProject.buildDir + into rootProject.layout.buildDirectory.get().asFile.toString() } task bumpVersionsInReadme(type: Copy, group: "releaser helpers", description: "replaces versions in README") { @@ -66,7 +66,7 @@ task bumpVersionsInReadme(type: Copy, group: "releaser helpers", description: "r doLast { println "Will replace $oldVersion with $currentVersion and $oldSnapshot with $nextVersion" } - from(rootProject.buildDir) { + from(rootProject.layout.buildDirectory.get().asFile.toString()) { include 'README.md' } into rootProject.rootDir diff --git a/gradle/setup.gradle b/gradle/setup.gradle index e53af7c0e6..5b6f72584f 100644 --- a/gradle/setup.gradle +++ b/gradle/setup.gradle @@ -91,7 +91,7 @@ publishing { repositories { maven { name = "mock" - url = "${rootProject.buildDir}/repo" + url = "${rootProject.layout.buildDirectory.get().asFile.toString()}/repo" } if (qualifyVersion("$version") == "RELEASE") { maven { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 37652 zcmZ6SQ*jNdnQBE-m!q1z)J^6!8liD~E|8k;d@!RKqW+P+c{{A_w4h-Fct^jI*3f}}> z2Q39vaxe&dYajQhot|R|okxP_$~ju*X0I0#4uyvp5Y5h!UbielGCB{+S&Y%+upGDb zq|BVDT9Ed2QC(eCsVrrfln`c3G!v|}sr1Y02i z%&LlPps4#Ty_mb$1n|@5Qfpv_+YV$Jdc936HIb{37?{S?l#NH+(Uw<@p6J%2p)un; z8fSGPL>@VtAl4yv;YO5e z$ce51CS;`NGd!WVoXeA9vfJC?1>OLi=8DCWBC=^_)V|)E5|B~`jRg01sgJZg#H@DN z(%3v>_-$+>k5p8l?YQWO0Xnm+Qg}U9W+}Al#c_RurG{H6IF}%vlMobp!nmIFL5{I# zoF z4ytIT@lBphb!xg@+~Hd9$f>Hh zUWt4fdi9Gtx|Z%Qfqw2|q5|Nnxh|mer1*VKpI}@@YPdN?TtU6jE;@uhxp8=l?#DTW z3?}F=_muS@5OK7^63G_i&I}DlJCSXGU*&Kq^(hgNE-=%%`BAo0 zBU#vb^C+2dcfe0`MDBTc%;9sY8a+%WNboJPY~n<&z)unXq5*0aZ&|aYVl1Am$Xp_c zU6TBDJ)I1Czr9Fusl92Pkm{EaI=QRi&nIo%&vvPM$PW7gOATu2+6A9&#{E|R8_vZD zo=}nNASfxDaaoMiy1+Z0+XD9hN4VaK<7I$rOt z5^|1qXwt%WJ5}+eQ#RFYSZ*(`YcT-098L^_8q29iO=XfmXO;Z9NHp+;FxUbI$Fg; zi510A`7H3>G6C##jBjc~Ixv7Rty}TthLu-u<1akLY7djP%xObB2KP!vAp?%YSbD^% zu=YcbKXUUhzgC;^%P&GvnnDJ&9=Xg%dauiSajot%RIn@(gf);fn@&Ru4)KS47(OdJ z$h)5lhgOh?n~P1R&)RcABS_Qia>NzjcvP`~C&VU6N2E8OL&X&1=1U2b&N`9o??Yn> zF<;;DseXn1&2-S!d-L&Z@p7C>>z>}0fA`19kNzf@X6+?iRv;E4ptwF7UwR@K58#?IR?)HVT8 zl~Dm+bfAIu3_Uc6J6a+zC+(~hEa^(RtRb#jVZn#5;_Fi`yR0K0?3LpaJTu+@7UsX& z#qUh`Nb;vJ0R=JB!leZl^YGMQ=p^l!6|^I_CMO(I)y+$u>K3zK#wVX08}j>x3CZwp zlk*ylL1!pfyq)Mh{n_|@TFPDddYx131Jmjk#j{Kh5*L*ig|AGXsfKOg#A9=C+CntSIZTb-d{G)j<>I+x8(cr40Xc1%<2LuzauvEDVt6i97SpA6 zsxGPO)MV;#UbwBSPiP{2*4l8o(o6o*tddwUFwx3;(g3LspjtuwUQvC*_4iMDCj+7uNe z>HNYl12vbCMsk!BRX&lF@neUQF46p|G{+&{RA1VANjF~C@9I6Br_$YAdX+rqwy7+| zPf=TFt(2f#W6Zb>-7(K%c~P$-E5B%z+?{oOh@b%O6VJEKH^@I;y!78V5vYfx#vL|J zte^#>+1NkFzOBEu6N-m!uO({kkWTY=oOtt5gF-!78Cb;LJH|+GW=czxXTyUDFBdbg zw&;1{SfPq|#+>6wJ;@YCj^E*1Z{Wtt;APe=!aZ&)_P~Wq$346{9sl6}#we1s$o+9H zH2@_Ct7gbH9Oqtdr=IDyUGFHc@}NPiXO$7%44}{^?+MTHPpFs}U1ktHWzj}Bmh7}} z0r`~t6xa4x#>EyC{l!C;zpw){$b=O||F?$c0b<;(<3p_FLE)z)5kvMz%M$s$!kQ_@ zn7YaOX%*Syd%2nV(t`wfW^U1#TSeTnz~P(CuN9rh$N(BdqHmQpSlbru>&Qzp$!Wk% z@i17nZv$pOU|V^^=Zs*wcArd+Ig@jr0zuo%Wd)iEO1x#u)m37$r7*KFW9)89oswQ# zSYKZ^R5ka^d-_*@na|Ow8zNyJ708zX4N6j&jykXV7%hZ|j*C~=m!BN;4KHywBL@+J zFMVY_D2@vrI@t{z&|1*KsUw>d1SRZ?V>}z7O@%r#Y@yFi4d#!`PKfi>SE6(y7$7?o zh^&V1d)~1F!w62_{X|LVW2E~`cd+u_koSGZOL**qSQj;OFHOrag&04h*(pJdFN6hx zh<`idoM?HedX~KoGce-)-;g^Xb;;7#SY~TY0~yH&G~!Kdm$7U4=b5|mk@Ktm{rke$ zRd_nDsKt3|h;WU(v78jFvhvoGaG=F!ZU7;=mve%3PVm+Zsz!^ELnE&b8=*|m;?b*BQe}|1AK&i+{?MLRhV+uBX*Du$tfT}EnHNpBthR}_xDzZ#PB_ElYd?REZ#@GIbt4a63@b<^e z0Roi}Zr-Q-sD~v`HAvj{K=fpGi}!iUTfwsL^W_7opUM5+Nom4Vf|-l>{5T=VEoa9` z$wdiRKM}u~6cGK4Hyv}17PNx+9%x+42m!jaas7pL9uM@LO#WpY_b#a??K_*O@u4As zNH0$up@AAflGq@Ck)t(XG>@nlrgzJuhUh>K8*K9?5DAIZZ53v-hlF|kK6vrENdAWw z<*oCApq8wFPL+lLQGuCv0r!I762os)Fb@WTS)7ZCeFb|Zct|UBAa<1<9M|wVu@TfO zAY@^rrg}Qu{e0z*!oHB!*>jZ}Zm^X;t)`1iOubj30>uC2dHBgCdTcn4*hIt&>mjgs z@chLwLzCM3Jk`)6J@77;ave;*g27yps*!8eRuZLmf z+~W>kS#<_W3dbNz0z1PI5<%@gMRiLvo9RlIcyf{gTTjZp>n zCA6CO0>+*AiqzO8qo3-eITXeI1N^_bvwWZ^K!gDU^FT|w=A=#{^cmmW%f^#;Yr)G(EHZ=8TYj> zSU%DrTk1YIp0WUqaalA-#p+mWV?;DN3=)M8r7Oej=b#Z}Xs{p~wrO27JcTDGW`H(0 z!qD_Xd^F$s$C;GWMER%{I%p#(W`>Mg=YV%ztG2Bf&VQByR5*<=W;(~&w450Sw- z&v)+bPcx|8L2x+5rc-uwKl**(w@A)E_^BHgze1&B1!a?Kcro8Vf7s-=ujFiEi}=4W zvQ80O;nlZ@sW?VZ$D}IQT1l~EunsL>ui8nrr5#Py;lRFQLppSXmNScPVcjw`_=j7P zC6G&zna5UjbOxVD{Q?%G!F`(<@txVX)Rb&Ci&WIc+boK)Vx(P@Y8^%#E9tp2FzsL7 zN|ujIll!%^2cqT#x#Uyw0QsvnjnYFmnVc&9Ld&rvD|uMh`9B(k0+h;9@|U*z83Zc| z^gDgyTIr>eE7P&o5`8o6Z-74$JA$Bv)q6&oCFFOj1RmC~f%)|`q|~|=VS@4ai}IRA zrk`paX)_$nXpBX5HkEt<+QYcJn>9!r{#OpG*?**E zF4DG7h+-+ilK6_$ewPrM*B&FEKdt7gB^xtmpUu&pu~YsM){ycr7!-yBp}ssn|2T*4%vhs9ZX;FE0WM5iEo7Jrgyj(au+Q_^8*7aN%nC2v9BpOz6E;@Ae z6`jsk$$MUJAA<`gSa8*9$LWW)G=q*z?}1lGb2_RIg8vFk4Kb@u0;H9#xQjVQLVD3rgP%9YxIfY>cZQp1Um8nZhx30;BqgqHI=dBJ- zdDdvni6NaU&Ju2^7K*hiXC33bnfox+8vbL>w;of20_c&+q)y&FWUtoFa-yRj_~F%* z=t;#(7UlA4%Fm}#R5c575CsnOc(YVYm$s!TAdo@;(UJrBnhU)PuuD)E^o@HJN32XF zYRqj+d$AM1tACioZZ8YvrXci@ELZr9ACNU$1_KXS?$MRCcwM*ZcE)&wi_#NLH;2%V268UW?OVFSIJ;C5d zKnqu91}(Z4e^!Ki`q{xJp?Jd2guS*fpuaD+t{iW;&|>9^MF4nuNuEk zeolrCT^Ek-YNOs`eZ&)69=31j{z1%<32I;=$`ub8Vi%T_1cDAB{f3dJi$)l~eK&Si z6kXy;&3=8NH(oC@C8nADzKW@aD|L^|q~s^QYooSr7bhXw! zuUyO%6(tOngxFePj>!*q@_o!6ypM;f-s^+xlK1=+ujdy244_Jo>v1f6(Pe6ez09HD z5S+aeYZ&4cxB^+feStV~!Wj9^s=zT|6sU-^I-Plyy5(MeJAz~QV0bHxP85Oi1^%Tx>axi;rp2a} z>Uy%3d(Zo0^Xv8fg4LQYpu`q5$rNQs;=XF?#5J!C7T|wJ4`yx zCf;EWH`O&&AAbQ8Z)h1_!=pZFDTPzM{C98nxWH6h4zf^Z@qOQRnH!=_=GxW=Z?srv7J=%JCXF*? zw;&5KD3-^6{WS3O+hyH5tzQ_ev{ zuOquYA(x%naj=Y8C+^9@Pn`mxO-Ws8gKa<|CKwHljJXoe146CN&DfGd+S&KK&6K1k zv?FDRELtxCRu~W?6;#dFMD2<~Oc=PWPC=v!(tOfriOePfkh^dga&#=mxYxmc4pXcf zfmFJ@7EZikj4xi{g@lHmj(N3P8#ol}n%^xUL&2GlG6z#o@BA5xgomE`-T4y}?6Cw| zx$OoWyAx{_EmPiM zEi%=fEgF+Zd2S7=j&s_l#rQZ6u%Fqo@*|xxH2irHz`i6nPt^V-Ou8_YYVQfeCAJ9K zAGqsa3u-)Hrr8K~wQJ7AQWZE%f%b%sR7l~T)YDpg%88Uq1Cc(OZ8i~ln};D7)*Ly< z9lUkgXPLAN=&w<1i5R73?8rUTPEdh#StrnUghGvJbbUq)?|p(cAAKe;QuPfd1ubD+ zl+)mVP!*K1J^Sl0khkO$JJ;ek*|!TE@7Ai@Uej%#@Ya-Nl$F0TDPz>u&S)#j$peaG zm(rIO;#Bz@Kqguv-Lbk_N)6?va8rmb0U6cZH*yUYaBK7}bbjf^^=Z15+ZO2p#3z0| zo%K((lY-D_&bNsp$;_h2W=6i{$k14a1 zu8Pj(iv4aKPJM26ZuvHk2i#{Bg+HsHj=r&)8LzZopotENKxdgup)@{UDN)?ydnAe^ zz`+DYsE8;BSSY(0793hBr*-soAl@H(kB9spa9UUr>`_qP?&q162GTWMKkmdc%~F?0OQvPBw%M3DjAH$mP_0 zn;RX&9lJ$sP|i!6&4StDdL>Oz8svAEg<5wtY-|z(uu#pLh&n?=w*%|EQ=aHVisIDh z3}DGGi|h6YYoJTe%1*Q?#aJOUF<<|(vPg&H)+|u~iu9vS9sg50!Jh21FtQ-Pz@-0q zwA}x1tYtZcPJ%x{1*NEO1C}H(zgAPp#c4)(B19LzlLYI?m}EoBSY?;O{hq6FwvrbW z)lHA7VJ(b2N-!(!IVHIH<{P-D%)mF9p z_v?`xOtzi+5CRLMJ^!E`ceH`wurLx)LoK<1?vNbHmJZX00c5H_f(EWqPZ}y~qOI(t zJxI~%HIt;jAwNf8r?TMW6-K7}r$h>HgwU2AF zYg%ruK{p0=fR@mW9RPFOJsCkllZXIzJ>`7cH&SG>sXL=!Wy(AU9z(NqV!IpoUa^)d zok2QH@BZ(1i8DFw6=)u*OH7j9ka*UR-LIEOI}w|z^Und?K;rb7{H;3HO15)S52HBj zse@>hT}GDaZn#Y2cHx1h(NJLFi+^t46z{2GOpo4}Cpx=4V76uK&CfJ`ly;RIQ_b zhK1n^bnX3=S1ZWRULjo^?^Ech$&!N^3VmQy?d(I{oRCK*{r}(mJ zPik|X+)CrZob_ZsN;}R=Tg{%3_|m&$wR0G;(5CCJZ$DAK_aF@U0mtHaS!*?8ifx64 z`H7aSSuvA*o+?b<;tSB*|K8ZkDZ1)Q-K3)yfg+*2`r?9&6MHexRSxdv&xv$Wq}UQO zHUx`7rPA=%i#!y`fADsSIb%$ngkI)zrE5Xzxm|Z zh|~QJ^;QB6S5Wgb_P{Xe#Xa0;ph&uC<9qQuVHBJAszfF%v9hT=2(u?G!i!Ht&=ieG zgDS!r#*!8Js!5pvrgN;5Uq1srr4>gEUjlkyZTY?*6RlBLSl;+)oseT%r4G{ch9L*} zU>TXDTA=^70wFFUESu9j=$7?02#dN0b+UbLbIq_@q>!{Y$u;rG{SrL-{(bRR0!<9V za2E#uYrGkqP@39Z#}Rpd6+WA5Izn^aD2GY7;b4bS?ig+2Qu1HO%iLlTaqu}hvjLiU zOy8q3(};?+|Gws4jkLa`FMd}DOkbQPH-SKKDA@ej_R6FW!JnW@1q@|WLEwACWn;1m zq?j^VRI}`q%CI78G$)k=BnD>CU#81a1_xl)_Q+|`3*=Xb7|H)Y7Z*ny$X}3FiyiDP zmb2Lz9hZ51KR^)aBTXD$##R)i9A--B7Q7+WNZiJi=?nRV6k_7x8<%3SfY652A z&V2*%x;wu?c^zj?ZN{}By_a0S@e&Q_n+4O7p*CBF#6u@UEcMFD+GkPgyxgJ+95>u+ zQgVKm9`_w)#ZuCFa$Z%t>|(ngMThCS_vhD52HNAY8FthjYZ4JdVsB?oN8q>O{kVV!IjZE)hnTcUc&~{Vyg!7tQ4nFp z;i?p@^=jOv?>~mT3FR4z&q}QJR+F+Uelw~!jt6@rsFY+vf_S|&ZB}hXL4fh(<+e+kGjS07#P=N zWJZg$-!MkOAGQy#eo1{&$D`X9SD${kCwI%Z9e&$Lry~;C;7_U@cP%0U2%useF8ovz z-%5Z$(;>zPH&<`m*Y=2 zmAK5EHz>RQ8Lt7_c*ZB`pTm3 zO?<8$R^ztmO9dtdOemZT_AH)su9yuW{WF|`s z`E$HVAoe3gCz`9|&hF1C(V*Dj%oUV7=2tit&}H5CNmSW9VZNn%g+e-7&J}w{2LJj3 zdxYxxSqPFkHOq>mQ9guwv-2-w8HY(Y7ERx`K6+)5@qwK3VIXTp=e|Tu+>zgklyW%a z^2{D*G$jO9SSjtn|A+9D6`a` zY_t#Jzv}gvVn%@cr{4B|kt>6IWBtj^V|&YoAD)LXR0b~)AIhWmt#*yVfgILzl6m*pC)sVEpC>2G zU@%r2Qbji8K{nWm_RIC=#$zHm@t$YW%wFPBD+FVZO&Ey!gEnhPSNkLF*OhUF*C3bD zWhCgqAJ~&iw-nYAWd>5?zNmDr>dfe9)c4mVuIghr#;12v8r(|cmc_&Kz?^_<-W($V zY(P0bg*XU_>HRy$z!emZ&0g>QLq*+;k&aiU0D~Ev#;4o*x+5ne$NjqK!l00`W5$L@ zGia0dJg*}t+^PQK7u?FokiKmyA=DfT_QIYTs3%1n(INy?gZN-RFi#J*55ks2)-}o6 z`2;^C;D@&Jvv5tE9B;@|1hdlwPfE$h#YkDFqOh-J<8W(AenY;$K+1efw_psQ;AjBC z0EOkWMnBU%hzPQ&1=>~CqD^}p={B=fB;d@2RfRG!dyQ=6Ml)%d6wjm$&!i7obBE1S zaQh-Q?YQF)xHq*}?Q7RZ@daB^IJ@IN5&o-}Ypvn#BtD5?xE=yS1a60|Q<$bPiHdJX zs84+OG3a1mbaY@~RR2du&`J5yupnzA-IbKDSjMx7Ip!=3YBV!6?eI$vxPbIw?HnkU zVTFFu0d3gGPdj=I3i1hx(E8w?8?>?o@>*HgDm2Xu1JX`#Ean+1@aFldgU#mY8Emps za>k3`BB`%ezKIMQ@LZn-!0WE(Y?nE~Dd3#1*Wvm-447Qnr>E6W+4*gT7wDrd!i$jY zMiaw% zG?#L)sKISRO49P7*$AtIAZU~h{4jaz_IzK{%cfWL?zT}*35C_HFhVB7Y}^ck{a8)3 z6j#N}q!lx(JP}=-VY@(J)p6_9#HLxP>SnyGXUE14?PQ*zo&C*H^3=tR?`dT8m7MCz*5lBy6p zq>TO{HFsBK8q}x_)`4;J%UdG~z3*|*LyS>mS-&6_ehQ#-77MfZDU(>N1)I9_U`N9+ zH+f^gh4O8k`BXs_ftV57Lddg*W{>WEa#%=S90s)8kK@;R?7;nAg%35yGoYraMjAEI z`;}1>+j>fSRnp1pAepm}PKtvdahlK+xS-YDYYOrB3lo-GxnHD<7rn(hhM-Z%-2Z$g zpggDHiZbvcIsgnut}WH*rSX{FCUvEzuBukQ(a-ZS5=)k;9E9VT++U49x4BZ{Tm zHL|19Ab?t?vA>~a<}B~~I9MXPO3jmISbtQF?^V*j4+k~Kh!yLKj-oScKLWA;GWoN7 z=xGvqAU?clBP2(fD73gngTRVf*TA=)k}w=7W?ev;(d6>R)Wm^qUttviohjljZc3w- zP(QP1wC>Ku5Ar59M@9%1NtkIFV02d<+>&$Y^lB%byWzGBRa9BPT5*gDYUmG*m#6ml z4LLOMA|ULbd@B=Rt6V&x@#a#}87oil=M-MN+z!neF<1k-Q1~$y*L6fUC|O|NcG)dk z+^eYd8FqDY-UqB%g@Xf7Sv^uEX# zdD(a}u^AN$OnvT4nihKguQ1Wx*L-(B|6z2jXt+CD)E5 zlfr~j14MK+5hE?`3uzvuri!35s%A@U)oy{oUflp(^z$vHK%k=C&bGv-C8t~JImU%0HUKZse(qO>{99Bvsl zib(}khqWh+7ZGQbGABDko8dOM@<)OQY{P^PA-faqW^(h4dcP5gfL2U6D>u5tXVDw! z4Mbs4R*60r8vEPgID5etTc_M|88B0cJuXn~4LM7zoSKp6D`^Ap&w3lB&6$*ApI^5c zGfA?L%c4rxTmAu$dCxJs!B!LIQhFfZOOowN7hW8$EfWkx-pCHxtd4UPBhZ$h6(in| zROv`G-FMhB-{;zL*jHHTf_X+S@Ji*O2BF#>vxP!3ZqV3cUyU&Z^!-@BBoDGSm6qai zhJve-6jR!`c1~(RRohZKRgo=3Z=zr#O4XyvilFJqv7EprbvjB;(FSzrkHtbybpR=P_7j|qGl{n5`~^i;e$_m}tZm)Hi5Ev+;t!0nAcuGY zxHvBZ`6_K67+`~ubaYA$J+tvv8MtO6sxEqrL}BVyaWe4=H)CJ{RSN5%?>0l57NBa& zV&ZZVbvN}gb&C|J14!Gln%Hh%OS~QzOx>yydwkN((`r5Hx)WSg(l$~V8J%PQ=p?h* ze5l%M2G{s0$crU z#!eygiTwrF*K|bMArB@?oO+F*nkO0lWAV@KPusDnKx5Fs1LJdEP0H=X zBJJ-uH@onSH20f&74iUiE_NL zQnlb>Bx9k4EXiWVg_N>0SW+AP)=lZ{=j{!hO#MtEEAPS6ZW;7 zSf;k9&Ilhol+gZTemQv^)H)jQ9^rYe z#tYKj@&l`HdyGwthiYX2ztuvHy`V;9YB zDwd^XE48}(sIlFwD@RtoO0iYxX?(npiDcZMf45rpD@q;t4D^ctz4a{3oofz9)c)I= ztNxP)8hCK@JH~_E%G(JtE_XH>JFn6?5QGp-T5MsbzrE znukDnlPT``K~uzJew$MRJxj6_&&SiGBu^%bBGu@A4{0*HbrfAmqkM$*%(x@iX-9o> zT6lo5;@gX%mUB)FVx@bJ$!52Qpox0xgM9*Z2+G%K%xfZ~st+X3NLtu2pCPyj+9C~~ z|6z3goCto*p|3WSz{IkoPYiQ_cXd$WzP1wZgkxZsRPn3T$b)CP+$&g)A~}OYUw&Yn z-|h7cD)Tk1x--q?+dxOt)ly4pF(WPxpR?4Ys)eVVcHG^DdNez~&QgFQbP zT{fIjOL%rOszhK21=6f{PT2 zyd5R4m~vOvSb=FB?7WrRKaI%|%8wlE0Gp&=Punl6yX#@uJ{VA&2xr zYo`-aamROVpiD^_p72LBu9@(!;v!M~XlB;lhG{4MNZBblPloOD*vaSE%x-s7zs4um z)Ff3aKS_{CCI5*cI&RfyI#9ly+*wlrdA%3BFn+qcc3C%Z#_*S853{*|*dKltn zC7y9@#b#L~m4Q|2fw@IJ`EId0^7Q_(9jC7biWYI%4J3HQJUo{$5apf@O%xp8i1QgR z(DG(2ZzTvKkdZNG4qcYtjw|TaZ1<`C#HCs%b*wZ9*rPEkwt=00>Fz<03# zU_#wZ)q+fj^xJfa_v-5qs4x4aiyu0qeE>M4YMws1Owp7B8tBnWkjFyL^BwxQhG)(o z8U*Qm&F0X#o7)+;h~I)Ca+XQfffjt?OPyPADv^&Jg0!8tb4CXWn2BEK6+p5+f~2!Z zRYMAdh)MyQO`$nIxrqWaNjmM^;Yc0+?zDJ)b1NBg;f|VW0&z?=J*CBvibxL|92s@~ z(#eZ^_X0Z@c%Pjk_X>CijiF<=tI2NApn!Q}q<;E@{;mAwl%csrBnJlBO!D|$=f$1b z^R1@4sgPTOs~g6B7i-6l9?XOaeXbgZ=LTzYeV&>JS|U=q++1PWyhq#^tn_dM<(L#6 zoT?Xhv~N~Mjnxv=t9v%p<~G%){f5z!^~Byza0XN(bq(NsqU1ti7(!t&hgPW|VXFjX ztCR-V$nOLtxTL%oS;fT0+CkxV!zGKc<$4k6ThZ+Tk;tBb*K-A`exdY7oOUT~&M_Zw zn@6g8%wbMJJ|S60xDFG_aFr&1;Sh@qh(Ex79NiN~mubW`KEsBdvIb>p&oa0Q%_31(B_(a3FgQFW(=#Ordovk@Ytc1s3W z&^6x@RiSs9Yj8{}|NH2S*G!NcrmEJ3{pzn$=XZ8UH*;iIV>Rt>L3CJbDen8z+haeN z&LWQC9?-1}nU$RgFWF;2_LR5RK3+~(zU`R{1rLHjnQ@}RgIOo{&jOvaL0+Zxu8e-A z4a-w<9^f$Ths7v42{^okK0Ii(hlt{F0bCHwcpe#w1-!le#pE`wbH>r6OS}6gvC;s; zV?eMm?|MuIlIpVwwsTvghd@`r4X-8h@70tNf6pJk7qGX}6*n0{<$x4x7d5mGbZAf2 zM|A949+S$H^bpJ<(qyFu8d@{f5C&2T+}LCRLj#dXnH5>1u8R4x!ABOVm+p;z>mRd) z_1n0+?E34#x0fOz$AOJ^CuGe6cutu=w&QD!z(E?GGzccc+_|l|djQraM_yHay-~&e z!M z-nTV`a>sFX40^~%{r32*EcMK-O&N!(_68aDs-9ys$H=I=Irk%Q>H`&l_Byybc^^n{d=(;1`NqW8|Ai8KXWjSUZ zrH6lPKR5MASwyP!=Ki;v6#YAnHNpzW-tqxydW#_6mYpdun|Fed@XEPE_4{`}HS<1EZ9>#pBf;OFNP5dJP~Ec4ZWjzHuP0V_1~N&z zsE65DUkRqM(KxDXezH-Oc3o&eaZO%;#!FuacDF$yv&?{(Zb*w=IEa+azX4QyfgQuk zLp&LZVV51-S~K<9 zsu!8uk8U3Dv-&!X-))yJXyg=@mDR5r_!BfI<8|69)pBNVstm5Wx5q$JxH`K**2nM+ zH$tDTN_D*HRmg|dx{)BNUSBbvcTI-=K4a3a@lR0pV4I3YSl`(9WxSF54^b7-XQ9QC z+O&tiAQ6QYlo4OeH@uRwzvCL(J{)?ItkeBAyx&9#0wk*bCVKId&5jMfkKJCwb)zf- zC(&U_S5t}8({#`1Tw}IFW=cY8&(s}|?ykgmk1s|kk)Q&^-a0OxjfV_48l_a7mXfpE zyyt!dS(w+PGBsbx%|m)G>75*GIID8g5vVM>L~v$pzly(0yZBL2+f>EZ=J0 zlAT@L<7dg;CJCi-*kI7hrY|2#CfklOObCNCzf(vm4S*4Wa54J)-)Z38IM^wuksl9! zfNt_4k~#xx0NHHLR~S84@a&7TR@`5*HFCdy?9XYZyLcILG_r#d-OTa&C!@RnD(Gim zpW^jv&aZ}`qCl@Xv;*=+h6Cl_QT?!Ie6JNm&k`+L+6ip~oNhoI6NdA%Pk>cFG|G57 zjV3@(vSt^}Chq2j-Ju=-x`Bjq)`o*I%jU!rAT5G^-QoD1rd6}CC-QP7Ss?wA)2^+d zXEi10(yosD^UgdPcA{41rncq)CR00O7nc+@T}=XY%&$;L3s_NR)dna!39kUTO*}7Q*@EVDm6}po zuAe31`e9C)+3su@bJ_j^uLpS~p#C(WauizGw707`K*tKz zYs0@_PEfmM^Knyn(T9@Rc28oa{JRXOj zg^@{fL*plU8ET4l{cQ34b1X|uB^lQq4w?2XeWE?gmLm9n7#x5dKSM5p$|7?L;{szWu!Z1$zyJm z0{~5BsM?DI**zFYscpUNQJ&gIfA5u5#O=nEI~mC%3#OgAVr-egpgDp(msqkjCBddk zU8tQS9M^dN>msPe60~p$yJGzQ?984+J7=(x%!z+ri}@%@|=37bX~rU2q4#DI8EGXi=o=idpUdfX$FX z$+2cH^!&pziAMg(f7R{npVYUfhEOz%TVTUcRF&o^%opw9>vE9%uL7R$X>p2_ST;~XaIINz`a%7AW$T} ztPKCdeobpS26iR~l-w@tbJOfi?A|~8d_SR$kQ4#q#ycXcVIWBCXsu?a-BTFe;@kP~ z#E`}i%Fu!n73t4FQf<05JQV_ARhH=0Vszb{q0sQ1`%uMPAI6(@!;=IK_qmM4_r{r< zYHTsaGOXKD=Iq$iUh)*|goECD(gS0f!nDR3@(mIOCH{myv~u!);eZt5$qW275nK(~ z76`v#qP(iqLlAnY&PuH$^sMb!lud^%T|rLHCHFAruWp6Jzga<~O_Cd%!ufa-wQP$5 zzl5pp#J+cse0S%37IL_&2fl1onJNaCs%#FjZ8&6Gd*EXKb-sxtwM^f+qG3c4*Kegv zsHMlUB35Oa*2|?sDQUtguZg{`3v0AFgtmiz2SkmwnSc(_=s^BE6?Q!3xUMUsrq!$h zpSy0X(fZN%_J=<`I0iGO zQciT|1_PP4OY=nujM7e0fF$6h7e`zu+#^UjIslQ&!00^ko-VmvQOkOT1YT|4f^xIz z>@q^52#?f=hQMzchjbxK7*s5HZQ8?_4$8+2rOsJ9kXP~C5KkCTQPp^jD#5!Y*BkBE z-su-^24H^wAEoQ7U##c^2Wuj7i`$1BnF=~{{AL$(ygx3(gQ ziHcSP2U@LYCvMhXHb!M3Jvg2QDf*s83Gw>gmavnlSw6^HzDe@tdcy@MfR~xFbv*yh z^`3q9J<0BQf6Lqb0=p6FT}kL4V?6C|#-PVKOH@c};I}3^zCG$V47pZz56&mh39+@! zL=SyVf0l^2`x#g*PRocx8in^-TZAX;hXuZgU#Wc}P5u!G^25~=i$)cBy$$SGQOd^D z1LX{IMP?Imeje6L5018e|XOA#>q(-A?493IPjgl*{AqOpD~In*jRq&xyG zk%@j-CcK9&pM2wue&1>L4?e8ObLE2D*0? z0%@1U?62gC^aI+?!5g_j>7VExQEzq{TIGT()jVvka^%V>mJKV42#L$%loz1eRkEl1 zL;8NI03$y6J9JOtwYEYEzT;-|h0iUix{x~0m4}mmHaayFd2Gd21&{t%1*4+}=qi>2 z)_Q?_D3CT&WP>9woR|(%423oeJEi6%I@>tjVF)su8FN^CZ2l1kM_$zB=L6D=aN~1f z+^FAMo5DN%OvD4RmX{q)z{3kua&u$Up6nUtPg80&e<(CFI-UOol|X90SO`(3p@W49 z5A>7%7{ai;ZW9uh$(2A3(3*O)f%g+a^aX!r23wx}fcEq+Q2vIV9_$S6L8bB8b3|w} z5D)zdZB>~6LQG6!WPF8i2!fR&S@lCBRuM#46baUj9u~(4OJbaLVw!bHc4^W}XiauA zxQvu!H-k~K2IOi?o*SpN3MCQiply1-8kAo*DCc8(dSGY|Eiv8Rm{ODKb6g^3!K8os zBl-mAq`D8CXvaogp*4WjbW)`(zChcI`a2?P-Rd5qf4-F9Q<#R)kZ}QFlF>^^?L#l? z$0QrT6uU?ghLB|!Fvo_al&eH8O5`(CMip6luTA1TQ5fW#^72v?lPe)gk)py-rfzF6 zT1gk(5Di^Rq)K=vVijfR>A+Jrfwnxy-|wS+AMu}?r4NZ{?D8q4zS=-b;6sTPAZ5by zBV3ekUb=ixB!&9FP)h>@6aWAS2mk;8K>!wxRf3+A>U%+d`)?CR5dQXTa`t6Sj2lQ( z8c2%^wv*Tnr4JHb!6}s1d5~906DXVW$~k(ybI<37{6qbjR^YTns`!aY{Z}d>`arEz z33c}3M79$-G;(%lcE6dO`DS+S*Ox#24B#wE299AgO2b(LeRx-?=c0HI?$sug6NWB--Kr+@ z39iO@!}Ur{dzR}koJysO_ry0M=SV-dKZrcUD$4K9wn`$fv4vC4&HJ9^ zlnE3eknftV%@7Uni&aVS$L4)uemNy7L9RMJWw_j#zm6G>2J~w8^J*AnIC%h?!I*bz zo++A1zQjL#YR+B3ge zv+R=eI99Mqhh=wD=eVs5?{Iv9yA1JmLx#iIHeNyb98e7ofi)Ga$#DuvhV1|A2Zm$2 zC$w!0bYzktlv32kshj5H*ELxsqlL|iBDGC_Pc=7H%OS}YBo!z5DmaEivvV`ImKjdJ zs^6w4iR#63Lb@zOCr>SBsPN`~?6cN|#aAxhEH2oHbjV0p1cMI!( z!kh3su}Ke8D!o#mrr#%=l|p(6gY*vf(Ob>padnGG3PDqsiaPmC($0~l(QIUf9zn}& zA@m(-8U|?WA`I{wPSD5$*}zG>O>6*fKc3%U|VrXM4*JUmjzYg_1jK*1h; z5G166JxyN};2DMZoIW7G(>Lf3oX4M7r2y~Z1x);n3jPg}$xy(n=*2r^6(aN1-3tbgWHIPQzZ>PQ#Dv1 zjUXFTAs1NY@fMW#5LIrB>@*6O{^Ah|uMg8#`u_t^O9KQH000OG0000%0MY{>(K-|W z05mKB03nlMcOHK(V{Bn_bIn=_d{oudKPQ>Ydzrj!14IS^M+FR7l`2bu5fXw4BmpxC zG@#N)@{){9X3|+$Y}ML|cC%uoi(0p~mM+p_D-#ea+C^Kt*?qT*TbHla?yJrBKli=a zk}=Zn`+mQEKxK!pYEXq&zIhU5<12UH9kXTX3Lp=Y0i}9ERE0hP!%td z!D4Ba2!nHUu9jU(b*|C4);emm4_Db`Lc3>&dcR< zh0Ls!W|e<5P0}=LyjtT6J=Dmvb#9T*i=UQ5bbhtzVd<&D&84g>~wvZW%Suv(F)>*@5A{1X2*%J;$%%RQE$Vk+R#kzvA zxCKHcFQ)eHTbqcFTH$zb(2PegS>E5Xv1ilPo*i4-djp-DdO+57g}K{o44L7P#y~t8 z439K3m9|B~vA7wIZ!tp&OXq`3i`KQTU)z7*)wiRky>IKL-iRA_H;?6>%b1IlhTKm_pZ|~g^=-k$hscK>>+uXb9;@2#Dk&6ZgU(&#ev{R*o-Hl5a5E`)z#B2IDMuCXOxAl z_?}2~S6^_>`)+wT$HW&%-hH(SaEPYOOmN7F6%}b|wpa?LI z?!#xh{W&L>Vv(8#oo77j_^SM;!}I_F!bd1_jI(b%WuWGK=V$wR)6Ofb!FcoZ8S!;# zAZ`xs!ajAH#_!Vj-5S4#sr%FvK4nl{J)?vEok%zpj#IoMzWv~TP=Hgkl8AqK&41EP zDhTfV|8FQI=X?a~aBu{vZfXTl8Mm-nh{|JDc&NiNhkC8oCaf3&snP*9l3ZhdZ>OD- za1^%8&#ZLBgxN|*b1>4_xv72cpf&ES6(*uVWX{~9Q4M0|u+<+8 zOhKHp<6>M+C(c#2cuO(uX#3OMt)MbT7 z;-gt7SwpEQ-T-^2>RekSAuO2Y<|v+H&@(ejfym%4EACXB9K)&#RF!{LWK$wOo`?ep zmN|yyf?znuDdEhb#?>0xdW0{*7T~En5tk@$gNcwCxBAnTI4i%Oa@AIr3#%)aK8{0ib%8eCoZ}R} znPyk#J;5V$TaYN^>RDnBoO@ekW+^@Aj>PO6UU4LrJ-IeII4XbFzQIA@f6;m8p3Bsb zHR|B4_@f5j$87Ln>3y6(flKcU zY8Z4I-EPqP=zxDgchCV`NoF8k^a>9G2+T(ex|8lQ=!107phxIYU}_ZkxM5uKytvcg z`}vbdHZmK_OvBzYai0Fr5N4m!_yL2Da?+q*&@T<1;9~|K=LZoyFJBE%GCJDVt~2-q z!}hqUY@zDtJ=;$tc{TNA;M ziku4J=8xJn%pZ^V4gMT|UYf^{Vg17-=#$@%pQgaK~bb{tE_wk)JU5OF#>Ki=ISzOl@yf1;QH2&czTTyVhhc$!TAf<|_t& zRm|}N;W0U`46%GC))9Ga)n$ z{>>rFR4@t0f&iF5k!BcZK*|wzk!bKrr>MDYAq@I0y=d?+`Bw)2ngRI=(Yis>M?Qi_$yF>_XHRv4g&&Fvz_2O8w z`nNQzYw%zBZ^#9C5^d+Y^p$nNOnLY`q$E_i(wyQ0?K9&}xWN7*Xm-9wXl{gdrG|e_ z>Pc;ya#@5W^WVj?^I|xQJPSH~qtVD7`?)Je=IiQ9 zgMg*pC)re(YR)m0qS1qC16Adarwk`gk5Mz$W9^Nrm(Vs8tgss7UV+lLJ2zzAXaVDT zJRNpA=A1V{;kewwS5{BoI(;VZ`5S-#&mNU>b1obaD=f()PG060&3p@+X>rkcieUyi zQ@*J5#H_e;qe0wcT~~AH)EPzbhyrUxbKiSBdQo>-3j_u4?uA)|`@q1T!K$GH+Q0xK2PyEE#`>aP_D3 zfN}0R%~R;}_;o7%d`L9Ia?Q-@rej-atYX%T!gF>iNjod^hIWtb8VW{ZnQs!ZU*f*Z zT+T~X*2YX5(Ya0K*Hw~YX5Xd>1&inHaESySF_8#bsQ*b_zW0(>BK zrvj4iW%B}n6pA1Z6%B?WF?nvm27$p*ODdO!en&*U&yn6{R8yyCifJTsU6Qb*W|yG5 zK5CAPsR!WrDM3Hax5NLlZK9tW;b(?oQ(`m)>ut8Ms+5S$vK^!*n{9uX4aNwDcSm-?f2;BsWBbfupHAmu zu-1KX^}TsM4drXA`PFSRpd*w~7KJRco@hn!Kchfzff4`#t z0LFMJqhF4>d+9@H4`H;O+~ktknp&=_KSl+|sBnT@_p41GM(e>R(fL$H7tlx0tFg)H zqx3QLTg!4K2CJS3QlNSwN}*zOpTlT`G?L$QR%S7ptNsjPoiQU$G2tj@PLq*+y_ zSyiT4RXVJsC;GW?%3=CA*1(htu_EGbK0)q*3DUZ1lB6G}Vy5o82x72rWV>r7f}zb zQS$r2eK9SePtbq;O2W#4(64{U5$n@RtcM-3p1`~&|J9&o zg1j}gM`>0~{ZX1-<8vLQIW@kbqr^2QsA{0LZh}rbN^@)GxQ~(##Pc$eFQHnC=1~`&L)}yl|C~pglvW)!x3pHv(poJ`Yqcz`)v~l!%N(twC#Z90>9;IL zzmxcRgdTr&g22LVp;=n<0I~P<<21hjD63SX1#0vdm7k!612sHBXB;DcMy)a>LNCpy zKB}fIN_?B)Qb+s=1a?t+%OB%S>TEoyT4T;9b= zT2fQ%Lm-}mQ8i4tG)b^GMDisG3rVVzroLrCC4GP4E~-004Fe~r5z%z6_q-%6!(p%T zo{!FgBwgTLj!u$ROwh`chiG+^Yi8;ubZkZ!c$@8=BFXBL_d^8_jZ+Mnz*fHnxFH$< zoVQ`+Qkq4V!K0TWqwb(OdJU43Nlmm9kv9n64`J^pb`K-ljvxgE(-@Y0pQF#iC<$5t zYd?Rk{CPNyfW!0!`XadNK;;wkB^c2IZ+;m*E>s4F8(yMujlROI8m(GMUsXnDx)MKM zqbD64_fw%A2hjJzB(-d<5yW1UNp-e2Ltrz8emI>jazpIvN)+jRgT9HK8D<6YWt+{c za4*!7|9r59d$_5{adVUV1g(MT*A9Sj>jZzb_4wRydy~s?wm7;;6PNq6B&|z1ygk)f zFHXO>si?A=9@3k18Fei86t5^LUQy~R^65$H99Ujla2H*6j5Z``PBf>sT9yC$gn zWL3$W;{E1|lB!bmSz1*(n|j8I58gormOT3p-bVA(oVB79?B>>DuBzlXZFW<=PcMI* zQ=Ftr4o%*UrCHwIBn5m$kCE;xN>X3_W3;_KN&SbYuSpYzDR6BCd_==nd0(9eRN4d$ zoNOx3f1oA@`pQpAk}iW)UqE!>lh1&)U*I#pTqS_p3}rq=_6 zS0VJTre?YZY4Z(8G}j_h--rTx9bkXCAEAFeAbA7rrZ zu@|Wk!SR%&M_!XcBzg`a(X$a*z%BF>`Y9Dc26pxqaWnmlehv$j@iG-eZWTIDQpqG( zm1)abg$3aT1|06BR3}v#TZ{yq1>^x1UMqn6pUE5^J;t z0sQAn| zT_C?{aPrV$I6?ATOAUAo_0%KV-S4$(AybluZzDqm#0UbS&O4flq@aJqPyGa4VaE=V zL#7BVRQ2+c;Pok(_W?+fq|?CNPsefpc`)mO*pg0TE$KAYLcak(3b1=6Abr5es4&() zsRW*#ownyH5d9VyRQBYMb62?$X4{pdPp?ycN^L4WG^|?EJF3v}7S2OQbc8P+B zI&O5A!1=uh`)lxN+o%SXA^1fH^ydQn4X7Tg0GCUkUN3@R6!y3V;d3nlNbGefEHD=o zzoXydga$gB{(znfGjr*W^e1?56gIZ!u0_fJGyMg>Kmj83C=&Wn&5K5M&@iQf4MSJ;YkJhMql_O_u#S0B zhU*O&j)F}^%rO!<&#VqW`9fC))gb2b9ClY&^}~4>1f)~Q>Kj0 zI(jxMo#Ci5LLEWj_R`(-7x$LU#qz|fMs;celUZ&h9<3-)2tfWlK=+2CEf+koW_w?kb4aA`UWR}|U1LV6HIg~Uk(L+jCnwm- zvGVXvuupM2=Oks|Q!%zKW}~E^vXZ9l8h=)LSbEcTO2vx;4qSl_bP7CzM+NpV2%}vf zg8c$r@JLOm6@eTcSFml_)i^b_l|Gp>%#?Hlu3=W-I&LVa?y_eDUShlpFAKban*y&g zc#UbV;|+l~@s_~bct^#%0`K8{fe-MZijM?7#wP-wGWTcrT*VgxU*anjw*3?tV zt-yDmH5 zr$Qzjse5vO=7xgaidVJrC0p6@)nUGO>(kO3)j5EmHB`b!^o(5Dm_adlOt5Y%rJyr> z@A177h4PbNoo5Fm1+C#qbE8ZVxqr6KaA{6b#%y9jd%Lx^Wf?Q6pSM)%6e@6SN`IQtlgr*5 zVsF;|{46~<#zER)beUm&ee(1x1EMxN3Dt@{cq&1!$8aqX`(%ISluntok~ zl2kYC&Z7#ow6;X{&qIlH%zvXQ(m9XnNONc&p-6MhJZd5fsQsCEs?bBQmL!1z`Y;2w z5{+bW5QhPO$2JuDr@2aJWI?%%8sFyKJ5Z-0zo9CRx;v+%py>j~tsVS%21 zqE_e8IEN$q^Vm3t9wI1A3=WzWv1zzt5u4{wN6VJmzhEn^+wypb_a5FDf&KTTha zr!j;W;y8l~7)AmkNaGy6XK~!341bSt{D=wsgh~8L9E-T*XD@;f$?d=q9QE^fw~)s$ ze!wxRp+eH#h126i-%X6_e{n&Ds-pLAZ1@MGv>{JGdK5g_*hg7^D#*HDU#?S4B#(!0 zS1g_g7z#$0)DZ0R`A?$XUk7l?KO3Y_57DlPXnPU-^%C_7X#WGV0i@Fc3N83v*!&p) z08TcO-liyj2Y6f66+Y)_JXwAjw&NtqR6(qlxlR6EN{#at(kdU-T>shv0Ie7b}9Ea=x&t9CV8A8k0viY&&^(L z;muxpl()#^Or2Z3G@09E(N>+e$(-#v@9TlFZJ+cLgc+3exH|Wtp3aM=@)!OKK+uf zl*d&be!p~I?cr-=?tTwno6pzr_409pJZ|*x2fXwjzDef~dZ|VDc#UtCo?E09m)3{8 zd@Fz0OA)?J#QKQralpg3>wJfFe$-1J<&Q~!=bawDOWt>T`5wO4!ylKCPY45_l!>46 z@Ifzsnm;2Oe^%%Fywt-R^*NdNfQKK{`H;?sJ^XnOe?dmS=%qe>NFGC8p3cKM zACdRNUK-#p={(}4ePVz|st9kr1KO_8q zJnP}F$@@9kUy|1SGW**)zpV3jy!0Vi za0`D|R(($dc*V<$MQ!`>K^xt6M$A}UI2ezcaVB4V!-m>z zO z2TTwDkV$XbSbNUW6)VwdZ2*ymHYRR#AY2?w?r^lH$BZ$}Y>LKus(WI=uCQ6XHx}&g zH)GXJY7k^SUD3Ufa5UJ(G$+@@#(H~PSm+NXdTYUdUq@Id&(F1BOXeIbnqlsL>kJRX zLwn2(p|Dxo*=fe(&A~`e@m8ISLc>uPfSh|xC=yDnWjed`7;+t3l6Pl&(RLuTVeRfv&p<4g2t^|`i!6_S2t}(!Ct`}u%yFhg$4v?nbz%EhsAE9Bx5dIt6D{%) zGf};*wGmSa!XjdQ#yp*Wgzl!X-Av2hRhtXOt-=nvFi{_hr8ZB?W~j|~h5F?iI)gu$ z{jv=DE$B8AoxRx{Y%k5GkS)xZvE$Y_JYYh^G`r&UsQ}?!_%p@GiYB&y4_99p>aPZ? zDIURpai)ITdV`42wt+slZg&tYfQ}wBF)k=Dp)C>YJij^Eue?a-AM5-Rgv>Z0GpL+# z{9cnS`l4K@;!X7Rr!?(`b9PBsPEV~|KhWK6#>}o(H6p@gP{|b9=*n`IpMvy21jpsxY?k(AT!G4+@nkm}~ib!0PzM^zI8}G^{%$ygG4!|uGs^**f`pwRS*`>^w z7wk+71jDMW_gQL(M#B~if-!$?J7;>WODu`0lXhoM)!Bm$+Cn{%U}7K!vWwq^);O<5 z%*V|{!#?;$LIPon8S4wh;}+;@;uG$8qANO(NI8e1wILdR>kB3l3K^VXBuY%~?*M{j zXlF|-Dk(ha67b1>rlRo^YEr?cdK)7k8yo0{{xacUf@QwCXkTA20*5v*DH^lgSm#%v zhfsV+D1x#EOgl;!0khrFcuP>soo8W*N;~7w0~7W5fGRg&m(E_W8#CcIMZ3qFT4z73 zp#TnVGm?mZ4W>{tD=o-~s~1v&gho}DO6RGn3h4eAu`ZsrZTxh z?e7%gSa4wy$ES^F#7?bCbCX(Ael*qv?|!B8ui(aR;A8ulJ+Dfp8Envx4EhRv)u1=%O@kif-+=xJ72mSxw+4NV9x&)2 zecGVU&}R+0kM1}4cl>*u{~+%_8vG~zv%!DiKch}MhDnwPxxX6xH~u?#%@oDpfABv6 zAxB?-Z1BH$hC#2=uMGMjH`5l8tp*xM#6pcY8cNvC4AyZ+0RwtH-i67K7Lvw(F<`feb<*3$>uZ8HDNum7!5Emm@Wnq;F=FcpF{iqZJ{*rh}BX@U|Zdv}CEdE`cy?s#>VvbcSuw}r)t{OvIqn&DKYsH0qIjRI3v$S>EX)?byi_cV5 z3D^)FNKoKJGw0aFp{~^#TD{g_Xd49i%F;lD$`;DpE>FMv{iPLIZ`A}A4c z?Q}!is5R=^CPODqQf+o3fY+D_5`tg%49IjcyVo{40cL!!HO(c&(Hm+(?U+pW!HT6mnL5UT0qumlN8 z&7~)Pmy^uib{Uj=_gs~KPgZi;+8c}RwNBs@vyUptWT!eB6H>88B33Sy zL+u!`Gp<#pl;*rhafjlT@Dmcz+P1pJ#w5Da@E!nt2bB#1c7cqyB9%_W?MZ5%tP;j+8sOt^0$coNUta%`H9F(NKB0 zxz9pe=uQ&P)+kdxcvry{>BJUGj&9GR-rhOI5CTuT*DnHp61xZbyMn^5jt&d4++8+8 zI!hPHEnx;EN={X3%}+!(rZ4x3OB-_s3Qq7nqF_KG_L@~%cPyvYL-B^b{=}f%f~)MV ze*PFocK7jwN=^Ehyi$(Ir{>hu@m~ix?%1U>2 z(Qp{u))il#Db}&`ZE23H$2_^H++bZl7QlDLijW_Q*C&q^&}Fa-emEH#tqVq?5mfzQ zD;lSk=D1B$DK#$o6UH;upS~K@_Xb0W4HCr@RhVRd~eY#=Y&2B1h&{m6Q+}oE7zp>u?!~ZUoK*| zwWWSa&KRgs0p1kdi{u*=s7~&YIVa~Hp5%!W@Se$6U2ibf2FEjjTgu6tVdY1~DL2Wc zo)Xge&*T>I5ue>6;nGA>Exrk=x$=V2VWZ9i|>zT ze3#<;6ZFZ{_ot{(Zq(2&luI@BzK`x#@6XYH19%r+pkLqR<58abP9M_O>-zfCs7T3 z0V8D=P5L4|M5J266RVbRrKy(i-$Qwd@`~~yGMdZ2Ncm_?XsH~ci2)~n zo|6JDbb5TQ5t`gy=5zU+73ITJFhqqD#azKoWOp28X@<}bs{uh3U5#`!bo z%fracKIafk3Ah|9-R_k-n4fxpJjL#R1Ef0-lGCx$Q|vham6lfw)3mb6VVYhh3ydN1 zmHS-7G^4B>oinleAk_yv#k%_*D)70UAp`Ru>Fj{Zxzc^5K3c4Qj7}P%Iqf4fw|$uW zh4Y4JKDIllZ~+=aR5DB_KVIy$YUPE68JvX?#ioO9y z*Xf&>xtcuh&;*?#%=wPf_$@Mjc$DUnN2g+)igfyxdOoiv5bHGSEg6{g20Sy5h`l07@{(J3Yz5^Q--MmJ}RCG3y)AG z3{=%FrmY^P#R0d^Jw!_ay1bV9^g{uU)$%+ZaKf?klGa=fVwFc|g&1^yWpeLTnKMp7 zuei?Y)F>Zkg}TQV5wzj?^SQh0|M}IEA%gihhKrnxQd$SYOLOm_19s| zeyq5T60q-Hx`77iM!JJO0NdQ8EZqvL&ZF(hf=;YnMlY$@I2%ClZF(8j8briBOW#p2 zFp{$Vh#hOv5MGJkv9YeK_qXsSP_0 zjy`i(?URQ0yYRdlcD@IZd@pT4+G#|pNeVL$c=2O}jo=|A%qArQk|Vt0C-hTr{4?|# zsh*$P;!Py&ZHeE1U+DD9HLY@M8Zrb zwvLp%9rSD4cpWyL%}37pjlwgL(?h_f-Eh?m*zw3sww*VB?o?<;bVFg&5o&H4p_Xls)V2jHxw`lp<95=Jsm+k8)3Zw3MhpNmLYYnf*S4QiP??d0!aAi^LS@8J+sPFdxc?YP@q(9Ifp`KoV%Aeqf zZrTdkf5xb|{SEXNC|Tn0YWgev4T_yam(t(qAK-m|BU0BtvDSe-*U-P{-=HGKSVH|6`XhA}mjBlOh!ek4OIbKK3$xIe+(3^Jbje-SXqFcqD1lHB z#Ha&n=h8bWmebKHV?VdOcoI3@B0r*ahSE$C7LPL7;rbFb61b?Ve1@Ed5qupe)x?iF z56HJfTVa>36mJ_SJ>{NAz4Y{tj zXc8=+X?FRU(GFHjP%zOdNOC*rN60*cW_NSNGuFol^&t3qTPnn)kFAs{u-IMfx|imE z`JBb>rO5mG5QPqqQR&kkrt>t~aitqk_mj%BiL1aK(Q720nGc7X3}a1!DW*dfKZIHh zA!@X13Ylz|jm(Mjsi37C3Dx3z|<$KRC?NznY2rIIDMnFr4MI!ax6mcFWA4J?f*`&Q;XOQoig+Rw^JH4a1r*>y=(z}BFon+LsdPS1 zqs!PwSFxY2;hA(T&!U^qzJ+JgtvrYB;JI`U&!bQBeEKpkP`2!c)pt-8D8H>yksgxf)rNWyLxre~l zlS;Y=z}?-p)f>p;8O6S-`WgQsI`!#19hH}kLLY882YrHk&dfrtj`(&>^3et3hA zXV@5d1~!qXD=NJF2wm}cx^jrFYAP>${}5d*r%~&m=9MYD5Y>CBl76axwZ!JzfR<;f zFxKQJe4FqSkX4|qrd-9+QoOEdu6UXjIo8guK)#z-ru?pA_EI<=N}H9=V(0DTa@>EV z1F`l~N&ok!a7H02S74(`Fi}O5xf-Fim=^I87-1m^lZ@%ZcDvppuaT zY?kp{m{nM>NvXU>RY$CU)H{J3Z&RVp^LX~_Afn0t^k8Gklj@K|bo~hJZ$~z{R%)8- z#B(2}>m{j}(z-!Pxf=y3arTg)_`ngmNsbzB`S>6ZMPlM+c>i-FbPGb~L+w8IFx@&# z9}ehcl``ozpFT_Yay! z-sN~-PFJe8GkuigQz?(v(Il=VAFqeU*5feJKYV6ml))(CwD?mCie8VnwB+7%+6l!O=g# z8CwB72hxS8D!q9Jxp@~A@NSyLX9M@op#^+yMp`dPNnFBz%a96LwU$FOlGf*{%E^Hu zdm68Ri&~Nzq`gIMx}-Df2c)t9$r@f`>)|ZBR`P;mrJOai!#Sy1f_YO^y(y|*P_=Ffyl^$^rohW<)lESv zQDe__e3~sTM!?1%x725xdp`?m+^PNC_I?`NSaREXx>K3MfdgDItm#D(wf=h)4*VG9 z{TH*@z@7vNSE58q=)-)HFs5if@D0~UW6!b>5%9Kw%S|HmR; z5%9o_UXaU^s%aT&-nLX-6MrCOHBB)xW!W?pQ^4Vi^AnRZQ>#l0Q}e6SbGfP2g~j>o z>_q{Qnd|aRIaQXmQfh%5Xr*xhof%y-Em^ac<+7~^=(;>VcWElK*s$s<8FI0#ESZWi ztyfsXb))L3$JMezE_$kleqAY8ld3_ZZrm0SJg;i1bwR+f*lz9Jvwx9g0sf3$B(L2w zs;11^mAqms%K5Uwa5>jy*-&}zE&8o>m69Bq(T!5dMV7i{$knQ1q%O#)(sTNUM8h2I{)09if zq*_u;OTeJ3WGV&QP_5gk+|J*mAIRUfxNzI9rUeL;o)#E2b1sO`GOy$k6@-HP4El_m~V9blWH>yhw$Ef*C-!Y^3om-rPilalaj zo{i%-5`N3l?|<-`fHNPy^4Z6E5xD8=FRB=?b5fyl zO`MBT-9=S1YHK$%{gy+~J7nENK9}dNxoc^`t8ib8TjTHtJjCQ8HnO)$5A5lFX{Ydd zV=b$FuQI1hd2lGLC}8vhoqeywxRY7>b|xoUUI4osExQMadYOySo46Q`wBTTp=q&3p z0TWGmO@CQ3Q~^g@AL=F_(#|>c5KEs}$YitIIFJ0FCgnDetaDEm2;k}c>Daf+g}7C? zjm{q%;Z_&4t3}x&cY)Z|G?Nf4deMThth;hBmTkFR@m3wbxw5!!=(o5vI^1^9%YeWa zm5sSIcG&_u@zHMD`R)FCD3)y6U~o4 zJdCpt@G+XTAwlzx@0gDw!rhPL2sc3biu8|~42_S{Y=v}u^zDwKUEN8&(jB&U(_!n}(B1qSZK6E*nj2;}0) zI)8$*@zF#b;yM2oLM!~My^in}I#%kCXx3RnSEQSUK0ggL^wjadxxlt=WS8!NUAm5x zY#If((7VzX=nK|yaI=wHKY}z4Q(iGbJ%YnTYKAD>K+?%^+Qr<+@eU?2MH#izoAq%b zxs9xBTqMaywiVJpOFU&rJ4*}%$d80eB!2}-ldc($3zKHd>2RC?`tRXT4TtM^aJHF? z3xCvw--H{cFYpirJ?+4YyKWlrh8-w^BQa2h_aJ5*cx`-#cmQ4}JGLB)^xZ>$jshN; zO;WUhEex*s3DnU#j`f_ZA-b8{!q7_O1nt$y`;O=1^n^Z6{+jeXM&krM@YCp_)PIL4 z@(GH~_#P$-f*8OYE>rvtqUckYC)*PwFJRHhW~_mJ3`-9BWs-vs@*>4)<15-jeVr`1 zwQS16Jf z_dn>QCltRAytvPiCHw3ro?^KqZ-33H3xgCqxtSdFU#nrH8T}At49YP;`AL*v59Ji0 z9Gbh;-$2lhr|}HM2;d-Aonn&ca4{C2gQXq9e-ROJjp5K+#DnuZxnbhYCn5wW`3geu zH_^74h>SL7zD?dWubd)dR7OrsrM8d5L-+RpzDmKKqTo-{7Cl27cFh5N$R3T;0DK;W z#s(3Fu1@-2bc$2KN1gJdYmw4CgZBRcv$4z`1r0!7MVMs+003DD0Bv1oI9yv7X7oNr zi&3IS^ftOEi5k&`3?anmT^NZnL^t|TBHCagL`y^qA$lizZuAl@2$RGmL40$4x%WQ4 z=R4=eIcx2At-b&3=Q(@tv)-3L6kl}94E%|Mq7u_ROefU9y@wJh^`y?>nUn(&7*UeA zq9r17&|0BMTVa^=CN+bzNO+oru8?%7fbC`ihg0w}+5UBfF9PZVK0d*(gWk-aeGzZS zI{A6JdWAs0O^bR(Lb%hK*haIEV;x}`+r}gM%^|Z-Wbma%Xoi0Hkeig76eGgY36oCK zi>gbEc;5K+JK>Q7_STl40~dddmvl|&rL@EGfZBL(x}icnuArP zOjg1c{s(i@BO?#2Lbi`J2T$&qxz=ml#nH?PH|4zZwZ!d^qpWvn3)1^+hPkH=s?t%= zyDV!}`R>no^$Z-VH{$hBS6JwHeq9Igvdy6) zeca_&BmGo(5LDlVR0L;enh8sLltx!icr^#U7-w7dzxWvQvqs(T9Y6God>$5r#3Z+e zEoqD@4Jjps##{9EysCB|-ay|xR(kfV@^otWfS*LMkjjpD{P9*JoXHL@9?dJ)PT_rw`oLGs(}<4by9i0709tX6c-;@Z+{iK*}?UTaOH?D zPL095%bcO*@dglXNYbjbztzTz-k>K70bR;tn+Xk8Y!8VJq_h)0HDdgxXU15o7ZX`lAaz+QK z-)B<$?7^XZ9 z*a*+0KRAx8pIqHnLI{)^m|{Gc5EVAMUy6GIzOk-u#@$CG89NlAtThaPQyd7S#E4y1 z)$=LU%_Mc$=%f;#W`k4A2vA>*$RW$>>ycc^U0n2>4)m}e;FK=}4jSZ;HTBzgZ#S1Q zrvnYF8=Ufh;485J374FzA6Je>%2jq&x^c9vG@XgYZ~!^^sd_0+I#7(jI54F_BZWmi zgfO-v;_da}V=(w#lv)oL4tMVxsvLpCU>aqy z7O{;J$rgHo9pyLP!Zj#RNjhL}7E>GEmAcTk23_0y>^*FJ>C1@_=B3zJIbF+GUIgDm zIn=^XLGfE0=dZU>$VK7h%0RZkmic7l{*l4@eD7=I51c3GVrRkOPoIR|L&@V)<>Ro+ zmp|b`e+Bm?(|tRl|HXc|N}PNd@i7^f@YgXz=3@#o?it zBR_Z-D@D$}u7IkD9i(7Ir66;k{2K4FaW2C4z3!0+=jz7|zFI zp3K|_B}MsBl^NC14~sA`2Qzqcp?t?;2BPO%Bx(6M0Cp z15Jz$YWhi9EOvX>Cx~S_de~@XZ+cgX zz4P0E*qw&rEPL$$`LI)xq)csn{`zWdU4>)423OtTIe~kK@Qj5*Hz8mcQ+V2w7g8D0krlL8 zOj#!cyy$WK^tL6XpWJVvk0?p}G+?43GnV*$aOS&4*=ED_?cow-RyQ;rUJ=H;!JX+6 z8P{23C3aorq!+oxu@WqHJj?ytqyB*YZ4(vB zdfX%-Lqumh5BFRsqdqdvmKmnLrxFYvu`~W!?VRhCyTLtZpv$>qehE?B+d8NjU%sj* z;8R#U704Bp(~;h5=e4dgK`yz4nm~M%SaFHO{}n%EL>EgX`0;&o?2!bN90h zxj)zD5X`V>@3B~t{~#N7h4*o3!nN;%fwZI!r6_kdY9H3ccBE#oVGsT|G2z=0p-VzD zR4pd$HsU0OpKe8fRn@-kA>=e(;p%Fy2#&$=c5`;BZj{&?9Y?($LyrV2#7RQ;r|WPb z4eg>CXz0kC?I%h1C0nUgi=k2stxP4`aS@}T%5|S3SZHUg+~ARDsI~?Fvs7ja{i*r3 zQk2KQkqW0%yOqNUAqpG1H1>4MTJb=i$Fn1J%FOVv2@?eWmI)+B&t`(fq!3^@Zg;l29oI8aZrMJc{HXqJCze)>g=;?*so zjnMK-uH&_C*O0}-Dvq*EpZkP7MRgOE%J{_0Ox5*_eReH&-7o@<@2U8RD_WiXP`N=H zjMf!Y6HyIbi2JS7$EN1q+J1!euw7lzl(nZ%#obJ^82i>;vn;pJHGDI$qG#^@CNnKB z6=WMbU$JC#Z4C=M3e#^kmFd6VID&TW1aNpjzgBtMmx#B2hb2j+01SunTi z{!nNS34c7c`2%Yomu9Kq^kG}<7Yc(h>e5+|ozOcP43QfigjavrJ0Re09#<}QdcNW3 zmS0>nW&5+|O-V<7-E9SJD$_Fkmk^YmDX_YGf z*y)xke_>E?%eZozTm@`A@8*4y(@2gkuSK!2taMQ;x(W1`GqGZ)acl)%Dh=T_UYGx1<{ls8=W?^L6Ov? zFC^UPo30rQfNA5r{RTytV>r1Cn5S-(XLmD6TiP3g>Xe5tVrZu!%tDE1or?v$)@h~| zbE`QXROe1=G22C&(>MpQwwt&;Q>%rZc9_tRtyDl~vR2f%RLWMOMA1{yfzy(c2XN!& zo+P-mwud>h+xuKDWJDmb)2p7i4>S)d+F+kfC`G#9BJ~gt6|z1n28iQ1ObX;H4KDYDRlzVMr= z!qyT#G}US~H+o21s`kRAqWD$*j^nQSV9_&!KXC~yPT1~C&r$Bk?!u?5ZR%jjexFgj zS6PAA(iXJOzE}D3)I@9j+3!_wTo(na?_FzH#9669nqI#f{%G5AOeL56MmFn{>~rs8 zhH_#BvyM|t_jIerYcF%ZN-xq6d41d;dnF7c^VRqjI%C~-@4ks7Z&RBYhhQcLMh)_J zdu4Vr`gVvIC2Ub*1a7g0UdFxQl{bWIK%*i@rSX&@e>k~Ryh8XwPXkk@mM?7ykhzd? zGtKIV+UPWGgEx3_JKZwEH4W#gX)paoqQT({tTTh=V8HVFRiI#9 zfbD`f?cY)OCpLT;SX$R3K9{=`+h7KbDWAu9ZE&-n3tr+otH;+$NneN=xpoc`yG9Kl zSHbN6iV6}Cs9XT{tN#Z6B{K*H^f$pI=NfK+-6j*L>Bjkxb2hn&&xN|$Hkm=R+ULIG zO)>ThB3&1<>geG?Jb1k>Qov(N2*i39;IrPE5gg14j*qb^`)!f~`+Gd>{}zl85O7_HC5a~bd&&})BL{{a~b{e%Dj delta 36122 zcmY(qQ*@wR6Rn$$ZL4G3wr$(C^~UL#9otUFwrzE6+v#9`dz>-$8UKA=FUx=)eZ;1Or zd~{}NCcVW==2~aNQ2E z`CuaUA4}uu727iJ0V!-;H~aMz1lDHz%8n7{{yDokd(C1tmag30=jUCr9=ds!n{yYkX;R zPI2rG=1~{&b=dnRr>4*vu5rRTE1oe;EEQ3Y*7S{MD4s{600@@fZBfQS1yXYQe)_X9 zWF!2Qg61I8rPN`2OT}5|UzoQ!4e6snbv?A9W9mc#f+_Vt$C~0_8W~0&pvu-5ju15v z%*EJ{Ulk*jk>k%?Ewp$t)^fkm{Vf@BC;U(7c-Ud=NGDjib2RSXc9j^-tpSX%dHb}p zxQb&FHTA*)Kn7fGMtj)olHETj#8OzC_j4}mbanP4V7}JsC|uT!aZWMOW3kC|@e(dfZ~y~V z@_8>n(HBd{$_|}}BN~?jicz-kw^>awsjuDuMxTx{utSV9=zece)Ki2N8gV>72h}Ff zkMLQwu2KSk+Ay`5vuwI<4k-X`T zC3FKuw9J@?izpw9&^bqq9=a2_@FsD#Lefgmr$|E43L{e|teI*t2G?d(E`g*W zZT^~^P9kl*MJPDiCLrDc^KR2)Ag9EcT2FSIT6dkBp8$m8TSk z1*6X3q)^8tbec))-fX&3+Q1#KuiEEXA!WexaCcs{%q4bTM2Q2UjlI~m{i~-E^d2k0 zXQ>D8E&Qibix0q&5$xRqy}|gDp*ai+DJp zq8Kud1-4I?3o8x%yV{@)luLxxop@9I@|&;m7mgyG@4g~?MlXxjshX}|mlYX78cr&r z)9BsWSw2!z4K=&cDguESTuA-C{X~@itg`?7&M|8R%@j#UHEylhJc8(`dU(6mJ6b() zmuKPNGQwqRvB}hVTPiT@KE*7DU-<)P1j+Roo;AV|;oa{*@wajDmBdSDok;f2!3c%e zub;RSe5?GeMHYtl=#I}u-KL@&CZCg1gvqV zagwiuSfdRS)+p2mQE=mlgn9FdqPqk84M-$gzDjxTxgf!l23Uai|2TurDbe<}j*O?* zh_W-{8<^99kENpo9RX2@h=FPfDI|R%7`GIEU`3@FtX1?4y!i2F&dvUZE3uNarPP9y zK=cE#4{`On($c`!HF*gV8|hxU(lDHe@SyP1=;F7qXW zx?Ce#9GClVDCF{Y?gad8{44nVc7_Gw>P2=yw@_xKmBJj#CaDn~N{)l0hhT!U%2gXZ z4Le$?)JZHl!ZSJz;^4fQ>J0UB0=o}VQb7Vc3*Q@v^M(I>UX|eI8DvVW(mqmKSMj9r zk*UJ2Xx3@2%;e=BT)L^y&~I%h?lwyg@1An9UC{k>N097VEKJM!Ym%^H!^<;>L%e3E zCfng|NUtu1I5tBPbUOUnw=iap%-C!7oh(*XVeBMcOaP#l_=5ZZ%&-Bic7K^Jn;02NadkRB9_= z*iAA`t}BeEa6Te#g!b3zm<#Ji@V|SoOXf+rU|s*Pf3V+BtBXT|M`}^}?fA~ckflc; zj9sweaZ{<79Y3jTwB}FheMB%&o$T9V$!Y?mV+`VpHl_K(yA-VaVVl57Oc*5KSq%1t zIAJa{!am`;W+hV`s%ZNV>co36u{scvV@P$%_U6lw79BRCug1g>0Z1G zIs#tFh_f%rt5rV{Tj}ukVwSBt0}3mXf^-^RT0@F)pTfw@q)UK#8kt9K#qX@Xb{!uu zq*hW!C1ekWm`&h_gSKk>7xYJV*yO7hBf|-W$2Nwi+uSleLxhd;;&SX?19KGll^QGd;n6irTXNp+t?F*S zjFgG6Zy@?Ppzd(&OkXw}s=I;~7IpprzDI12Jg77$DVgfOt+X$LANKC#2vV*K7(Byz z1-qnezu~;n{(VPx3`tj$h;%O^H|x=%qYh_j1iXsTLk(^;b;|n+PRr1Jk^0qpnIL`L zSlVNL~27L|5I{gd~B_fTL>NQ=#$a_cJ^B)`LvJYWi(9FFt&Bqp?|BPW32O4fc3;5x` zI^vy}xkgXuI> zo9>CF1S_yn&0Ntr6NcE>OQo1X{Z;1bW0B-GXQDs3vAVF@xK|myujWGz417#qS!KfL5 zPpxv@3W&-=XcC!TvjWDEChH{%3i)$Mm4Sav1n0XA8&eLE!0`7RmLbz!|LdhA$!X4( zJOXA-BvKBq>&d3;4R_9Gz}*pTAg&Eg`r3?MHi zXrUI5nN&+xkdfB8lx7!U-eV}wE`J2T@)oyxGDEDXl6PRn;zj8n9*g-RzVQ@xF)5TA z<&a;@Yv)Z#TI;n-4Ow;7A<~S0{Vy2Sz>SZ+DIy99-}r^V8tpl>6Kv~=Ub9DO!K3bpK&6{au9}md1z?T~RI?^)L za7r#`(RZwV-!EBOfMvb%a8C?_uhm`)vo}WK5WV)Kft%D~7VZ)Fw*x$*`jY)JKB5ta z*L~PB(Tdv{4_YPc$VKfC96Sdw93^@ahN&}+T?zTyjTf*a)E}qGjji%d&F05T$EZpt z@{IioV}s~wtaHpx+7x_gL5*O%qvUk4v1mmosgG%wS;;alekSVVo%*|otc#7MtU`MP zvHc5*5w;6QX-F-aNLRnnP=@9`a!&SuoHp9SbU>TcVVmcsOZ8Rwycsh1%$w5>Hfhm& z3s?IcU@4{eMM8qtJQ;+q6)&Bu!e#=swv32Q(&x5X_f%#f!@o=*YolWHCxK z#08Csf2bzRVt$a%!PsOQiQzPrYS*6m~w~rk=hDS9x#05auP=EBFU|51{v^84U(b~9$k%k{kwzZ3-U+JHJcZd zc})&214p-AW2b9eZAM7O{|BecaiVnEILQXMcd}M+$6Z4=4bl1LoA<4tN_Ugzvgz>D z6cA6#4Z*ASiZ&8#86?pHjY4a!L{3}gV*WV$wZAMQA;kF5BJqV$sa^G^m&y6$7kc2j z<&doyu5A$oJ9!GpRsAL=))?%?Y^B>J8ptiU7_NT5;DVJNm)faxnJql)eDhXhfYAf} z?Hk%#I)iMR9zjJD#Jk;>w9TWFrhp(;5iP6jgQ6Q9;eS+f8)rMD@`=?WS^~D z`geXXA0py{t3OdJ;0Xo#blQ~`#9HC_tE(=< zOp%PdUOU)xac$Yfg;{j+zZ0hEp=bdHf~HxGP;QRo69)mxtJ1ShrrEUwaBE<`FXI7eEqh>)f29GMQt--aWV zMRDgX0`h_AUD0T;+onceaW4;``d#Dz_*j;0{3Bz=cY_eP&oL zC0i>sSk1 zXm{OlrxjOgLynpcS6IL<#{;e{NjIZ&qr>hKMo--kIR}=WaFxJP`eNe9E!K0Ui5_E# z`|VbhC34#q+<_;r1p`{?&V#dL$FTBZL3fOq&@2mF+VD2CTBa!R->>TNAvS-Qqah9x zGnZ~2MJ|1?VjBX#jVWfo!VMVfr8^o}wZ3o?oJ$eAL>|m4cs+$LHYJxQ3 zR}GRjX+~6ax8B-<*OPFGA%pU}vRXLJby8F1zQoz|5^Z7%P@2~)uW+*?zbg<-xEa1N-ipwKSYUQXqT^|7 zx_jj@>zki?DSgm(PK5dA2nG}iiur=B@*mDoUe1)K3CB-Ezd)NiVm0Pt91TD5pgjb_ zI;BXdy1YIeyy+=)nChIdA&MzCX9`JWG9|Ltn!U;Btx)@4Ry#~BQ1h7wHZ@R(%jVid z|3rI!LvRBL4STnv<#(Q#BYqHqWuxm{wRe~sqLPb7bTSc^&U?3VbMy0CTtT+$L2pfM z3cGx@H`Z3Tqe%x?y${Pv3Q92zewt-(=RRx1CN+Wqce*;MmV5T3@I*7lxm@w;`#;x+ z1VV@fMg{H^eQf+AIfr_kL>P2kN1#0Fbw{8JO!rRFF(|sDvpjkEVE_iW10{7yQwYAjm7NYU4}!Ce z%IUK&V$(mpjKk!4td#f|df~URHcG0WIG+Y@x90|S_al=Q`9{U zBo8r{_MBOC4LE7O%Iob7088&ribIFxS)eM_rlEFMk%Z)2UQbDykd~ul7M;tc-*GWR zZG{eD1bh4K#J{KyJcT);##pLkUN_M5%|1dms*l#BUDTGZTX-+FOiU^i5u4T6NV7iT z34&_xQ+d)`zrDabycvLmv5T0jS2zoRO*oaTuQ6?{nhYK%FRELruGtPWrw|fQe0XA( z@jedR^U1D=9)h(JsyEN^N5|#r$+Qr+aLTRd33iPt-!H!a`yo^tA}ff6}-b^&s&cZSzm{gJ?^(Wbmc3*YX=`$(p3z zwv6yrR@D0x?&Dh_TQB4tA^^a(G1RADxh#c{a zWf21R1*&M4uXbE)quQy+Qn0cgyb+1e9oWLnCe~O$q$7Rq$cylXJ$}vbyb~c7D59hE zkoU|TH`pToD-{R7PMV_uIRqTaFjl{49))Y6eY^l@1fVf!=;Q-YJL^OGBli+0WNo-8T$Sg{ekh|vk-4Y(z;F6 zpKfFB1L1?;ZUmgcQ52xFe5>#=#QlR6eNyF&S`ea2J9V(Ne*{WF)|UkT7vBg2P~+rl zc3tR_lwzzh`vsw7Wey=g%62X>v6DHL@Bo*BsiI#ks8gO$bw*CbtCS;;wv*uXtg z-eEN=)t)5=lR$ZP8KRDTO0U`YDA#2#vw2xCojm;4%Yw_|8{sLU-oN~WQ}fA|E?#&f z%HX~J`($%S^W_TV2AH!oD|XsauMt{=dwBF58po9OKgCzP7#R$J==peyCHM0LB36&i z_yOVYllun8uuVv3t#n&hAKhYi#;Ja?{8x)f5_y+D{NS9}9R@J%ir}#7O0KBo;ctD< zEt($PA=gGLMelrxFe*Uw>!b#aHntduwb z44HfONOoL6_JT8jHb`^qzBv#aB~Bo#WswdyWp)&18O1K!W>BGiHwYiny{U4=G5C1L z^>QJOu*54rF5Jio4CJ!NeahOaZ<=ExwX`75w>z%=-U94C%6bB)TEE?OJ}0E1NqsG zL>Vb)in&baxP?EMvWcstelgWZlXk*c{HcS+QSF2V?q`E%RI<(U>zT>cxWdQM3bAtv zp7`drrDKtu4f_7fc1g8}iN{=GQT;@GBSD9n^tcs6^d@QhC5vv^7K4&!3FU9E*8dvA zr2I5L(L?Nbk66K9p0$vKGQsyw7|B1x;jj8{EkL)TLK-qvGG*H16cf=MuIF0)ZxysT zVTD>3vc!h8;UKpTf()s`Y@^IC6UbVs`)^aDOk`%A2Qrr;{peH2|K$$8A=#i46a=Ia z5(I?v|6R8~xv>E?d&Na1^nmM?d1W5_I@q2-_$}BF79r#)Xoh(@?LM>cp?Gt)#$sFP z4HO_;FqARi2WjM9WAA8rUd%}gf&vFMgZ}KK|BUN3|H)&(=hGWppm++o853ziUhg{- zt%*V~i24Ai3<;(3f(Jr|*#|*-`Z`ZjwmTZo_FZ_1YVf zIJGivLkX`ozzD}?i$#5a!~I`inRiWR?w*3-(H!=WkCAerW|%GXfD}Dpf!eP{m`Bt> zHNQS`zeHf>FPrz0(UX$kin?qop3StUd}l$JE%-RrUrf&zq}Yx+cb}9fXnK&*mEz*N zT(WIOCb=E(={a3iyq4=$uldUFzw(ou^iPTGCF8t;e-i_8RVT+x zghn89Bl9zKx{X%{MOtQOtzIBz^3d+|Mlf5fZ)<_)U}IVibM2Rys4JWn%lG5@`3zqY zNMciXMr?|M$`R)S_>ynJ&t61Kk2vEt-3zOzgVA7}Ri+On82@N6h!T5voA5e)-_(RK z<6{0^^Z8tn4zg*1-`z@AE!+OPY$dKNb$AGV&l|pBE>}f8oXpN63LFo5bT@(S^H6VyPtUEWS~P+@YW;uwH_5s&@sf`B);L((~8& z2<(#Bh&$yweX*|`erx=~%E(xUd&D>cVBd5OGf6E5%(TA_Ns`!;BFhD;ef=dw`;HV; z(?>{k6!)D2XN!=fAX=prl&4v!RF=5T9w$r3RfqW2t&=9PzY+cy^9;J)gR&nWAVpvx zAYA_sb0mH#Fl2w6Mjig(9}wJhl$RCBdjdkhx59rm#n-dX)$aoRUdPx)@ z*s7YDnIt_Q`@_+i@#xlPb(28i=P>21p%gf(ydTKV39e3h=qBj`X-i8B%bqt2iw!{l z_=04Lu=K|ctVm8@Nfc2|FCnvV+YBr*)`$o%L^dZrPHLkyIbq*iy$vKD3E>g-@Xi8& zCQC>|RX61oawM%5F+^w=zh-nj&7dW7K|TRM{gO0DNV>e^e>-3hApIdM0u zB2gW^k^c%`n+dQdRBp_}QQCEUT)+a3N;#8nBCQfoD{9N&Pe|0^L)W!em4 zB2|Ic6B!Z03=!euNRS9mPm_Vf{4>Vng8A|mcd&BV*M~-j(-z4LDbc^mRJsRHi=K&e z;jnz)X>zt+*|<%(qqEQZhCi-6n0)wP-x0wIa?N87Dy2U;Me=!gJR(AfKyeTXL%>HM! zp?_I;Y?Mr5(uk-x1#1FD0DXQ)M-@T_$bO-x&rab24^&1&N^* zX?|0f`Zek*)9D-(JOoT}-uT~KOa;6>e~|`?SD#85OGGeWAwVEB@~BOX9~Fdqx67|A z{mC!*&pvC_=iM|?f*mG+Y(BpNwBZNcH=1)>kUZ(X+t=KwSXEv!2i8$~=nouJ5MHhV zi97w#|K@H$`)}B*cMp>8MbACp#AIIR1T3Qn8=*MVT))vb9!2wyvSh{CqdqIO`8KSx z?m?yI^>(O)3EN7bu;)-OAq~|t5$v^0VQZgFquaWTL|6VM|3}z2{7e!FAYb|hNO3*i zOA4*Kk)ls)Dh?@o{*{ew^+c++(E7jSxR|6)JI0e3)Z9RKU&1#Qn`otRs~$>A3E~A{ zvWRFu`a!+zb90HOCgbR3-)qg^a%8oh>+x`(@B_>mOjhf^Rxoa49Ib?|&cxGlFp7At zU-l)XcqcL&l?F2%V*$(pPNx67=V6_y)N9d&&sQy(q@RB)&XGIQwPIXL&W1buHS1;7 z%J(b_F-|b3fMp0PvHC@lOh=lP&JP7hB98vIS7aPYn~iarfYchN&?VoyApzkc-bPh! zkmCMe^8Rq@>*@d4b(CEx=SG)Q$-F2%X|~YFmS&*J8P~W`$pH~cUNx~u+?|qH$XAeX zFIetc(@dnozD24BV!AsyF)MC|zvN`ycx^a|n*;RsT#32^Tn?(!`1ft1xiW;OZVPK> zhQ@!qmRWV#X11=mszNo#N-bsc@~7u-;K6cwt&-xX&CK}L=^G?cJt53B;WA@?V11yq zMNt3MbP^mmxuYd&SZq`9$iAmWXBOEG4Yh={$|RA&{zm*?UaR3p@F9|?#bf}#Hu;lL zWap52<*l54E0X!4P&-+s&b2K#wrW{#+ieet?_|zxtNk#+zMtlNj*}F4WKzk`evjO< z-ZS1CJ3zn}s8e8SEL$Z9OS#3}kOYDv{iRkp8Ve);nRp#^h0j5#kw6MM+u#y0GXiUU7+yi8A&Mx@E9-<%C z;OnfBUoK%i@Ht)-aR>;KE`34C|MBe)!)?3a^FO~}O;63GK!Vc_RtE?;o^|Enrum+g zn*J!R=~{3QUhR0qy`NkYk>JzydWg8+Ijv@rOZKzIh_i7m8akSzglO*>(t`PGGd;oz zwGAf@rmmHG0)4L|alntPJe-_j7MIHtG>{GTJ~MKv5i#->80oAkc=;{5lAgg2pAZYu zQf);d82QWJ&QL?4wrzN5JA)J_FfVmW><8&LSraX#Das<+b16uaVT^0I$@Ladld3)o znm!9&p*3yQs0Tj}I5y;qU#B@VieIA!m%2FAO9B##Q#4nu<`plD49qy6s88x z5RTJf^AxMGMzR7Fw(z~@+7J~4!EDv_C7+$FfcBif>ZLxu14^%$4 zy-=~OY7waE(J0tdhRgSuC#liMA(5B)(wmpcl9m4X8_0&cFtJyn81XEv-+ zCqAryPQge)6osY{X5Qqwqg2k;`zy>Ed#qu59R`_N_COWw3gRrGR<;Ml}FCW?>Cu82m?U7 zd>hMJrN)%rd(s5oQZaQ7PNuP$MIpJwK)Y0pS8~+crS|;4VqEk52lsZN)C(0_cLQx< z<5N`aipemSM9uT%LmGJoOlP#{)WEda zpEbKkvFU(=%xXoPd>>mP8;i?M;e;$^Cu&Z))>NaVsNYpK8cX)oRkivp&Vcz-rTQd8 zCE9DMBdi`_`DqN4D28(5@}@>T3v!v-UVAVKitgI5zBD1}Lb)v%LGgG6QcF14-3%4G zUMe^5YybkpKn(^*Nc$w|{7Te{RX(?w23uG#2Fz9})L<$7=xM_g-f2YYd?#NWRjb^&>=yObBwT+JPe#C^!| z)U84Q#R2NS-a^5O!-EIW2)9^nbIM5mI@iKV7lf9Iks%;V_cNnhp=DB(-+Rt!*`o-%fYNM@ri)r3V>)Be=!mKz>1gA~)0 zWTDEYyGGup35-b8R^?Ug)2SRc4?Yq#5Fn#eIG05c5hDpSS>Edj&CuZrOjLb6$1K8_ z>P;;e`Gb~~DF4a&1~e{GCSFyP=l8vVX$`UbDBF&4?a#;{eFz7o#=V{=%O?rJ9cGM! z&^c2y?2xSF<-wlwKt*AQk%DxKixbP5wqmioR3_JxT(YazOTZtOMRV|GDm|Qb;OzW` zng%73v$K_}r`3s>_=PHYyd|m`6;PR(Q(AWC)i|u+i?C3VK_rCp+8Y*+ zHQ6;9x!ftg!6u@}qPe5OJPFg*%i0Zop2cv~bEy2{vj8b`86NGcSu@|I*tFZ7tb9@5 zln6cF*zXdmj@_^_!CfG!fxI5^2mLns!y5>-IFi5t1Gqq~7ZY9uPYB23jh-viAX+!9 zC;SnEKTDtw7ZWFz0ZwpG(-d$!{tH)4npfr9%@HiZDK?`t8q(70dY*kCkcYcTml11@ z{SMb7*Ti#)wWD-HXNbWdr#eodky0 zfY6q-46HX;v7xdb`j9`a1zDrOvscBSoIi=T_b1>TQHVNdguf=)aUNp6H4t~2l@Yh@ zbBOkk7_uL73|IEu2?M1H>v%r}SFI?*K zmn-K)nSrk9#|P*;Nu7__CXX&U>}N`a9hdL+RHlF`x2QMMoLFX8SxO{YcGNYy1r26^ z=r}%9RR7D1Xl2G>>AYAA+a>RE{xATnZX7J!uP86S!n8l3o92)HqRHX_&Yit!4e?G2 zkWRdl1XVH5MvF~o(lzoC(A<~c@O#QMTUkBXk@h`8+qW1)S8RGk=-05#i3LpxO|iDv zy2P_$9%j}xl1i`A5l`6$)%?hmJNy5t(#Q^W>Y8HXI6!-&0F}2IZ3d!!I?1V;J>%Z!4b&G&Lf|Ue4Vs z4Aw^whQ-7Ah!q#gLnP><|A~j7BKRohu~J1RZ>>ipY0q%_^iRzKNVu?@d55-rKy^XE zb6`A}sCe909yEAK-U$g?iYfWSgUI;!8G#C8FA{e3WWv#lF%Aaxq(j*`XUG)j;$8;1 z4XWzEZ34p&QQWKQKgnD`<+!QXHBVJ`gSJC92vgNERseD}TIPsLb$fB&y!g&wvX45g+lD9=bgnJcT%z9#qhv3-j zV{ULwxx4?hX;Wy4LAQDuX9f;OL)z-!=wlwITKRjt#1Z+=8j&$84%7bf_3Vza3haOwzZe; z$%97^y%2|QHiG~FqUp@Ii2$|h4XgdH`nTt8MI(eof7p6kGXG%dsQxSNhOKw~jy&wJ zt$?n0ma3i$vJO&Ld|A3zwfH0*q^VsYIN0(=h%eW-`?J2?&C!j+reyyZ+doS+<}^FK z?lJ4rux+IevIazAO(&3%AMjOI!?)r4D%^o6?&J{(qj;gfgw88~;_Z-41Gyqvg>Qkhgz|}^rDtzMV;{^beu=w#GLNR>K|Gjk^-S>!a;iG| z3vpr`j3caeM55Uf1G(L-8H;2@O&mZIN#da(Hq8gLttnLzeQYzn_FQ(hQaVW)w1p z;y>kylQ8UvXr{18rC4>YpI8s=xIe0mUG#z(*o=5rSTI(YuU8I)?fR12(7(fDy%5s& zG>iR^Vqc*$oj|8q;7en~qveFEUgs&q*T^i3^v_X}+}G%`kP{Kz#+KJeI7w-ch$-T4 zKdJ42-TF5XUpgu4$4as!-y z(z>CT2XGguGK^PD}pjD>m#xS%=v^J z^1xaHIHWp_8O>Gy`WEFj<8}1W_#>(n7Nb2&VjE|OJ-NRpv8gG7e|Kt+=6$aCI3R0;_{BiV5TO^vp(JTXyS4fJ7lb?w#%Su&Zv|)^?j4JS<$zGb2qbnldJ-b<&I+-XjR}({B7wI)70|kEtxhL$lR-kh9#G4@R>PGLDmcXr&&QYX{{aPZ|J;1g{mFA(}_- zT?~%9C-<@+15r=<*a}~=k;`KI*Y8XLeO4=NH&?I30Yg>+KUMGeEM41nz`Km*Yl_jg zQbq>-;o+FsuU)7OhT_!)CO5|UjBm_8LJM+8>-I1{QndGyPi|SS6Js+LVl^XI8Du`_ z?z|WuuIt6V)|0w&lMaECAuittL#L_ZJGrP)yr%Mr7Chz;@JiJ6=NnuLU6>b&Mfjl; z^EoI5tMaCIM{O}l=Dc(t@G?~4c;l|Hx}pZfIjQRa*&j2}zx$>RjWGpWuO>*-OXf5+ zf7YQL{gt%ix0}5g)(M~P&{+*m!rB`b1ibHvs}<{tnCO)y)!PBeOJN0SQJcX`6?YE9 zN}qMO4#lov?7wRf)<>{(RIyl&&aO0hBt!(Dz*9)+C^)4BE38*ab2 zg}L$!_5PxsKcOmm)|!ATnzzVdahO4DyqC&hiBK(@+8d&7jP6l;OXX8-I=Iz=8dp|h z5~3vNPl?|X2nVIj#7R=U?|OACt@T&Y{G%SHN-+~qyrXZd@fYW6zJcC@8J5r-9PeI5{R~WP))G6h_7d zA!21M@0A`(Q5+q~$|Y(({(GeOtTgK@@)gN#u+Yue<*#bTP5k*8!8$nBlyG!Ldwlzj z=g%VG>+^s-@Zq&KkS`cC?f?xfPlwBKU*rcCvwC3AEbw@i6gKIT*TPivX+f_ye}AHr z-gp}pR;ANpvDXpi4aZ4Ghwg-Ch`39;xlme1?`K*#GzW-Fs7y0sAD~Ubx4(IbGF>u` zOVM%IT#$J8t%@#im9z~En&(Q%v0t7NN%*bH!rps=ODgW*soit)i~n0Mn( zyweQ^0V#r*WDH_7>jsDH9RU_ykD-D`!ed1?N*a+dm5peQRnU|ZA2|js{SEuSZyXIMY8BSY8oJ}$EI(nG*_<~u{mp$s9zhpt)D~yu8 zOO-nI{>y%~CBT0_H2V1KVa||HZWg_U6d;3WVy$^H2;?sl{V7n`EU1b&ShDQEtMp6x zFO(B%8Bdy~kwpuNBbGle_7~bnRPy9!*hkdfZ_oK}(BqwL$3tT?<(GNH4*PvJW$cS6 z0g+7Brg-z7OYr`=F;Ala3YEXs6Sn;}CJU~RI#g`T=iI}WPARun6!^0^cE&r1kbq}B z0A+Elc^G5qdpb%*J9modgc;!!XO`yzbK1+kK5SMQEiEYD==_^PC?1HIV)Ql31_NIa z+x9x@m1`6+56-->BYYB~kN-w6h)R#|9*0sXVXA+XUG${4V)8B62<9pWjX*Ss{+s2$ zK>yl*QETC3<#fViA72(AOI}J;q+kwI#|AnjUjuz%rA3I1Ek%avmqreGyL^kjhjU}l z7lQw71*88wWf^0S+kXm)+`m%RPuq{DLRJsH7t{bZST2I(@pjIaP1l~A&Xdb6%UQq= zbeI0W%xdI|&RjUN@krSCnb}N)v+$^R*H2;Cw4n)e0!=2AezH=4?U3CspEL?dG%b}# zkOwv$^SCk`2Vs?MiY3&pRp*FMRCE5Ra=p@0!!B3S1B)yAYt9|0;T(X7qBQpo+ZB#Iyr{)v{6ba&lDC)eT%8C}7kU zH?*#f!cP)Ujxr;6?Y$ zi_a&~Ffq>g9<2)^Y&tvONv9=}td>Ri zv_M4oF`>akdA%c!i}i8AJ{|lnEI9NOyWh0G05R@?bt<@xDV|MvuZ0lv~L4 zd>WwGZiSx5!oIO@|7MRf?}@mFdz5qZbwZeb)!OcmhgE<7__>5`+9ijzWx?p<*EciC4>}7K znw)*nrno9W;*O;X2a@NK8d7fjzmdCiOIj-QAsE0YFfeP&d0H< zz5WsD4TnHp*#cki3?2x?fl?Yp`uZXG8o76A|5tiJAu7l1C41{6oBxE{@gwU3Rwt?JS>Tlt@#k8@vbdcRs@!!paHfl=ZN zn%js!DLBiNe*R02kvSC3L7L?eonBI5wMQ>`J6NmHnq1jU-k1?)R>jAZxmqN@TYI*< zl^algSS>lwExpx`4>EMeKf|z7u8|oyCp8bWN2kF)NjSkptV;y6u7NQ_?^y}ec`(+6D~F>&<>SP7w*to*_faM3aaVTc>X7(#dt9T;kF*tI%waeL zjre)Sai)ZDJeb_6PWqz=aps$5r?#^nD$@LGz6(z@;L{t=3dbU9M?=k8wfav zmK$CWN3KHbT;MBeO%$WjWH=4qbw9D=u5;&FnCB9u!bK}*sY!eJxw3Ul~u&_8V57}4_D_H+X=8h``UK9wQbwBZQE}DTidp6+qT_px3=xK_r9CFIMh3UYvn}Dk7B#j) zM@$7Kr$>E$xF*FTk}2Mlfc*}vZ4O)DAhpCOXo6|6dwY!h58JBRVV*H&+Tyc(-ByVI%myd<%f}t48z{XjI!^ zsf53X#{T%D2x)4V#JQ&#Y|jhTy2cG8bcLb90w(xtjdy+ z>}L$jH+15JVt>`?$d)NO@grTg2ntD-Q?aCS@G?$C>Ddod^9iHdqwUW(Xj~8{t~Dd0 z6a>+ThPfrI?LV_s8?rEbnKVwRb-f^t5PP{Wef{)-9OVsBhkepB+cY%|gjYOu!m-mmB&*_d6-xHpbt+b@q&6_-*gz(h+x%-M>fd5HB-02Jc^e7W! zL;(8iLwptXFv|vTceV%-r2PFaN}l%bO`8-ieoSI>Y>@G3SVgop0q}8C7?` zNR(GW7{(njV$V<%V8lHGYf`QDc3wx9DwLW@HL5@yt_9yaOX1}fMV~s91x>&7J_BES zc1n-*8yyw>7DoT9fa8re<$_mtu4=eo1*B1X4Rnm)z49z&=)lP%JaI8$>(fgT(lj0bhM)R7C{@(`Uc;5U5=C_c`yeTt9jPW3$o zFrR{D^P0$4x7v{%{ySM--90#r0j1!UTCb#;UO^h#&fXsXwZ;aL2OK4D`EC59P>M%o zx6D!e;yE<$?bTW;RPIGI!v9zql$4A2D zC3UtoW~)nQD~rdcv#qVIwWS3jGwa`9J4AZ>icoVqAiW~Lp{%5&!^S7y&4xwLY*@9q zqRK^2!-cTE$BOUO5sHSTLnL89Xy8`LF5%Sh%24$N5xd2w@O?ZSxyCLj%TQ{dQ-pvW z2|nG9y|BTMbXt{{S?cy&_r*6m*U`nvmf(139k>u1Z8OK5ip(BDq~+=zD*hgHV4&W9 zw8-!;VEXcpS`r&4_Tq1z$idJK3Y0%9c)0AuPN;=-Frh)_peiOCXpMQ}Pe)l9*>VZ~ zBDeT(zwqw%@Wh*Sc9EHbBS`$bEt~M+BU|8IGev|Xz3){gLFVLs3}&^#ZJOSR_x>o_ z8zz=wrkPQ)^e7qdPk_+3JD~;qwQlWYX=1{V1Tfz6l3=f;9rxl@hM^Mrf{CX(KgW=w ztL9yN%j$SsuUkE4JS7ngu7&`s+-!xo+_Q%;sJ$|WFj!a%Fa;>ASJb923Ib$Fv!O~jk7ug z8ckAA{~Xc^8XBxGznI_QVy-0*ddGOvu5&D)PP*XuHzo$ddcG^$aK_bZ+jtTN0$R<( z@L5tBY!F^1_FKwnCG4hF3eXU7O5G?o?bAh&(>>)O^p~-q0<>#dAbn@z$E9>fw8fh~ z2zUS@Ga&Bf7R_35p@ASa<`C+op``nfaH4IG)UxJg+3|WhS=+xSYR#yHCeEV5T$fIz ztoE0;S0^!atm$dD2;IQa3Uhw50(Pn|OgR~6D5t!FB>LHZ?fEYtU}(a2rBO>clo(!1 zPM>$)lQhWw&9vN&w;XwYN1}&K&GzS3k(>QYJtrgAcFt%p%$jDc?z~`z6H@Cj6y;iekBGRsGi@W#=(W|tV#rn;<3#h)M^Q{`q z+uKSnJ)u0o*_~~(`qX+?Jm49=l>f%XS)dx8KUIH^7H?Z#JZbJvStk2-Yh4LT<8u#EK^hd}k!Dkg8Q&gPSLHTwO~JPb|Vl6NwUjel^CDPhnA2Ou!#7>yb1egY7# z6hD-Y&K&h*BsJ+ImBJSM5oBe)TzeJcrw&a+3i$@c<36i<$Xd<$W^Q7a#sQ95x&0r!+pmmmzi}P_aTFml&&D=oQvE!SWWR ztG}I&oYgzezxM9s^#iAG)`9R1w!lV|a*cKJzTnQuAJspV9smQC4*jNa(DrJ#1z?$@ zm&~DTj1=}5X6}-sXkw-Oj2#vDOCSv3`$M-v!}g8*!wJjalJ#dIQ-^1n;Cc?%O zxc8me(piz^>CCD%M{;ID(%#>!X(MVvn#+x8`mTr2Gy3Thcs!5h4m8iOTgiY(mX3+{nb8E%RX2FD7@<;t~Glr->}0(ozdSAGtO+m zl^02ttRAh@!=G;K@1&)mpo?CikA9oN7(G7%9AT@(>>lJ1^BhTf*Vw4^oKC154U?U& zDmuY5!2lO4)ae+3Tws#1IP)zg?%7!Mk*)^YBUDcP0YI6NH#*8Eh6<7`wMa8t-Kn;yrzThd2K^G2!oU!G(yC14v}P>HU!;93-WP4+ zE*|5K?kV-9vK52HOO3j8Ctb1X7`PAz<*&`Gs8<1Q+VUC;Kh4xgmE-5ePJH-|0W?aa zof_i>0fb?roa>X4UN=-cL{;qQHHo21`K$&R?s=Jpn!8|!<-{-2LnvYp={ijvPmGTqFMbh!Pp zy{LQ3HSIjq1}U|Tb}`0U+31bVRY*@tr@S4r&j46zVp=R4IS)pt2VF!$Nu@W5c4~Vm z^?jgo8QxXa;H}}SC5mn)c3UKmu4hr?wQya@C2F_hkM;Bb5Mkuu@+tQ=^`^a)_qI9ux#v7wezQZ_DiR6HmuyI4@Xz`h-6z-qIW*J=fIM&g*V8&6V!>BZ4xncIe%^lc=RP;dovS`$ECQ&t>!-<34hY`dI(YFlBcOaK*5b zeIJF%ek;ImcQc4Hr#Kx2)D?C;;z^*sH7+ObTc)P!toCFy$s-%9`}~H*10{RR}}JA&5F-YYx9`qK9u*@MOA;}GvIn~=V0B2_(bAR z?G!pB7q`sWnffugMOm_lmkSSntl$zptR>*bKZPybgccsdTfPYvFeStFo8$;6wFrvARZ5EO*N^=8N#lmMcH>j@%&N_A zntg{O^rFL>a$eCT8bBrDLNrX?1JX?OZ3hCfMzoTZgV_q@47X?#jd>?x0^+#KVm3pe zZ!}$gFGAB(e13}gJjC#vxVEgFs@zpG7(0q`OLUD!Ua|UZ^B}VwYp52&%FlPVIAdThp%9kL|V|40^w?_2hWKjt?%frhB z4U9DGQCt1L6E=q}8*uVfY&6yMGwWj&8;nf@BV_MR1~4oEmnh$nTj5_}Vi6FkLWtfC zt2x_QfwsQwh)p7@dcfv@SqhC>f4ea%8xYjVReX4>r9JAuyCs zoH2V|96j2-qn+NA<>fQ?#7#c7^}mBtFG9Mq`AM$*PWsXnT63cN#{`~=>q~LmQFms{ ziJPI|yK?Y~4X;FnV}U0hxT0~XgCD`3{R&U_1vsLVIv5`V3v`|8u4kMS34d!y5cy{aveFg5X^uuPFWv;)5#bR7aA+UB`A@%hE|BEqBJDSPMa{02la za>;`q5UQ0cMhM%%;Ax!7h3qJw+J)>SWVnQ{M@SM#P|AjkpjWp;@QYgYmwVku=L7QO zKWzu+I$qCkO8N&{na}zpL=p@YYKNhN8Esk=#fI70|L$bCV(rQ$Qo~j;AA`IX) zxQs1`I3j)&N`GPfd(XM!d!vXWRWucV09a=)%0s)v zTm!=M&XqdnBk@SXT=}ypR9-;yp9q&fkT|`9%?rZcm25SN=1YB2LRE2WBug3~-l=d& z5z90d=S;mZeMPkTia<2o#ijG60v`FlwihYFE*rgIms|OSFk3Xdp1`hdpSkq&0pDQQ zcxpSq4fw9ce=cqjz=5h=)LEW`pf|NnP+Bmu2J|ILJTwA@1=rtD;0cB&!a2DT{T5FS zb=TeGt!kshzJq)24Ikyo;f>CPZM?{OQ)8*~v4!81ln~8Hq}Bw#oALu(keT(6oS-T` zq=Qe@pyUhc9tr|B`d?|@ZMGFf0A&lihR0y0>?r-a1A!v*4d7icDT^I zgA(>qXYUvl8jpLD#6OZhVE7@SG*k|_hj@!I$bx-nOi1`)FPtibK%{6A{$hQtd~eXz z!Ax1EHPMsW&~nXFX0#ob<=o;A2@(p8d{At49qsf&<}F7;kb4eyK~w~dfXueEgv!`~ zJbfW)zZ6jvsSkM->0I)&UAS$t$GOJ0>@dg=LKO(OfYK?%(o7(lO#Lud2Sb0{XHQh6M-1t)EmvR^*-ov_q1AC# z$!~_m!H4+mA2sV@9PrcKVsZ5)^LLL8+Lj{p55#HRb_=dA3iE5@l<%nTEp}VFUSi@B zQLE5xly=6V8K(sYvOV5+0YpK#;GUrXDe7k`Xc|aAeC|euxab2U zo+xBSS^?m3qWcMilEXlSQ$(5R)3f|(2}b5DEHs(vAH6rl)rT&jq!9}!>?~bD4W9o+fWk&CKfIY3;uvsqOloL=Gxp= zQ{&l{Cap7a@}ZWx`MNnKy4{o7ns z!j%>QqExkUEj88A=ApKJON;r5n*rCvNBr0Zi+YpNlgnCvauV#f)pV* zJDmOXx&qYfZ)F-BUtaq9np5jUoUPgH=?r!4#9G{1tMnna0Le0zqCJR%98amCsrHIJ zbcj0UE4J?1J^1c_5j5R3fAQdN_W9s%4nV;PtY4uBY+i#q1V$`i_}6a1dsS~!LhJ4T zF`*7lDuzA_Q4d^O92Q5;s~hB0p3ymfI<)%~^QvXL`fJr5=;z;m>j}5@mHTdRIDdZM ztBMSzbTWKcnL<6Pc9*2wtWDa-4Zlv-%t-=NPk;t%6`=?gERH5~;V#Be1KqZ)0%F&D z#$Ke+s1*WRdQlU>o=2!-qM5}GH@TMpMaWXv9^1bHx-cg?&qDm|Nd^}*$`ufhKPU#dj1j3&e)0$vBJ)}zK2!q#3$;i9|8c%14 zp3Scq=I(V={99AgEF{2>{Vir&adWiw?jC8Jx7p6zB2uz!b>0+hN<5hwhj6vVaEM{A z%{^Bn4sny_WeI+L5Y7jlzkB141J%@oq>MV(FPb8#XKp@Tp%oZVppdSJbupd&Ub_nXaq~mYrPgXy|+)*Tmgkfymenw?1 zA&8UH73sw-me3ofSOTViJjMuv2ovPAz{?S2vJO1Xz#<|18;pCbp?}HEY-pr^)HwO% zA7{cpqhMjs!1(|sLjqU=CGcJ#izK(Eehg+`6^s``ejV~Fcf9z$dJUf1<{-l@ZWV==HLKdq zZWqEezna+sl*MeSR$HxW{#;tyy!gFow^;Z7bll8{Lj-@H$8Ept=*{v?{m{O|&h>qi zP=s41v@Xbyb!%rrSmE?6PsnlC0i2Nf?u*lOebwcT#P1F2L$N}#1(3T83SaPO7b5Irj*hZaSP0TJiE4Phqw zu`Yu{LHaubJbc|#GErWV?7gBVGJL)nNFCcl8lDxi=Y7n1`Ufv3OAN1|i@Ha9RV5!d zhz2w+0;hWy_ix@ibOaodE=6H4o@WNWNwXY26>_hy8~{mg`-HagZol=ZwtG8$m^Lx&Pu&-u->q8%yII ze~!RK3BP?}9Hi;WiRpe2zQ5#2n4ACbP@K1CUo`&hA`n17lfl!A8K87BcF1*EB80#2 zmY?Px@s2q0AhT$@@>ZYLHytPQ5S&JTLWD?gcblZ|AK8~UrtqKv2+6DSdd2qQr^(_i zdsypf)T~>!^v7*cCg;?6f$uc8+|@r z$zo@(bLcV@`5J8j$coWnLb!uj3kNtF$Vm`mz`d+6#n^;H^*9>45VBf&ze37-k8Qrg zV$kV@wmp+0S)Cj1n?wG~Av{D7dw-wCT53*}tgb6%z&M4@VB;|fuw0H_X)YIve|i*k z4;4ueL|mHIMa}wkrOCPi*j~ZIiH7t@w+SR_>h0Q! z9@7Ec`@LU7ju}#FLJ!3CGHJ+}t~uiBzu|Pq-Aj5i^ZYp@I~yt)H^Ev!hQ+=G0oggd zJ_-ae!kcg{Xz4Q`a~rMkMOSdqg`jC+|4p3D!s#atIsIkSl)@I3L!#n zU^2;_6%h59TKFR?v!jybGFCl^(8;)f6abaSHdn$WQ-#P>jEg8>sRTgI1{hh3;O1Cq$^w+D?c|ZgH6Gq& z2a|jN)A1RM=s7V7mQfu;VDZKv!Ho^7xO{;WBnfN{ghQHihN!9(W<0lwtY^dDMaQ+% zJ0@y5vjVGc6i9U(C>QK&;U%$T`Y3?Gb+C}rm;lYZjQVE1ct*%VDN`*yvGhegEMd=a z#@Fnkk!drDP}KIWkg(DqC{#@NjDfF>i8>Qdx*Wc?=A zB}Jvn_Hg0(pHDHdegs0$cDnx+GZoz2MF!UW6^2L=|KZ(LmI!z{;Bi_ZKkE zuF+^m-AebljGy;xX`LAO0s;Dt&fT%D)48j6R3ni&YFWeA&a|0<#@>^aQBt4oLJuXKv z&6$5?3m0Nz-4N}43J&ss_JK_l0s_PcXtTdqayqnORv-*N@Q!YP(He*GiZUzi_f_oh z7KrgRFHLHBHVe78VIe+&Y74~W;vYnJ6LLqDZoBFVw_I?Wa-gs>Z293(WhE8>4aImk zb;Do%)VN=Y#*<~FNMs$Fc?%31O68SsOk6?sDxQx0vL#Moo4ZuQ4Sy3&ar1ank$LLq zC*`coMjABJJYOrCk~TqV3^1te8JR)FN9gnwex)`l&DsY7_%e@pta!e@zai zb2aFe;AYDRupialGld)$bs+dA`@p`ED!a`VE+F`1Y$$~E3T($b6H^w`InzQ$pU(`hQ3j_C zhg*Tyyv0v~?M;H{k0IorSV7A514vA3=@zK3y5eqj$G;sbPAS*$-)*PY@CnCC{-~}iyg_6Z=}ycFf%U3|t4 z+}u+Zy0+9N6g1pk3}DXB9Z@UC+&r;~Ch}mVV#IHkSs<78)tXl_ zpJsRFRdVO`og0p=LA(!@3I#UwE$B%(!fWi_(l;UhMeN4Vn6SGY@=1Vm&maq{1#ed} z)6{z*?D}gH&aM| zX-r5is?zx3QVnwCrJW(vSFnq->06WC0cc9r9QOvvjv=XWfp_Om&$R zG=%c}WAxr7%W|mz22r?={hc!)3UIU11YcYKEwDf!na>3HdT_=cIesf@fWjaT{X{a7 zd~P(BOTn|LymC5SytjXK@gOCS3~KQ)jSxTrwZTqLzPdd6-qAD#s+1arI4}mQqOHal z_{<=yCrB}_>0{N$kq_Krpg_fL>{KzoY_(a^HbaWPTdN9mp8j2{hNWGiZf3cnEw?DA z##kLU0wMxX{>I(}amozQ?if%3JKw1_Tt{dvH;Gm2GTUR*`t0ibfF!0I#+^!|XKNOt zcDIUxw~Gj!GXpU)h@~Eg)!KBvV$J9yj+#?di>N2!Mk*^e%k=u(S6n-Xvnz4$ET~Bw z*T_W>Ew>H$HnqBlE49&y@M`fFjg{=?jz&>hn`Hzvv$S~Y>DXDqYZn!;S=lglFHuv% zU(wpt6tLM`C_*Jq>KmWv(W_Oap!sEY87JnhEpdIf zVUYwdbyvZgfu>R-aSrp!4ze+4o~%}c9V6tLvd2beTpHFjtKt^QZ*)#!MmA}~- z#7v$Mct4*07vo{t`PcON>$_^IrLc%o%_0}A=`4$#;kbnUw|ws_pE#8-^GF?!VP40% zi#`~o4AENuHD@fDHBNuzU-Aq5Vd-4wNIq%6WwcLngc+BE{PsXQSvLi;)F^9PJ$2bs zCGHxKNkws7tJZ^EX)H${DW+eCdob275aPO#tXPK&D4$86-Bd$AC+}207m6PIj$Iq2 zGFJ@`N|Z4dZ3LMT2c9R-{IzW~SmNhYJhu`z)>FT+?ufJ@{o*-cZJYZY5!{Mi8hmH~ zJh?0|OFC?+uAj2jv7Z<-*mwq(n{`$P?DsWXl|-wY3ZZ187#0 zZSVH6nJDvwpLESz#Q5eTnXZ(Ui@h)4p!g33j5z6#{?ZgqETkkKmIF>WdFWxKwwck> zuMw=qEqSnGx3hywX0F-XTon}xe_M}#Aq|332<3q2cB-WlY4IwO1TbM?!p%tU7Lowe zkaCAW)e*XY0Y;ee^-e)r`1>OJ8Rozex>;vri;?sV3i`;cZmbfW&vKyn<-=7#HSG!# zbjZA&-pEmgF&>Xx>QOuKbQ0_c+bWw8QN2qu)Z2ikN#kc2Jt$wl5isi}5|qSX`K(AP zhdlL^(?tohk?tc!v_++WUr5;v92lz2aMTzL$An}_5=*1PbI*P9HiuN>?qrhiZo14w zbRar<*nOvyJGqp<#TZhR1{yX%b~Sy&>;~!l4VVVXwmuF?wk%gMSMtsTTeh)PH{JeR z@@prHNwEyKo2(I)i-oX&yF=nQJKM)IkwPT+MYb2`9kc-fXijunnb0K^Emm5Yaia8K zYEa{O<^W7|e^qkY7A6C9o~(5;E!-Ahz9wkhv1rbTlnB#KLb{d;o~R2*e5y~`-Uh|g zN_*^OB^=!*UNt=@x@f%Dymh`3MPs*OPf?^mK07S(In_7rL^Qzp6L+Iv?whvvq4IdP z{9;iqN#A{|PwJt6{zk}GoQt?b;)!8$UDQl)1?=0Cr+YZ;V*dukHHDZ|)ikarITsWE zjpa2-gJ0BrKRGt8qyGfJIwDZy@x{MV{TCq^n<8UHoCATcJ}+BYqEa5)`#Zroirg;& zpG4VV5VeY9Pg=!cFb%Y4h&2$uz@OlVYEp(Kbi$JEhxq8gOjl=xF{aL~Fh}u1xNPi% zTNXU$h(B#kON&W3WJvXq6#Yh_fdg6?xklKBJf_UMz8!~_gL)O9uJx-}Q%4%|3@P1l z0puM84B`o>Jh!WO;LmOx+>1^+?Y3QqjQ}mV&b@As2F7FW-~T5A<>S@4far`S04LHbxh4 zubmPn?vRWJJd67+(_6|J;y8ISZuARQg%*Z#=wWU-fDS}yAGoPG&aIgD#62i~;LPxE z1*rQ?gr#mkv3USDfvUD@>hi^>_K6Yobv16Ovk#0uO=GDlNGXUs)*GW*fU;YeW}A61 z#&W0MwUM@F)s#ts!mhzZ!w#>6bxyy#{$jq2VD&j69eNgh6BOdo{WNx2fyR6jG27$^ zkrt47v~VJ&EV%;av=?-7OkqkHc%XLw_WgI&-v|xBl9u9)cPB!XhsjrrX45Yk`{0Zh z!xcHkyBSvwKd4xzC}wvaIG!|i8WQdXZzeyT4hf)!zh>_UaPHXI*IeFWVu2LOK%Rr{ z9A2oDmYOc%y|B~>X6r}AF~%l(*!LmDbaXE25oCQFFg#o=VI>;TZ+`Cqc;fnCOdHR- zN0u#skJIc{*^!!N-y_$C4Qkeu=Z}UJIHc?ZoN8`oLFm5_ByO$T79eS&1Y)~-r#+zU zUs!|t`kSI2ix}^o)Rf+Pm~#t==$mtLR7cy%8rfC8LMoNxBsE%Poj-7N=~=d;)DF zzZ5ok_6O_E9;4clP5X-1>_B^sElTL`nj@Rb)Yfxb$&aiOf6Z>k`>i&gb@sxi`sjN`zGUdHQ3dzvFuPA>rR4F@7`JiHyP#T9mrzXh`qS zzo*!B)AJP12yMQ#z*rHNg(2m(0#{-ujFEWExSzM{fb$k+MQS4`+e{f*Ux?D{@1AR_ zX$l%VFLPU1zQorl%eES=M0ZL2DDAlbxXs~~xIV-Iu!pTTBt@%a4E$@z;mZr=G=&&v zqiXw=-uwQQRK*LGC~NyP2#ex0Kz7bMGH?7A1A#`H-6JQ-wOl&2PItWvX{JSsUxxM| zST$+|#wFg`)|=6T!TC@eXpF!WT}Tr?FxlP^+n)4jiV*0u;?KGWSj+of7WcpLkSL@|_Z@OZD(I zP5wvx(lh8(F0&J&0Gz7?XqOu>6F6~9?A0C7oRq<^Q`}~|`wJX)P_*uz Ac-xW1# z){S32Cnt6?HmLcD&~7U(;X$!yC#c{BkMEFE^u#7L>^&RuEYO0<>O(2Xl(1glD6OoL z_6a5Un|;_18~O#E@V@IdkAM*72PxbyrFDo#EJubGK}GVag->PY8`KS8o!^+V6B@@| z?~c(^>YXo)tj{R+~MkU2Ovf)*aa=>{ltsx;eDzwsK!V742EyP;Rak)pM zcVsKl+m%d1OrlI>-%g^es!%kSA{f?;v0O+$fTQ<38@z zBZq4ECfhWH7|_~e90-GYg(CHj(j?6IxZB*)eye_RN`L?B`(>A6yhxEMMWne;@AOBZ z$!`|rh4|3VFFnv_qWu0W>{K|WRrert?W>vEZr{3ILa+p({b&fF2+lYpie&*~dMEN5 z-LMB=wr=PH!o5tk2c(w-zo6mD@+$ z$#VWGF_I6ZawAC2>iE~8@j$_-{^t<|OQJcMSYa9R06of9)vF$waK616SZRR#%K_T8 zE1DQkP$8ut5Unm?n@N~SX^nBjMvs4uk?eU9r^*rMGWiCNq*-M}KFkWUna5O30hY&x zZ*JZ+Pv14-h=pHaj8QL=_qmhut+CaQ^$)QRgg&gzo}>ocQp)1pcWb4^cwPn@e|#-y zb+r@FVsbuZCw?v2+}5e{uXG)!UP7o^5l6(hB4d07GF?GHR7bpZ2b4FxIAglxmKTcN zaMGFd^FqsI*@YL*pZ=vYjPBi0mQ(j!DUUna&Nz#uGA{(bkP~Vhaib?X)tON0qE;2E zxFH1Y!oP62K;_=QC*0}#?d3(+n>wIAI!+;`Qzi$c+J;K!i~v$93T2MB&CU*?C)Xz^ zM+jt(P@LU>wN`IbFT)$UGZdVkL2md{UhI^#SZxBNj0pY+-`Qy?(JH0V>ZP-LC;xmS znBf-V!;N*(?%MX#${^RLle0{t&eoE`)1V>O99+qorc8~}TVAwVGwFA!Rg^3TQ0?5x zZOtTt4ZA@FVRdLbH}uIgj6EkmnH$gJzV2y&snt^twJjEtwj!ll?NEs-yO=_jZ z5$d&OS&XEOdgx2;xz+F*ly5klyG6vfKfFe>2n#2EQVqwF%a*yqEa!|i|3uh22Q9O5 zz&IlU7VxF0OS}J&mTb?UP&qv#mA%djb&AE}*uT9p5=Xuc*9iVRgq^Xs&q{FnT_bn; z1@miHrD;qQ^Z5R&CzJIxx#7nN3kvJr``h5~h;ZpyhKAyGzdt1sasf8$Gt&MVRcV1g z>#ecpg|1hWJNsyrs8GhsA4KlR_vXolv=u%CkVMTHuqlt2D{SeGZFPoya{k3@%!goR z#~OY@zayCD%)?t8R6F?K&FgR&QvoVY<#vn95B{zd8_t0_=^9j(p*vYNNIiBH7DjQMAS1{DpJ{2WeQwRQIf2w=j9W|<<&Lxc$dcDc< zPVu_|FTlc~6G^Rv!-0syp+tB`eCf@1_zTvO-eFGiqJ%0!26(ZLCPK!GiIv34FSRlo zm$H%K0Yz{*ahdSS&berCz$$%77Sq!cK*iAHR|Q>l=vr9SltkiJv9GS9N`vvail%Sh zl>cDWy6EN!Lk$~WeZ;MUc(T!wh)G&?SY3cOF13O(omF@)r}Pw9>8ADfDCOBK05d;n zDp0j>G`9*!SSDJV=efOv|6uz=t6(%|bI$JOU#%#0+seT(&9UQOAPo>3?*!2r=pXzO z^=yNOZ-OSh(OfY2{JmBluz%z#4Z?po^Z#h@_){1Fz^@f_J`*~UsRQs4srm-g5$Fcm z2@EOdWX@vI)(CW3o+t4fpjkhH zlL>a`00xI^AD3OelU$FJ*^iep0)M!_ocu5cSnAry5(!}|jHy8xAjQ#uf3>EXQtWagX!Sprml<~z5l0~%5gSJn z8JENN+n=`P@72G@m(AWPv#BSvnb`ihM)%8qKQrmE&}lVc93|F3opK8BxY!%p_V!j4 zS&oM!HX2fo7VDcMazs~_;j7BPgq&7ly_=Ca#8g4VbUNt?-X>R8tXcs>nr!v7&4pqB zz+cB6LBy`ImD$WT=}*v1^k-AhN~b#LCqpM)92OjEDw7ZUlkL$|=$n?=L~2#hNZj;W z)#u`&%rZCY=PUZjMp%f+_hH#}TVU65mak7+e$KUXP1GDEFkC<&93 zZ>7}}=qbr5R+92xz6V6#rTN zzJ3O&jO1X0JKKLfRky!d{i8ep;J82cDRWHn5wAJHy9a#8fcW^4W*`Ii&*1QX{Zgpk zw0jJ%Rl$8mdV+03wX#eqRxRlZv?b%qI~HL|pE*`vBK^7KW`y}|4cs<1soLtT-Iu?X zu9S%?&(vL0D%mTo(YGQy-B<=21oE3F+9N81aqOjDDa!Or+D}hPNLbv>%B3T|AFlc^9x7hym``{pff)G#@iWOkV?TnpDu$!Vss?QEgv)xA^fGVij*CT zNMVhn@Xmpxy{}ONU<>A$Z&eKvZF;8WCeC4fe6=bstP0(dhhX<3+46m{eQ>KbHBoT{ z{UgH{kZTC6|s75dx1o(1b~;)PtuK*PHHh?iQ+B;=}VqviJnUy9vy5%r$ZT zoqfLVkU|;KnseQ)IP%on9TZe1VaQcog5Qalpm6J+C~__S1{h~JR&Ol zLsu3lR3Rb@fK%bsj&xy-NyY~;i7(8HA}fLW1DTfd5_1AUcswF_X%Ju%_hhy?LAI6? zzO6N~@bR%DMA(pf`=rJ+j`P-UAI z;|9^=i57<}4&>8tiIyZvfa!9_rK?T!iHVGymX4su!^=IVf`xIdN-P|l=s<+_14MTb zG4AMhtaGCBFiFKMY<9S;YW?8SevyP1%z&~^0`^Ubw_xIGn1(y}p_|RUr!u~V7|!Y1 zyz~)>s*(-UyPr()1wl1)VEIMxA3Qs0@&zYJH669dZSe{W9y1_K3*%ori{igpV!Hoc zo4v1)NzrTQ06mo@LA200VXIA)Q;#<^WVFqEQ6WX(sCkSUbw}-fY=`vZQ50KLaw)UX z-NTUCb*E8Sz;A)cJ6n|eKlT>gToz3y-8cLjOOJEA27SPW;LpNHp%fsz@cm7M(SxBt zS-|UsQzxvZR_hpl!Do0_FBjuc2^jWb3+T)j$l?Fxwl^A8|I)$dD^HgCMP$&OIAn;eHZ9;DLS2fB<_Y~hCW@( z+0=^6Pzl9AzUk!FE@=Y>T#wU#GgvGuqS0G*cJ5lMt34=Iu;UUQs9NFDl#1u|$mRK! zYJoA60fwrl+*B&qRNod=>FHHf{BMr43joXKbSeeEUlH~?txvjYNfKx4F;QCAOD#LQ z!Wu`hX0{oRT%g_&iN@5fJJjOsL_iGc`6sKR^qX%Uqa)D1?1H&NvoSvZc7 z=`o^YLmo8}4u%pHlUzI=`T1-iI-hYwtUNidfcLoVf;*6)>j!chUI8lca2}kJVkqn8 z1st0V_v1nc*TEdH@~b)J8TQhAff0{DZ2e6#{$_`o<^Ee8r0mMH7`k^8J4F|r^mi;B zh#(hP_y>x+%+UkK&!V2VGuQE#ITLDe?!#;n)6!=ABj~WhTByH;OQ{I2D_yPxiAMIw zHSi`KU5iln05n&ZOL>L|uCjgFiJ-<~CE#@6#V`PL-$V$boiHl?IN((i37SpDPw=$* zbO4(^yg!iEMTnID-&~z<-hwDO2&%Oo7+p_zp&S3<8;^`(3d)w{Czyyo4oWZi8+^i9 zDD{mH7{e5kt%IMC3Q_bp5KJqckA7T)UosxtD<+e}PjHksUV^hi;!={=aPQHqC6WEodO1YIGnV%Kz;kbU=!Rmm%5_ z`j1>=)&^XXAv-A&RHt#WnFmp%5)#r8lo^{w6Ev$~mOOisscBQw?5wk8&4{`U;+YiP zb8F2qhK?;!L8&xiWKGY_KTGIGY0Hft%jK*+gv`(STkK2kZrAS1Rnm{wqZ1I#odY&Q z!rda0erY}oLz+`vARz)}Jm3~)$Eze-BjnZ^yH3dwQ@<)70}_3D02sSYg%K0dI^u!< zyE|7P?Dg;0rx&QoF4ka{r!UJ*cZh`p{GJ^ze}7(E*ewG7?!+Oampf;$$K5LuU zzWcBJV|Qz9Lx7Q#Xxpu+RmWz|a^vnf*$#W-t_9_sJ!7N0#_Y;YyEI=$Qxl@zNu+Wq zNKyWBi?M!1L|0K<7GU>>vHQNVDoiEl56bb4z*Wx|2Ld8# z7mp7lBrhqMMF!WOMtDAkf{lhs!(SQCz5D!H>IVCp#-`)V{{om8x4h(V*{oQ2%%lIc z9GGbzTpgscXD)2LXllpKwm5i<$_17gTD2OPpD7-sTP0*?Jpr0~HtJ7pv_D8)MW+Vx z5E@+S{!dls9n{3uhH;1>%|Z=rks2TrDFPzBNJ~Hkg4A4;-ix4u1Owb4Ua8)ti4Zzi zXcEK#mm(q{BGMxQN|z3iq97lBdo#}Gm)(Efv-3OWJ-f3zv*(>T&qF-juL9!;P&Kn4 zHDFCIZy+sG7Dr<=+8ggQ_10yMl{p@*p0uhT&p;oF>$h$sq*tSe@oA0gREl)1R z0QHp%Qe!#^6JL)0={H-u7%-o?xAe4;uc_M1J$*#)OCP^4Ms$e%0m-Uzj0M@CVBveA z)xQtjd&2dcIJ&je0DC39;D$Ntx>uyCo|%Q2W3bbW7x&-u_`eZEp-S*%+ece+J)X>o zX*F|re)xmMN)NUH_S@bK6WA)*9V0$%Y9#lX{l4W77(U2`YJ&Sq==Ph1L!(g*=^Zx_ zlsQ?;1T}SE#YVbzd+=be`;o=l3SYDBW{i{kqcW63v*bi41}Erx)z`JOFPvCfI`d%B zejxc2U-P1^%F9MS0c$)}9hXN)y0sipoJq069r=t2l@GF~DrRCj2g=8Ik`Gtzxtt6TQmKgPue z-n}h4E0YwFa4&zx8`Gv&nW^*78wC&3c>ab~lilx{6RI-tuk7(PGWaUD+-NEX`~ZD` zdR-m4E1YyGOIUHGO%RTMc&{mRe9Yt8xZ-5br-`p>R&c5_U+b$4yMx#>h(sVi;}2CfBsTprdM8`tYR#hEa2$c)F;jD z*(Xtb<8id3^EiGb)Ta32O{K8KHiGARXiz(Ma5c+xTu)DC$X^>)j-xjp{kUwl=PaqJ zqm((!uC3ujWk>;=E6ulG=qzQth`%1#5}f0B zr>AH$s!J1}2evd&E`3CM1xDw_6BeImmsh#x7qkCO&OSW#u2BCDyHkx|UfoBdr*h%_ z`+~$BoKx^l1^O}Qn<)~!$tgtjeuC6 zp;MsI@pmzqg676&9tquxe_GC!Z5ZMsq&}3E%1#c%Z(kS4a?_5s`@%^?6!@!Pn1BQY zpT9ov8hvwrdCV`N9W|xWEBGX(^nyX2=_IsCRX)pF(PMaswS!GE_HxB$H;U}F%K49t z{PFN_+D4^-2y1`e;}2@fY!Rm!T|}55eDUNCs`pjK-2RD~ z#+;cJ_AGbx`B8P=B=zhLr`0YkpyN3qRJX7K-}Q=|a`7bx zu71sUc^_}Y{4j5oj*wEkmV(5n#a(CF4rgR>FsG1)wnNn)hAsp(Qwyb0X{M!PncW_E z(uJoCYbjjcBCNrO9jDIwv`?oXU3Qq#9mwuN_QCltpNOkqb(8(&&QxV#7L6tO%#pLF zZ`g&LsTz>9oqmxkzojQpi%9gY@w%&1Q?E_TZr9q!MhT!!roJ_~qFmyac#;wl_>JG&K$?~J`#5~&R@Rdi`vbFG zmG3juYOjmSh*0cDzuoORB2GENZ8v`{U+RJOG!~ z5;F|HrQdk8B8;RC$xkuL&7p)9F2e8J<2H^UE>{Q7dgEpsjGWx zPr0*ilC#fppWULoo~e|ni52Q*3(&D)#64vpSFyU-GgV1W@srzrV5eAo_;^iFrG|Ww zBRK}>DK$SQm>EwwmnDG3u|5xarfJt>{TL&HHn-r+F=FpS^eV4QQU2lN2;ui!nmpgy z#ZypUbUM%k`_V-*@`g;_9D`yavIrf--iW;> z+h#Sts%tZy6{TC~uedz8yeu*OI_@X&Ck(8a2S~Q_x0#q0zx%d=BTJr4=KfhQu*-7=9lmv16*>w5%j3bVeA| zTjd1)wZeP?fgKR2`8uAJ+!gu3>~jE+#lSe3M*VfGs1X}{Qj+{ z(=DY-XG-h9@=L}Ptij-wK|ZkWD=!%TRTXwJ^jGsd%>4!}OufYkp4;LC z=yVq8N51(BWCNq35TM`=CqR=>gVyJ_p+}=e2UDW{2eZTofb84QTk`*cO?2>E{4bal zwB1&Qefj%;&0wff4kZG+w}oI7kal@*8wLB6Lks^X5CZv^I6=aW5UeN!a^j3uDKu0BAwYlDvKEQ2@ze6gzx8*8JbIo#EN#lND3r`{e19lpr|boG3? z?!K>ofjqQ{4}TMaJ?jNGzZn5^#SFSWF9*O7iV_(8ofDv2uhBsgBcRy09Q+cR8T!f~ Q>rd#Eb%7 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 62f495dfed..3fa8f862f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index aeb74cbb43..0adc8e1a53 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index ab859689e0..970280dfd9 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -201,7 +201,7 @@ def japicmpReport = tasks.register('japicmpReport') { japicmp.state.failure != null } doLast { - def reportFile = file("${project.buildDir}/reports/japi.txt") + def reportFile = project.layout.buildDirectory.file("reports/japi.txt").get().asFile if (reportFile.exists()) { println "\n **********************************" println " * /!\\ API compatibility failures *" @@ -246,7 +246,7 @@ task japicmp(type: JapicmpTask) { onlyModified = true failOnModification = true failOnSourceIncompatibility = true - txtOutputFile = file("${project.buildDir}/reports/japi.txt") + txtOutputFile = project.layout.buildDirectory.file("reports/japi.txt").get().asFile ignoreMissingClasses = true includeSynthetic = true compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ] @@ -265,7 +265,7 @@ task japicmp(type: JapicmpTask) { gradle.taskGraph.afterTask { task, state -> if (task instanceof JapicmpTask && state.failure && ((JapicmpTask) task).richReport == null) { //FIXME print the rich report somehow on console ? - print file("${project.buildDir}/reports/japi.txt").getText() + print project.layout.buildDirectory.file("reports/japi.txt").get().asFile.getText() } } @@ -280,7 +280,7 @@ javadoc { // work around https://github.com/gradle/gradle/issues/4046 copy { from('src/main/java') - into "$project.buildDir/docs/javadoc/" + into "${project.layout.buildDirectory.dir('/docs/javadoc/').get().asFile.toString()}" include "**/doc-files/**/*" } } diff --git a/reactor-test/build.gradle b/reactor-test/build.gradle index 262b02ca1a..1ee5698a9e 100644 --- a/reactor-test/build.gradle +++ b/reactor-test/build.gradle @@ -65,7 +65,7 @@ def japicmpReport = tasks.register('japicmpReport') { japicmp.state.failure != null } doLast { - def reportFile = file("${project.buildDir}/reports/japi.txt") + def reportFile = project.layout.buildDirectory.file("reports/japi.txt").get().asFile if (reportFile.exists()) { println "\n *********************************" println " * /!\\ API compatibility failures *" @@ -105,7 +105,7 @@ task japicmp(type: JapicmpTask) { onlyModified = true failOnModification = true failOnSourceIncompatibility = true - txtOutputFile = file("${project.buildDir}/reports/japi.txt") + txtOutputFile = project.layout.buildDirectory.file("reports/japi.txt").get().asFile ignoreMissingClasses = true includeSynthetic = true compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ] diff --git a/reactor-tools/build.gradle b/reactor-tools/build.gradle index 86b91843a1..ef603e05bc 100644 --- a/reactor-tools/build.gradle +++ b/reactor-tools/build.gradle @@ -192,7 +192,7 @@ project.tasks.buildPluginTest.mustRunAfter(shadowJar) project.tasks.buildPluginTest.dependsOn(shadowJar) task generateMockGradle(type: Copy) { - def coreJar = rootProject.findProject("reactor-core").buildDir.toString() + "/libs/reactor-core-" + version + ".jar" + def coreJar = rootProject.findProject("reactor-core").layout.buildDirectory.get().asFile.toString() + "/libs/reactor-core-" + version + ".jar" def agentJar = buildDir.toString() + "/libs/reactor-tools-" + version + "-original.jar" def mockGradleDir = buildDir.toString() + "/mock-gradle" @@ -208,7 +208,7 @@ task generateMockGradle(type: Copy) { } buildPluginTest { - def mockGradleDir = "$buildDir/mock-gradle" + def mockGradleDir = layout.buildDirectory.dir("mock-gradle").get().asFile.toString() println "will run mock gradle build from $mockGradleDir" systemProperty "mock-gradle-dir", mockGradleDir } From 24bea93882b19b357c3e873aa1cc9c3958fd5a91 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Mon, 13 Nov 2023 19:14:12 +0100 Subject: [PATCH 203/312] Update micrometer and micrometerTracing (#3636) For preparation of upcoming reactor-core version 3.5.12, we need to update Micrometer to 1.10.13 and micrometerTracingTest to 1.0.12 --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0343893154..4b88eec9f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,9 +14,9 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.10.12" +micrometer = "1.10.13" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.0.11" +micrometerTracingTest="1.0.12" contextPropagation="1.0.6" kotlin = "1.5.32" reactiveStreams = "1.0.4" From bd7564e5fd7ccca2c17ead102828e4242f52e598 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Mon, 13 Nov 2023 19:14:37 +0100 Subject: [PATCH 204/312] Update micrometer, micrometerTracing, contextPropagation (#3637) For upcoming reactor-core 3.6.0 release, update micrometer to 1.12.0, micrometerTracingTest to 1.2.0, and contextPropagation to 1.1.0 --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d675dc73a0..4d4f1a76e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,10 +14,10 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0-RC1" +micrometer = "1.12.0" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.0-RC1" -contextPropagation="1.1.0-RC1" +micrometerTracingTest="1.2.0" +contextPropagation="1.1.0" kotlin = "1.5.32" reactiveStreams = "1.0.4" From 00a814bc753fa9959daf13ac4b9f75248f6d382b Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 13 Nov 2023 21:22:12 +0200 Subject: [PATCH 205/312] fixes javadocs build Signed-off-by: Oleh Dokuka --- reactor-core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 970280dfd9..79464263ff 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -280,7 +280,7 @@ javadoc { // work around https://github.com/gradle/gradle/issues/4046 copy { from('src/main/java') - into "${project.layout.buildDirectory.dir('/docs/javadoc/').get().asFile.toString()}" + into "${project.layout.buildDirectory.dir('docs/javadoc/').get().asFile.toString()}" include "**/doc-files/**/*" } } From 9e9649c030f35b5aceed14261e96db16f8c87ef3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:45:00 +0200 Subject: [PATCH 206/312] makes `throwable` assignment HB `done` assignment in `onError` (#3638) Signed-off-by: Oleh Dokuka --- .../src/main/java/reactor/core/publisher/FluxPublish.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java b/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java index ef0d7587bf..b3211b1200 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java @@ -316,11 +316,13 @@ public void onError(Throwable t) { return; } - done = true; if (!Exceptions.addThrowable(ERROR, this, t)) { Operators.onErrorDroppedMulticast(t, subscribers); + return; } + done = true; + long previousState = markTerminated(this); if (isTerminated(previousState) || isCancelled(previousState)) { return; From fd79952f435fad5f34df3236ae75039a44c3faf0 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 14 Nov 2023 09:32:45 +0100 Subject: [PATCH 207/312] [release] Prepare and release 3.5.12 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a27f4e22de..e6ad93fdda 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.11" - testCompile "io.projectreactor:reactor-test:3.5.11" + compile "io.projectreactor:reactor-core:3.5.12" + testCompile "io.projectreactor:reactor-test:3.5.12" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.12-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.12-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.13-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.13-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.11" + // implementation "io.projectreactor:reactor-tools:3.5.12" } ``` diff --git a/gradle.properties b/gradle.properties index 9865d37b2f..e5876868d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.12-SNAPSHOT -bomVersion=2022.0.12 -metricsMicrometerVersion=1.0.12-SNAPSHOT +version=3.5.12 +bomVersion=2022.0.13 +metricsMicrometerVersion=1.0.12 From e42dd15faf7c022235be020c79b64671d5a7f870 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 14 Nov 2023 09:33:08 +0100 Subject: [PATCH 208/312] [release] Prepare and release 3.6.0 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b4b24c4352..fa6ebbb469 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0-RC1" - testCompile "io.projectreactor:reactor-test:3.6.0-RC1" + compile "io.projectreactor:reactor-core:3.6.0" + testCompile "io.projectreactor:reactor-test:3.6.0" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.0-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.0-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.1-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.1-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0-RC1" + // implementation "io.projectreactor:reactor-tools:3.6.0" } ``` diff --git a/gradle.properties b/gradle.properties index 47f6da2b71..2e3b452e41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0-SNAPSHOT -bomVersion=2023.0.0-RC1 -metricsMicrometerVersion=1.1.0-SNAPSHOT +version=3.6.0 +bomVersion=2023.0.0 +metricsMicrometerVersion=1.1.0 From d07ac40b94000b3b1d1e1545ff2111ecd9c7592b Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 14 Nov 2023 11:44:40 +0100 Subject: [PATCH 209/312] [release] Next development version 3.5.13-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index e5876868d9..2b2ee10a18 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.12 +version=3.5.13-SNAPSHOT bomVersion=2022.0.13 -metricsMicrometerVersion=1.0.12 +metricsMicrometerVersion=1.0.13-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4b88eec9f2..3ac159c27f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.11" -baselinePerfCore = "3.5.11" +baseline-core-api = "3.5.12" +baselinePerfCore = "3.5.12" baselinePerfExtra = "3.5.1" # Other shared versions From 1d4f042d4db378a5f7b1d465f90b6a93184846c5 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 14 Nov 2023 12:10:54 +0100 Subject: [PATCH 210/312] [release] Next development version 3.6.1-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- reactor-core/build.gradle | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2e3b452e41..5a820702ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.0 +version=3.6.1-SNAPSHOT bomVersion=2023.0.0 -metricsMicrometerVersion=1.1.0 +metricsMicrometerVersion=1.1.1-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d4f1a76e0..50c5a2deb1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.11" -baselinePerfCore = "3.5.11" +baseline-core-api = "3.6.0" +baselinePerfCore = "3.6.0" baselinePerfExtra = "3.5.1" # Other shared versions diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 79464263ff..863ba333eb 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -256,9 +256,6 @@ task japicmp(type: JapicmpTask) { classExcludes = [ ] methodExcludes = [ - "reactor.core.publisher.Operators#toFluxOrMono(org.reactivestreams.Publisher)", - "reactor.core.publisher.Operators#toFluxOrMono(org.reactivestreams.Publisher[])", - "reactor.core.publisher.ParallelFlux#from(reactor.core.publisher.ParallelFlux)" ] } From 2b623740ba93bfb87162010722192756de5913eb Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:51:46 +0200 Subject: [PATCH 211/312] improves documentation for `VirtualThreads` with `boundedElastic()` behaviour (#3635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: OlegDokuka Co-authored-by: Rossen Stoyanchev Co-authored-by: Dariusz Jędrzejczyk --- docs/asciidoc/coreFeatures.adoc | 20 ++- docs/asciidoc/faq.adoc | 2 +- .../BoundedElasticSchedulerSupplier.java | 5 +- ...BoundedElasticThreadPerTaskScheduler.java} | 20 ++- .../reactor/core/scheduler/Schedulers.java | 120 ++++++++++++++---- .../core/scheduler/VirtualThreadFactory.java | 11 +- .../BoundedElasticSchedulerSupplier.java | 6 +- ...BoundedElasticThreadPerTaskScheduler.java} | 22 ++-- .../core/scheduler/VirtualThreadFactory.java | 3 +- ...dedElasticThreadPerTaskSchedulerTest.java} | 10 +- ...dedElasticThreadPerTaskSchedulerTest.java} | 65 +++++----- 11 files changed, 182 insertions(+), 102 deletions(-) rename reactor-core/src/main/java/reactor/core/scheduler/{ThreadPerTaskBoundedElasticScheduler.java => BoundedElasticThreadPerTaskScheduler.java} (69%) rename reactor-core/src/main/java21/reactor/core/scheduler/{ThreadPerTaskBoundedElasticScheduler.java => BoundedElasticThreadPerTaskScheduler.java} (97%) rename reactor-core/src/test/java/reactor/core/scheduler/{GenericThreadPerTaskBoundedElasticSchedulerTest.java => GenericBoundedElasticThreadPerTaskSchedulerTest.java} (85%) rename reactor-core/src/test/java21/reactor/core/scheduler/{ThreadPerTaskBoundedElasticSchedulerTest.java => BoundedElasticThreadPerTaskSchedulerTest.java} (91%) diff --git a/docs/asciidoc/coreFeatures.adoc b/docs/asciidoc/coreFeatures.adoc index 2314256d19..9225536669 100644 --- a/docs/asciidoc/coreFeatures.adoc +++ b/docs/asciidoc/coreFeatures.adoc @@ -209,13 +209,23 @@ dedicated thread, use `Schedulers.newSingle()` for each call. * An unbounded elastic thread pool (`Schedulers.elastic()`). This one is no longer preferred with the introduction of `Schedulers.boundedElastic()`, as it has a tendency to hide backpressure problems and lead to too many threads (see below). -* A bounded elastic thread pool (`Schedulers.boundedElastic()`). Like its predecessor `elastic()`, it -creates new worker pools as needed and reuses idle ones. Worker pools that stay idle for too long (the default is 60s) are +* A bounded elastic thread pool (`Schedulers.boundedElastic()`). This is a handy way to +give a blocking process its own thread so that it does not tie up other resources. This is a better choice for I/O blocking work. See +<>, but doesn't pressure the system too much with new threads. +Starting from 3.6.0 this can offer two different implementations depending on the setup: + - `ExecutorService`-based, which reuses Platform `Thread`s between tasks. This +implementation is like its predecessor `elastic()` creates new worker pools as needed +and reuses idle ones. Worker pools that stay idle for too long (the default is 60s) are also disposed. Unlike its `elastic()` predecessor, it has a cap on the number of backing threads it can create (default is number of CPU cores x 10). Up to 100 000 tasks submitted after the cap has been reached are enqueued and will be re-scheduled when a thread becomes available -(when scheduling with a delay, the delay starts when the thread becomes available). This is a better choice for I/O blocking work. -`Schedulers.boundedElastic()` is a handy way to give a blocking process its own thread so that -it does not tie up other resources. See <>, but doesn't pressure the system too much with new threads. +(when scheduling with a delay, the delay starts when the thread becomes available). + - Thread-per-task-based, designed to run on `VirtualThread` instances. +To embrace that functionality, the application should run in Java 21+ environment and set the `reactor.schedulers.defaultBoundedElasticOnVirtualThreads` system property to `true`. +Once the above is set, the shared `Schedulers.boundedElastic()` return a specific implementation +of `BoundedElasticScheduler` tailored to run every task on a new instance of the +`VirtualThread` class. This implementation is similar in terms of the behavior to the +`ExecutorService`-based one but does not have idle pool and creates a new `VirtualThread` +for each task. * A fixed pool of workers that is tuned for parallel work (`Schedulers.parallel()`). It creates as many workers as you have CPU cores. diff --git a/docs/asciidoc/faq.adoc b/docs/asciidoc/faq.adoc index a62413ad66..b83bdfc5ec 100644 --- a/docs/asciidoc/faq.adoc +++ b/docs/asciidoc/faq.adoc @@ -28,7 +28,7 @@ blockingWrapper = blockingWrapper.subscribeOn(Schedulers.boundedElastic()); <3> ---- <1> Create a new `Mono` by using `fromCallable`. <2> Return the asynchronous, blocking resource. -<3> Ensure each subscription happens on a dedicated single-threaded worker +<3> Ensure each subscription happens on a dedicated worker from `Schedulers.boundedElastic()`. ==== diff --git a/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java index 9f59e421c5..9a7bf2dd18 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java @@ -28,8 +28,9 @@ import static reactor.core.scheduler.Schedulers.newBoundedElastic; /** - * JDK 8 Specific implementation of BoundedElasticScheduler supplier which warns when - * one enables virtual thread support + * JDK 8 Specific implementation of BoundedElasticScheduler supplier, which warns when + * one enables virtual thread support. An alternative variant is available for use on JDK 21+ + * where virtual threads are supported. */ class BoundedElasticSchedulerSupplier implements Supplier { diff --git a/reactor-core/src/main/java/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java similarity index 69% rename from reactor-core/src/main/java/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java rename to reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java index 839b3072c4..fe16678837 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java @@ -22,11 +22,17 @@ import reactor.core.Disposable; import reactor.core.Scannable; -final class ThreadPerTaskBoundedElasticScheduler - implements Scheduler, SchedulerState.DisposeAwaiter, Scannable { +/** + * This {@link BoundedElasticThreadPerTaskScheduler} variant is included when Reactor is + * used with JDK versions lower than 21, and all methods raise an + * {@link UnsupportedOperationException}. An alternative variant is available for use on + * JDK 21+ where virtual threads are supported. + */ +final class BoundedElasticThreadPerTaskScheduler + implements Scheduler, SchedulerState.DisposeAwaiter, Scannable { - ThreadPerTaskBoundedElasticScheduler(int maxThreads, int maxTaskQueuedPerThread, ThreadFactory factory) { - throw new UnsupportedOperationException("Unsupported in JDK lower thank 21"); + BoundedElasticThreadPerTaskScheduler(int maxThreads, int maxTaskQueuedPerThread, ThreadFactory factory) { + throw new UnsupportedOperationException("Unsupported in JDK lower than 21"); } @Override @@ -42,12 +48,12 @@ public Object scanUnsafe(Attr key) { @Override public Disposable schedule(Runnable task) { - throw new UnsupportedOperationException("Unsupported in JDK lower thank 21"); + throw new UnsupportedOperationException("Unsupported in JDK lower than 21"); } @Override public Worker createWorker() { - throw new UnsupportedOperationException("Unsupported in JDK lower thank 21"); + throw new UnsupportedOperationException("Unsupported in JDK lower than 21"); } static final class BoundedServices { @@ -55,6 +61,6 @@ private BoundedServices() { } - BoundedServices(ThreadPerTaskBoundedElasticScheduler parent) {} + BoundedServices(BoundedElasticThreadPerTaskScheduler parent) {} } } \ No newline at end of file diff --git a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java index 5d7b4465f2..9dbd985b60 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java @@ -63,6 +63,10 @@ * Factories prefixed with {@code new} (eg. {@link #newBoundedElastic(int, int, String)} return a new instance of their flavor of {@link Scheduler}, * while other factories like {@link #boundedElastic()} return a shared instance - which is the one used by operators requiring that flavor as their default Scheduler. * All instances are returned in a {@link Scheduler#init() initialized} state. + *

    + * Since 3.6.0 {@link #boundedElastic()} can run tasks on {@link VirtualThread}s if the application + * runs on a Java 21+ runtime and the {@link #DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS} + * system property is set to {@code true}. * * @author Stephane Maldini */ @@ -187,26 +191,69 @@ public static Scheduler fromExecutorService(ExecutorService executorService, Str } /** - * The common boundedElastic instance, a {@link Scheduler} that dynamically creates a bounded number of - * ExecutorService-based Workers, reusing them once the Workers have been shut down. The underlying daemon - * threads can be evicted if idle for more than {@link BoundedElasticScheduler#DEFAULT_TTL_SECONDS 60} seconds. + * The common boundedElastic instance, a {@link Scheduler} that + * dynamically creates a bounded number of workers. *

    - * The maximum number of created threads is bounded by a {@code cap} (by default + * Depends on the available environment and specified configurations, there are two types + * of implementations for this shared scheduler: + *

      + * + *
    • ExecutorService-based implementation tailored to run on Platform {@link Thread} + * instances. Every Worker is {@link ExecutorService}-based. Reusing {@link Thread}s + * once the Workers have been shut down. The underlying daemon threads can be + * evicted if idle for more than + * {@link BoundedElasticScheduler#DEFAULT_TTL_SECONDS 60} seconds. + *
    • + * + *
    • As of 3.6.0 there is a thread-per-task implementation tailored for use + * with virtual threads. This implementation is enabled if the + * application runs on a JDK 21+ runtime and the system property + * {@link #DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS} is set to + * {@code true}. Every Worker is based on the custom implementation of the execution + * mechanism which ensures every submitted task runs on a new + * {@link VirtualThread} instance. This implementation has a shared instance of + * {@link ScheduledExecutorService} used to schedule delayed and periodic tasks + * such that when triggered they are offloaded to a dedicated new + * {@link VirtualThread} instance. + *
    • + * + *
    + * + *

    + * Both implementations share the same configurations: + *

      + *
    • + * The maximum number of concurrent + * threads is bounded by a {@code cap} (by default * ten times the number of available CPU cores, see {@link #DEFAULT_BOUNDED_ELASTIC_SIZE}). + *

      + * Note: Consider increasing {@link #DEFAULT_BOUNDED_ELASTIC_SIZE} with the + * thread-per-task implementation to run more concurrent {@link VirtualThread} + * instances underneath. + *

    • + *
    • * The maximum number of task submissions that can be enqueued and deferred on each of these * backing threads is bounded (by default 100K additional tasks, see * {@link #DEFAULT_BOUNDED_ELASTIC_QUEUESIZE}). Past that point, a {@link RejectedExecutionException} * is thrown. + *
    • + *
    + * *

    - * By order of preference, threads backing a new {@link reactor.core.scheduler.Scheduler.Worker} are - * picked from the idle pool, created anew or reused from the busy pool. In the later case, a best effort - * attempt at picking the thread backing the least amount of workers is made. + * Threads backing a new {@link reactor.core.scheduler.Scheduler.Worker} are + * picked from a pool or are created when needed. In the ExecutorService-based + * implementation, the pool is comprised either of idle or busy threads. When all + * threads are busy, a best effort attempt is made at picking the thread backing + * the least number of workers. In the case of the thread-per-task implementation, it + * always creates new threads up to the specified limit. *

    - * Note that if a thread is backing a low amount of workers, but these workers submit a lot of pending tasks, - * a second worker could end up being backed by the same thread and see tasks rejected. - * The picking of the backing thread is also done once and for all at worker creation, so - * tasks could be delayed due to two workers sharing the same backing thread and submitting long-running tasks, - * despite another backing thread becoming idle in the meantime. + * Note that if a scheduling mechanism is backing a low amount of workers, but these + * workers submit a lot of pending tasks, a second worker could end up being + * backed by the same mechanism and see tasks rejected. + * The picking of the backing mechanism is also done once and for all at worker + * creation, so tasks could be delayed due to two workers sharing the same backing + * mechanism and submitting long-running tasks, despite another backing mechanism + * becoming idle in the meantime. *

    * Only one instance of this common scheduler will be created on the first call and is cached. The same instance * is returned on subsequent calls until it is disposed. @@ -215,9 +262,12 @@ public static Scheduler fromExecutorService(ExecutorService executorService, Str * between callers. They can however be all {@link #shutdownNow() shut down} together, or replaced by a * {@link #setFactory(Factory) change in Factory}. * - * @return the common boundedElastic instance, a {@link Scheduler} that dynamically creates workers with - * an upper bound to the number of backing threads and after that on the number of enqueued tasks, that reuses - * threads and evict idle ones + *

    + * + * @return the ExecutorService/thread-per-task-based boundedElastic + * instance. + * A {@link Scheduler} that dynamically creates workers with an upper + * bound to the number of backing threads and after that on the number of enqueued tasks. */ public static Scheduler boundedElastic() { return cache(CACHED_BOUNDED_ELASTIC, BOUNDED_ELASTIC, BOUNDED_ELASTIC_SUPPLIER); @@ -279,6 +329,12 @@ public static Scheduler immediate() { * from exiting until their worker has been disposed AND they've been evicted by TTL, or the whole * scheduler has been {@link Scheduler#dispose() disposed}. * + *

    + * Please note, this implementation is not designed to run tasks on + * {@link VirtualThread}. Please see + * {@link Factory#newThreadPerTaskBoundedElastic(int, int, ThreadFactory)} if you need + * {@link VirtualThread} compatible scheduler implementation + * * @param threadCap maximum number of underlying threads to create * @param queuedTaskCap maximum number of tasks to enqueue when no more threads can be created. Can be {@link Integer#MAX_VALUE} for unbounded enqueueing. * @param name Thread prefix @@ -314,6 +370,12 @@ public static Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, Stri * from exiting until their worker has been disposed AND they've been evicted by TTL, or the whole * scheduler has been {@link Scheduler#dispose() disposed}. * + *

    + * Please note, this implementation is not designed to run tasks on + * {@link VirtualThread}. Please see + * {@link Factory#newThreadPerTaskBoundedElastic(int, int, ThreadFactory)} if you need + * {@link VirtualThread} compatible scheduler implementation + * * @param threadCap maximum number of underlying threads to create * @param queuedTaskCap maximum number of tasks to enqueue when no more threads can be created. Can be {@link Integer#MAX_VALUE} for unbounded enqueueing. * @param name Thread prefix @@ -351,6 +413,12 @@ public static Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, Stri * worker has been disposed AND they've been evicted by TTL, or the whole scheduler has been * {@link Scheduler#dispose() disposed}. * + *

    + * Please note, this implementation is not designed to run tasks on + * {@link VirtualThread}. Please see + * {@link Factory#newThreadPerTaskBoundedElastic(int, int, ThreadFactory)} if you need + * {@link VirtualThread} compatible scheduler implementation + * * @param threadCap maximum number of underlying threads to create * @param queuedTaskCap maximum number of tasks to enqueue when no more threads can be created. Can be {@link Integer#MAX_VALUE} for unbounded enqueueing. * @param name Thread prefix @@ -392,6 +460,12 @@ public static Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, Stri * will prevent the JVM from exiting until their worker has been disposed AND they've been evicted by TTL, * or the whole scheduler has been {@link Scheduler#dispose() disposed}. * + *

    + * Please note, this implementation is not designed to run tasks on + * {@link VirtualThread}. Please see + * {@link Factory#newThreadPerTaskBoundedElastic(int, int, ThreadFactory)} if you need + * {@link VirtualThread} compatible scheduler implementation + * * @param threadCap maximum number of underlying threads to create * @param queuedTaskCap maximum number of tasks to enqueue when no more threads can be created. Can be {@link Integer#MAX_VALUE} for unbounded enqueueing. * @param threadFactory a {@link ThreadFactory} to use each thread initialization @@ -409,14 +483,6 @@ public static Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, Thre return fromFactory; } - static Scheduler newThreadPerTaskBoundedElastic(int threadCap, int queuedTaskCap, ThreadFactory threadFactory) { - Scheduler fromFactory = factory.newThreadPerTaskBoundedElastic(threadCap, - queuedTaskCap, - threadFactory); - fromFactory.init(); - return fromFactory; - } - /** * {@link Scheduler} that hosts a fixed pool of single-threaded ExecutorService-based * workers and is suited for parallel work. This type of {@link Scheduler} detects and @@ -1014,21 +1080,23 @@ default Scheduler newBoundedElastic(int threadCap, int queuedTaskCap, ThreadFact * The maximum number of created thread pools is bounded by the provided {@code threadCap}. *

    * The main difference between {@link BoundedElasticScheduler} and - * {@link ThreadPerTaskBoundedElasticScheduler} is that underlying machinery + * {@link BoundedElasticThreadPerTaskScheduler} is that underlying machinery * allocates a new thread for every new task which is one of the requirements * for usage with {@link VirtualThread}s *

    - * Note: for now this scheduler is available only in Java 21 runtime + * Note: for now this scheduler is available only in Java 21+ runtime * * @param threadCap maximum number of underlying threads to create * @param queuedTaskCap maximum number of tasks to enqueue when no more threads can be created. Can be {@link Integer#MAX_VALUE} for unbounded enqueueing. * @param threadFactory a {@link ThreadFactory} to use each thread initialization * + * @since 3.6.0 + * * @return a new {@link Scheduler} that dynamically creates workers with an upper bound to * the number of backing threads */ default Scheduler newThreadPerTaskBoundedElastic(int threadCap, int queuedTaskCap, ThreadFactory threadFactory) { - return new ThreadPerTaskBoundedElasticScheduler(threadCap, queuedTaskCap, threadFactory); + return new BoundedElasticThreadPerTaskScheduler(threadCap, queuedTaskCap, threadFactory); } /** diff --git a/reactor-core/src/main/java/reactor/core/scheduler/VirtualThreadFactory.java b/reactor-core/src/main/java/reactor/core/scheduler/VirtualThreadFactory.java index f4b1563c1c..7ff22c9993 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/VirtualThreadFactory.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/VirtualThreadFactory.java @@ -17,18 +17,19 @@ package reactor.core.scheduler; import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; -import java.util.function.Supplier; import reactor.util.annotation.NonNull; import reactor.util.annotation.Nullable; /** * The noop {@link VirtualThread} Reactor {@link ThreadFactory} to be - * used with {@link ThreadPerTaskBoundedElasticScheduler}. It throws exceptions when is - * being created, so it indicates that current Java Runtime does not support - * {@link VirtualThread}s. + * used with {@link BoundedElasticThreadPerTaskScheduler}. + * This {@link VirtualThreadFactory} variant is included when Reactor is used with + * JDK versions lower than 21, + * and all methods raise an {@link UnsupportedOperationException}. + * An alternative variant is available for use on JDK 21+ + * where virtual threads are supported. * * @author Oleh Dokuka */ diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java index 44dd4bb5d7..8a7892a6ad 100644 --- a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java +++ b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java @@ -24,10 +24,10 @@ import static reactor.core.scheduler.Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE; import static reactor.core.scheduler.Schedulers.LOOM_BOUNDED_ELASTIC; import static reactor.core.scheduler.Schedulers.newBoundedElastic; -import static reactor.core.scheduler.Schedulers.newThreadPerTaskBoundedElastic; +import static reactor.core.scheduler.Schedulers.factory; /** - * JDK 8 Specific implementation of BoundedElasticScheduler supplier which uses + * JDK 21+ Specific implementation of BoundedElasticScheduler supplier which uses * {@link java.lang.ThreadBuilders.VirtualThreadFactory} instead of the default * {@link ReactorThreadFactory} when one enables virtual thread support */ @@ -36,7 +36,7 @@ class BoundedElasticSchedulerSupplier implements Supplier { @Override public Scheduler get() { if (DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS) { - return newThreadPerTaskBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, + return factory.newThreadPerTaskBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, Thread.ofVirtual() .name(LOOM_BOUNDED_ELASTIC + "-", 1) diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java similarity index 97% rename from reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java rename to reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java index a8d0b777e7..def20a1e0b 100644 --- a/reactor-core/src/main/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticScheduler.java +++ b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java @@ -47,15 +47,15 @@ import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; -import static reactor.core.scheduler.ThreadPerTaskBoundedElasticScheduler.BoundedServices.CREATING; -import static reactor.core.scheduler.ThreadPerTaskBoundedElasticScheduler.BoundedServices.SHUTDOWN; -import static reactor.core.scheduler.ThreadPerTaskBoundedElasticScheduler.BoundedServices; +import static reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler.BoundedServices.CREATING; +import static reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler.BoundedServices.SHUTDOWN; +import static reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler.BoundedServices; import static reactor.core.scheduler.Schedulers.onSchedule; -final class ThreadPerTaskBoundedElasticScheduler +final class BoundedElasticThreadPerTaskScheduler implements Scheduler, SchedulerState.DisposeAwaiter, Scannable { - static final Logger LOGGER = Loggers.getLogger(ThreadPerTaskBoundedElasticScheduler.class); + static final Logger LOGGER = Loggers.getLogger(BoundedElasticThreadPerTaskScheduler.class); final int maxThreads; final int maxTasksQueuedPerThread; @@ -64,8 +64,8 @@ final class ThreadPerTaskBoundedElasticScheduler volatile SchedulerState state; @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater STATE = - AtomicReferenceFieldUpdater.newUpdater(ThreadPerTaskBoundedElasticScheduler.class, SchedulerState.class, "state"); + static final AtomicReferenceFieldUpdaterSTATE = + AtomicReferenceFieldUpdater.newUpdater(BoundedElasticThreadPerTaskScheduler.class, SchedulerState.class, "state"); private static final SchedulerState INIT = SchedulerState.init(SHUTDOWN); @@ -73,7 +73,7 @@ final class ThreadPerTaskBoundedElasticScheduler /** - * Create a {@link ThreadPerTaskBoundedElasticScheduler} with the given configuration. Note that backing threads + * Create a {@link BoundedElasticThreadPerTaskScheduler} with the given configuration. Note that backing threads * (or executors) can be shared by each {@link reactor.core.scheduler.Scheduler.Worker}, so each worker * can contribute to the task queue size. * @@ -81,7 +81,7 @@ final class ThreadPerTaskBoundedElasticScheduler * @param maxTasksQueuedPerThread the maximum amount of tasks an executor can queue up * @param threadFactory the {@link ThreadFactory} to name the backing threads */ - ThreadPerTaskBoundedElasticScheduler(int maxThreads, int maxTasksQueuedPerThread, ThreadFactory threadFactory) { + BoundedElasticThreadPerTaskScheduler(int maxThreads, int maxTasksQueuedPerThread, ThreadFactory threadFactory) { if (maxThreads <= 0) { throw new IllegalArgumentException("maxThreads must be strictly positive, was " + maxThreads); } @@ -361,7 +361,7 @@ public String toString() { return t; }; - final ThreadPerTaskBoundedElasticScheduler parent; + final BoundedElasticThreadPerTaskScheduler parent; final ScheduledExecutorService sharedDelayedTasksScheduler; final ThreadFactory factory; final int maxTasksQueuedPerThread; @@ -380,7 +380,7 @@ private BoundedServices() { this.sharedDelayedTasksScheduler = DELAYED_TASKS_SCHEDULER_SHUTDOWN; } - BoundedServices(ThreadPerTaskBoundedElasticScheduler parent) { + BoundedServices(BoundedElasticThreadPerTaskScheduler parent) { this.parent = parent; this.maxTasksQueuedPerThread = parent.maxTasksQueuedPerThread; this.factory = parent.factory; diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/VirtualThreadFactory.java b/reactor-core/src/main/java21/reactor/core/scheduler/VirtualThreadFactory.java index 5dbbf1371e..03c0946ab6 100644 --- a/reactor-core/src/main/java21/reactor/core/scheduler/VirtualThreadFactory.java +++ b/reactor-core/src/main/java21/reactor/core/scheduler/VirtualThreadFactory.java @@ -18,14 +18,13 @@ import java.util.concurrent.ThreadFactory; import java.util.function.BiConsumer; -import java.util.function.Supplier; import reactor.util.annotation.NonNull; import reactor.util.annotation.Nullable; /** * The {@link VirtualThread} Reactor {@link ThreadFactory} to be used with - * {@link ThreadPerTaskBoundedElasticScheduler}, delegates all allocations to real {@link + * {@link BoundedElasticThreadPerTaskScheduler}, delegates all allocations to real {@link * java.lang.ThreadBuilders.VirtualThreadFactory} * * @author Oleh Dokuka diff --git a/reactor-core/src/test/java/reactor/core/scheduler/GenericThreadPerTaskBoundedElasticSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/GenericBoundedElasticThreadPerTaskSchedulerTest.java similarity index 85% rename from reactor-core/src/test/java/reactor/core/scheduler/GenericThreadPerTaskBoundedElasticSchedulerTest.java rename to reactor-core/src/test/java/reactor/core/scheduler/GenericBoundedElasticThreadPerTaskSchedulerTest.java index 8703829a59..e2339e9d3d 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/GenericThreadPerTaskBoundedElasticSchedulerTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/GenericBoundedElasticThreadPerTaskSchedulerTest.java @@ -19,7 +19,7 @@ import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.Assertions; -class GenericThreadPerTaskBoundedElasticSchedulerTest extends AbstractSchedulerTest { +class GenericBoundedElasticThreadPerTaskSchedulerTest extends AbstractSchedulerTest { static boolean SUPPORTED; @@ -37,16 +37,16 @@ class GenericThreadPerTaskBoundedElasticSchedulerTest extends AbstractSchedulerT } @Override - protected ThreadPerTaskBoundedElasticScheduler scheduler() { - ThreadPerTaskBoundedElasticScheduler test = freshScheduler(); + protected BoundedElasticThreadPerTaskScheduler scheduler() { + BoundedElasticThreadPerTaskScheduler test = freshScheduler(); test.init(); return test; } @Override - protected ThreadPerTaskBoundedElasticScheduler freshScheduler() { + protected BoundedElasticThreadPerTaskScheduler freshScheduler() { Assumptions.assumeThat(SUPPORTED).isTrue(); - return new ThreadPerTaskBoundedElasticScheduler(4, + return new BoundedElasticThreadPerTaskScheduler(4, Integer.MAX_VALUE, new VirtualThreadFactory( "threadPerTaskBoundedElasticSchedulerTest", false, diff --git a/reactor-core/src/test/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticSchedulerTest.java b/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java similarity index 91% rename from reactor-core/src/test/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticSchedulerTest.java rename to reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java index 953e627220..a91b4b88b2 100644 --- a/reactor-core/src/test/java21/reactor/core/scheduler/ThreadPerTaskBoundedElasticSchedulerTest.java +++ b/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java @@ -22,16 +22,11 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import com.pivovarit.function.ThrowingRunnable; import org.assertj.core.api.Assertions; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; @@ -43,10 +38,10 @@ import reactor.test.util.RaceTestUtils; import reactor.util.concurrent.Queues; -class ThreadPerTaskBoundedElasticSchedulerTest { +class BoundedElasticThreadPerTaskSchedulerTest { - ThreadPerTaskBoundedElasticScheduler scheduler; - List disposables = new ArrayList<>(); + BoundedElasticThreadPerTaskScheduler scheduler; + List disposables = new ArrayList<>(); @BeforeEach void setup() { @@ -60,9 +55,9 @@ void teardown() { disposables.clear(); } - ThreadPerTaskBoundedElasticScheduler newScheduler(int maxThreads, int maxCapacity) { - ThreadPerTaskBoundedElasticScheduler scheduler = - new ThreadPerTaskBoundedElasticScheduler(maxThreads, + BoundedElasticThreadPerTaskScheduler newScheduler(int maxThreads, int maxCapacity) { + BoundedElasticThreadPerTaskScheduler scheduler = + new BoundedElasticThreadPerTaskScheduler(maxThreads, maxCapacity, Thread.ofVirtual() .name("virtualThreadPerTaskBoundedElasticScheduler", 1) @@ -88,7 +83,7 @@ public void ensuresTasksDelayedScheduling() throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); CountDownLatch awaiter = new CountDownLatch(1); - ThreadPerTaskBoundedElasticScheduler.BoundedServices resource = + BoundedElasticThreadPerTaskScheduler.BoundedServices resource = scheduler.state.currentResource; // submit task which occupy shared single threaded scheduler resource.sharedDelayedTasksScheduler.submit(() -> { @@ -121,7 +116,7 @@ public void ensuresTasksDelayedZeroDelayScheduling() throws InterruptedException CountDownLatch latch = new CountDownLatch(1); CountDownLatch awaiter = new CountDownLatch(1); - ThreadPerTaskBoundedElasticScheduler.BoundedServices resource = + BoundedElasticThreadPerTaskScheduler.BoundedServices resource = scheduler.state.currentResource; // submit task which occupy shared single threaded scheduler resource.sharedDelayedTasksScheduler.submit(() -> { @@ -352,7 +347,7 @@ public void ensuresConcurrentWorkerTaskDisposure() throws InterruptedException { @Test public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenWorkerIsDisposed() throws InterruptedException { - ThreadPerTaskBoundedElasticScheduler + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(2, 1000); scheduler.init(); Runnable task = () -> { @@ -396,7 +391,7 @@ public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenWorkerIsDispo @Test public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDisposed() throws InterruptedException { - ThreadPerTaskBoundedElasticScheduler + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(2, 1000); scheduler.init(); @@ -406,7 +401,7 @@ public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDi for (int i = 0; i < 100; i++) { CountDownLatch latch = new CountDownLatch(1); - ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker worker = (BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); List tasks = new ArrayList<>(); tasks.add(worker.schedule(() -> { try { @@ -431,7 +426,7 @@ public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDi Awaitility.await() .atMost(Duration.ofSeconds(5)) - .until(() -> !ThreadPerTaskBoundedElasticScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); + .until(() -> !BoundedElasticThreadPerTaskScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) .isEqualTo(2000); @@ -444,7 +439,7 @@ public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDi @Test public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDisposedDelayedCase() throws InterruptedException { - ThreadPerTaskBoundedElasticScheduler + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(2, 1000); scheduler.init(); @@ -454,7 +449,7 @@ public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDi for (int i = 0; i < 100; i++) { CountDownLatch latch = new CountDownLatch(1); - ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker worker = (BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); List tasks = new ArrayList<>(); tasks.add(worker.schedule(() -> { try { @@ -479,7 +474,7 @@ public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDi Awaitility.await() .atMost(Duration.ofSeconds(5)) - .until(() -> !ThreadPerTaskBoundedElasticScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); + .until(() -> !BoundedElasticThreadPerTaskScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) .isEqualTo(2000); @@ -492,7 +487,7 @@ public void ensuresTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDi @Test public void ensuresRandomTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTasksAreDisposed() throws InterruptedException { - ThreadPerTaskBoundedElasticScheduler + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(2, 10000); scheduler.init(); Runnable task = () -> { @@ -501,7 +496,7 @@ public void ensuresRandomTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTask for (int i = 0; i < 100; i++) { CountDownLatch latch = new CountDownLatch(1); - ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker worker = (BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); List tasks = new ArrayList<>(); tasks.add(worker.schedule(() -> { try { @@ -543,7 +538,7 @@ public void ensuresRandomTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTask Awaitility.await() .atMost(Duration.ofSeconds(5)) - .until(() -> !ThreadPerTaskBoundedElasticScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); + .until(() -> !BoundedElasticThreadPerTaskScheduler.SequentialThreadPerTaskExecutor.hasWork(worker.executor.wipAndRefCnt)); Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()) .isEqualTo(20000); @@ -556,11 +551,11 @@ public void ensuresRandomTasksAreDisposedAndQueueCounterIsDecrementedWhenAllTask @Test public void ensuresTasksAreOrderedWithinAWorker() throws InterruptedException { - ThreadPerTaskBoundedElasticScheduler scheduler = newScheduler(1, 1000); + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(1, 1000); scheduler.init(); - ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = - (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker worker = + (BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); ConcurrentLinkedQueue tasksIds = new ConcurrentLinkedQueue<>(); @@ -578,11 +573,11 @@ public void ensuresTasksAreOrderedWithinAWorker() throws InterruptedException { @Test public void ensuresDelayedTasksAreOrderedWithinAWorker() throws InterruptedException { - ThreadPerTaskBoundedElasticScheduler scheduler = newScheduler(1, 1000); + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(1, 1000); scheduler.init(); - ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = - (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker worker = + (BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); ConcurrentLinkedQueue tasksIds = new ConcurrentLinkedQueue<>(); @@ -599,11 +594,11 @@ public void ensuresDelayedTasksAreOrderedWithinAWorker() throws InterruptedExcep @Test public void ensuresWorkersAreNotIntersecting() throws InterruptedException { - ThreadPerTaskBoundedElasticScheduler scheduler = newScheduler(1, 1000); + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(1, 1000); scheduler.init(); - ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker worker = - (ThreadPerTaskBoundedElasticScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); + BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker worker = + (BoundedElasticThreadPerTaskScheduler.SingleThreadExecutorWorker) scheduler.createWorker(); AtomicInteger counter = new AtomicInteger(); @@ -652,7 +647,7 @@ public void run() { @Test public void ensuresSupportGracefulShutdown() { - ThreadPerTaskBoundedElasticScheduler scheduler = newScheduler(100, 100_000); + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(100, 100_000); scheduler.init(); AtomicInteger counter = new AtomicInteger(); @@ -708,7 +703,7 @@ public void run() { @Test void ensuresTotalTasksMathIsDoneCorrectlyInOverflow() { - ThreadPerTaskBoundedElasticScheduler scheduler = + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(10, Integer.MAX_VALUE - 1); scheduler.init(); @@ -735,7 +730,7 @@ void ensuresTotalTasksMathIsDoneCorrectlyInOverflow() { @Test void ensuresTotalTasksMathIsDoneCorrectlyInEdgeCase() { - ThreadPerTaskBoundedElasticScheduler scheduler = + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(10, Integer.MAX_VALUE / 10 + 1); scheduler.init(); From 06bf103de8db24d3e3f118f3cdfb257e8304b8a0 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 15 Nov 2023 12:20:23 +0200 Subject: [PATCH 212/312] ensures scheduler is init Signed-off-by: Oleh Dokuka --- .../core/scheduler/BoundedElasticSchedulerSupplier.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java index 8a7892a6ad..ae3a7c90dc 100644 --- a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java +++ b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticSchedulerSupplier.java @@ -36,12 +36,15 @@ class BoundedElasticSchedulerSupplier implements Supplier { @Override public Scheduler get() { if (DEFAULT_BOUNDED_ELASTIC_ON_VIRTUAL_THREADS) { - return factory.newThreadPerTaskBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, + Scheduler scheduler = factory.newThreadPerTaskBoundedElastic( + DEFAULT_BOUNDED_ELASTIC_SIZE, DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, Thread.ofVirtual() .name(LOOM_BOUNDED_ELASTIC + "-", 1) .uncaughtExceptionHandler(Schedulers::defaultUncaughtException) .factory()); + scheduler.init(); + return scheduler; } return newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, From 4f332e5ea0d3e3d5da5f3b252fe3dc5cd45d2e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 21 Nov 2023 14:34:24 +0100 Subject: [PATCH 213/312] SinkManyUnicast discard support during subscription cancel (#3641) When subscription to SinkManyUnicast is cancelled at the time of being delivered, the discarding fails, as the actual Subscriber is not yet assigned. This change reorders the operations so that the actual is stored properly before delivering the subscription. --- .../publisher/SinkManyUnicastStressTest.java | 62 +++++++++++++++++++ .../core/publisher/SinkManyUnicast.java | 11 ++-- .../core/publisher/SinkManyUnicastTest.java | 24 ++++++- .../test/publisher/BaseOperatorTest.java | 12 +--- 4 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 reactor-core/src/jcstress/java/reactor/core/publisher/SinkManyUnicastStressTest.java diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/SinkManyUnicastStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/SinkManyUnicastStressTest.java new file mode 100644 index 0000000000..6d17412371 --- /dev/null +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/SinkManyUnicastStressTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.Expect; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.I_Result; + +public class SinkManyUnicastStressTest { + + @JCStressTest + @Outcome(id = {"1"}, expect = Expect.ACCEPTABLE, desc = "Item delivered") + @State + public static class ParallelSubscribeAndEmit { + + final Sinks.Many sink = + Sinks.many().unicast().onBackpressureBuffer(); + + final StressSubscriber subscriber = new StressSubscriber<>(); + + @Actor + public void subscribe() { + sink.asFlux() + // Force request bump before the subscription is actually delivered. + // Otherwise, there is no race, because emitNext would not deliver due + // to lack of requests. + .doOnSubscribe(s -> s.request(Long.MAX_VALUE)) + .subscribe(subscriber); + } + + @Actor + public void emit() { + sink.tryEmitNext("Test"); + } + + @Arbiter + public void arbiter(I_Result result) { + result.r1 = subscriber.onNextCalls.get(); + if (subscriber.concurrentOnSubscribe.get() || subscriber.concurrentOnNext.get()) { + throw new IllegalStateException("Concurrent onSubscribe with onNext"); + } + } + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java index d266bd3cd4..d50a72d1c9 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -130,6 +130,7 @@ static SinkManyUnicast create(Queue queue, Disposable endcallback) { AtomicReferenceFieldUpdater.newUpdater(SinkManyUnicast.class, Disposable.class, "onTerminate"); volatile boolean done; + volatile boolean subscriptionDelivered; Throwable error; boolean hasDownstream; //important to not loose the downstream too early and miss discard hook, while having relevant hasDownstreams() @@ -355,9 +356,8 @@ else if (done) { int missed = 1; for (;;) { - CoreSubscriber a = actual; - if (a != null) { - + if (subscriptionDelivered) { + CoreSubscriber a = actual; if (outputFused) { drainFused(a); } else { @@ -411,8 +411,9 @@ public void subscribe(CoreSubscriber actual) { if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { this.hasDownstream = true; - actual.onSubscribe(this); this.actual = actual; + actual.onSubscribe(this); + subscriptionDelivered = true; if (cancelled) { this.hasDownstream = false; } else { diff --git a/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastTest.java index 49e347e117..87637b289a 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SinkManyUnicastTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; @@ -27,6 +29,7 @@ import org.junit.jupiter.api.Test; +import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.Exceptions; @@ -276,6 +279,25 @@ public void emitNextWithNoSubscriberAndBoundedQueueIgnoresValueAndKeepsSinkOpen( .verifyComplete(); } + @Test + public void emitWithoutSubscriberAndSubscribeCancellingSubscriptionDiscards() { + Sinks.Many sink = Sinks.many() + .unicast() + .onBackpressureBuffer(); + + sink.tryEmitNext("Hello"); + + List discarded = new CopyOnWriteArrayList(); + + sink.asFlux() + .doOnSubscribe(Subscription::cancel) + .contextWrite(ctx -> Operators.enableOnDiscard(ctx, + item -> discarded.add((String) item))) + .subscribe(); + + assertThat(discarded).containsExactly("Hello"); + } + @Test public void scanTerminatedCancelled() { Sinks.Many sink = SinkManyUnicast.create(); diff --git a/reactor-core/src/test/java/reactor/test/publisher/BaseOperatorTest.java b/reactor-core/src/test/java/reactor/test/publisher/BaseOperatorTest.java index ded2f2a375..9d931df478 100644 --- a/reactor-core/src/test/java/reactor/test/publisher/BaseOperatorTest.java +++ b/reactor-core/src/test/java/reactor/test/publisher/BaseOperatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -595,20 +595,14 @@ final void inputFusedAsyncOutputFusedAsyncCancel(OperatorScenario Sinks.Many up = Sinks.unsafe().many().unicast().onBackpressureBuffer(); testUnicastSource(scenario, up); StepVerifier.create(scenario.body() - .apply(withFluxSource(up.asFlux())), 0) + .apply(withFluxSource(up.asFlux())), scenario.producing) .consumeSubscriptionWith(s -> { if (s instanceof Fuseable.QueueSubscription) { @SuppressWarnings("unchecked") Fuseable.QueueSubscription qs = ((Fuseable.QueueSubscription) s); qs.requestFusion(ASYNC); - //UnicastProcessor#actual - if (up.scan(Attr.ACTUAL) != qs || scenario.prefetch() == -1) { - qs.size(); //touch undeterministic - } - else { - assertThat(qs.size()).isEqualTo(up.scan(Attr.BUFFERED)); //UnicastProcessor#size - } try { + // Assuming scenario.producing >= 3 qs.poll(); qs.poll(); qs.poll(); From 609e037b1e10751a444b79e32872ba2a28fea09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 21 Nov 2023 14:40:34 +0100 Subject: [PATCH 214/312] StressSubscriber discardedValues should not have downstream type (#3643) StressSubscriber.discardedValues should not be of the type of the downstream, as the discarded values come from the upstream. This change simply treats these values as Object to allow for flexibility and avoid complicating this test util. --- .../java/reactor/core/publisher/StressSubscriber.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/StressSubscriber.java b/reactor-core/src/jcstress/java/reactor/core/publisher/StressSubscriber.java index cc69fefc36..9c1e691a4a 100644 --- a/reactor-core/src/jcstress/java/reactor/core/publisher/StressSubscriber.java +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/StressSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2021-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ enum Operation { public final AtomicInteger onNextDiscarded = new AtomicInteger(); - public final List discardedValues = new CopyOnWriteArrayList<>(); + public final List discardedValues = new CopyOnWriteArrayList<>(); public final AtomicInteger onErrorCalls = new AtomicInteger(); @@ -101,7 +101,7 @@ public StressSubscriber(long initRequest) { }), (value) -> { onNextDiscarded.incrementAndGet(); - discardedValues.add((T) value); + discardedValues.add(value); }); } From 1bee07c56b955e2102a884fb2d3b82f2deee64e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 22 Nov 2023 09:41:59 +0100 Subject: [PATCH 215/312] Post-merge fix for SinkManyUnicast context propagation --- .../src/main/java/reactor/core/publisher/SinkManyUnicast.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java index 85e7ae7024..a62bbba146 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManyUnicast.java @@ -414,7 +414,7 @@ public void subscribe(CoreSubscriber actual) { if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { this.hasDownstream = true; - this.actual = actual; + this.actual = wrapped; wrapped.onSubscribe(this); subscriptionDelivered = true; if (cancelled) { From 94e431cbb1fb4b6194de7fdaf0304c604d82eb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 22 Nov 2023 15:23:34 +0100 Subject: [PATCH 216/312] Add test label to release notes template (#3646) --- .github/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/release.yml b/.github/release.yml index 8ab5a3ed3e..8f4a94b1eb 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -20,6 +20,7 @@ changelog: labels: - "type/documentation" - "type/chores" + - "type/test" - title: ":up: Dependency Upgrades" labels: - "type/dependency-upgrade" From 12770c7909aae6cd9f7dd9fbdb7cd42b3bae6471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 27 Nov 2023 13:31:12 +0100 Subject: [PATCH 217/312] Adding Pull Request template (#3650) With the template, contributors can learn our conventions and strategy for squash merge commit so that the PR description is properly formatted. --- .github/pull_request_template.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..8dee82b7aa --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,22 @@ + + + + + + + + + + + + + From 7bf08b139a5fd28872d80ed225671d3490f29434 Mon Sep 17 00:00:00 2001 From: Navaneeth Sen Date: Wed, 29 Nov 2023 15:26:20 +0100 Subject: [PATCH 218/312] [doc] reactiveProgramming.adoc code sample polish (#3592) Reworded task to statistic in CompletableFuture sample. --- docs/asciidoc/reactiveProgramming.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/asciidoc/reactiveProgramming.adoc b/docs/asciidoc/reactiveProgramming.adoc index 11fa48d555..fb0e298d89 100644 --- a/docs/asciidoc/reactiveProgramming.adoc +++ b/docs/asciidoc/reactiveProgramming.adoc @@ -261,7 +261,7 @@ assertThat(results).contains( <2> We want to start some deeper asynchronous processing once we get the list. <3> For each element in the list: <4> Asynchronously get the associated name. -<5> Asynchronously get the associated task. +<5> Asynchronously get the associated statistic. <6> Combine both results. <7> We now have a list of futures that represent all the combination tasks. To execute these tasks, we need to convert the list to an array. From c6220fc5b6eae033b89a11455e2d2ad395353982 Mon Sep 17 00:00:00 2001 From: ByoungJoonIm Date: Wed, 29 Nov 2023 23:30:49 +0900 Subject: [PATCH 219/312] [doc] added @Override annotations in sample java code (#3605) Methods that override superclass' methods now have @Override annotations in subscribe-details.adoc Signed-off-by: ByoungJoonIm --- docs/asciidoc/subscribe-details.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/asciidoc/subscribe-details.adoc b/docs/asciidoc/subscribe-details.adoc index 7cd9889e91..e0328f1f02 100644 --- a/docs/asciidoc/subscribe-details.adoc +++ b/docs/asciidoc/subscribe-details.adoc @@ -170,11 +170,13 @@ import reactor.core.publisher.BaseSubscriber; public class SampleSubscriber extends BaseSubscriber { + @Override public void hookOnSubscribe(Subscription subscription) { System.out.println("Subscribed"); request(1); } + @Override public void hookOnNext(T value) { System.out.println(value); request(1); From 3c3fc779aef6151ef64d24df3f945c102023edbe Mon Sep 17 00:00:00 2001 From: Subba Rao Pasupuleti Date: Thu, 30 Nov 2023 07:35:12 -0500 Subject: [PATCH 220/312] [polish] final method declaration in final class (#3461) Co-authored-by: Subba Rao Pasupuleti --- .../java/reactor/core/publisher/SinkManySerialized.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/SinkManySerialized.java b/reactor-core/src/main/java/reactor/core/publisher/SinkManySerialized.java index 940708b11a..c2a3d7be3a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/SinkManySerialized.java +++ b/reactor-core/src/main/java/reactor/core/publisher/SinkManySerialized.java @@ -54,7 +54,7 @@ public boolean isCancelled() { } @Override - public final Sinks.EmitResult tryEmitComplete() { + public Sinks.EmitResult tryEmitComplete() { Thread currentThread = Thread.currentThread(); if (!tryAcquire(currentThread)) { return Sinks.EmitResult.FAIL_NON_SERIALIZED; @@ -70,7 +70,7 @@ public final Sinks.EmitResult tryEmitComplete() { } @Override - public final Sinks.EmitResult tryEmitError(Throwable t) { + public Sinks.EmitResult tryEmitError(Throwable t) { Objects.requireNonNull(t, "t is null in sink.error(t)"); Thread currentThread = Thread.currentThread(); @@ -88,7 +88,7 @@ public final Sinks.EmitResult tryEmitError(Throwable t) { } @Override - public final Sinks.EmitResult tryEmitNext(T t) { + public Sinks.EmitResult tryEmitNext(T t) { Objects.requireNonNull(t, "t is null in sink.next(t)"); Thread currentThread = Thread.currentThread(); From 9d0f122ab0d0bcaeb8ab2769be1c195576c05e5a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Mon, 11 Dec 2023 22:09:40 +0200 Subject: [PATCH 221/312] bump micrometer libs versions (#3662) Signed-off-by: OlegDokuka --- gradle/libs.versions.toml | 4 ++-- settings.gradle | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 50c5a2deb1..f2bcf854cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,9 +14,9 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.0" +micrometer = "1.12.1" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.0" +micrometerTracingTest="1.2.1" contextPropagation="1.1.0" kotlin = "1.5.32" reactiveStreams = "1.0.4" diff --git a/settings.gradle b/settings.gradle index 7405c35b84..e0985fffa5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,10 +26,10 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.12.0-SNAPSHOT') + version('micrometer', '1.12.2-SNAPSHOT') version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") - version('micrometerTracingTest', "1.2.0-SNAPSHOT") - version('contextPropagation', "1.0.6-SNAPSHOT") + version('micrometerTracingTest', "1.2.2-SNAPSHOT") + version('contextPropagation', "1.1.1-SNAPSHOT") } } } From cc2ad6edac0fffe3f60adc8debc1ae8b6347bf6d Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Dec 2023 09:00:42 +0200 Subject: [PATCH 222/312] [release] Prepare and release 3.5.13 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e6ad93fdda..cc51b0f72b 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.12" - testCompile "io.projectreactor:reactor-test:3.5.12" + compile "io.projectreactor:reactor-core:3.5.13" + testCompile "io.projectreactor:reactor-test:3.5.13" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.13-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.13-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.14-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.14-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.12" + // implementation "io.projectreactor:reactor-tools:3.5.13" } ``` diff --git a/gradle.properties b/gradle.properties index 2b2ee10a18..3abe18df73 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.13-SNAPSHOT -bomVersion=2022.0.13 -metricsMicrometerVersion=1.0.13-SNAPSHOT +version=3.5.13 +bomVersion=2022.0.14 +metricsMicrometerVersion=1.0.13 From 943c0d2f1bde6dacc6dbec2cb81d459801aa07e8 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Dec 2023 10:13:05 +0200 Subject: [PATCH 223/312] [release] Next development version 3.5.14-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3abe18df73..8d75c7fbae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.13 +version=3.5.14-SNAPSHOT bomVersion=2022.0.14 -metricsMicrometerVersion=1.0.13 +metricsMicrometerVersion=1.0.14-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ac159c27f..7f035d4844 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.12" -baselinePerfCore = "3.5.12" +baseline-core-api = "3.5.13" +baselinePerfCore = "3.5.13" baselinePerfExtra = "3.5.1" # Other shared versions From 70b912d3dc96d32e41d1cdb8ee67d418e2007f08 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Dec 2023 10:52:51 +0200 Subject: [PATCH 224/312] [release] Prepare and release 3.6.1 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fa6ebbb469..0953bbb2ed 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.0" - testCompile "io.projectreactor:reactor-test:3.6.0" + compile "io.projectreactor:reactor-core:3.6.1" + testCompile "io.projectreactor:reactor-test:3.6.1" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.1-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.1-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.2-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.2-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.0" + // implementation "io.projectreactor:reactor-tools:3.6.1" } ``` diff --git a/gradle.properties b/gradle.properties index 5a820702ce..384e6ba249 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.1-SNAPSHOT -bomVersion=2023.0.0 -metricsMicrometerVersion=1.1.1-SNAPSHOT +version=3.6.1 +bomVersion=2023.0.1 +metricsMicrometerVersion=1.1.1 From 0b0f97eba230910c63ebfb7a877d6744ad157c0a Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Dec 2023 13:22:06 +0200 Subject: [PATCH 225/312] [release] Next development version 3.6.2-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 384e6ba249..5abf0e8a74 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.1 +version=3.6.2-SNAPSHOT bomVersion=2023.0.1 -metricsMicrometerVersion=1.1.1 +metricsMicrometerVersion=1.1.2-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f2bcf854cf..74ef4138f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.0" -baselinePerfCore = "3.6.0" +baseline-core-api = "3.6.1" +baselinePerfCore = "3.6.1" baselinePerfExtra = "3.5.1" # Other shared versions From 24f04bc7dad40ae9c48499c6cfbf5c85a1468669 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka <5380167+OlegDokuka@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:32:03 +0200 Subject: [PATCH 226/312] ensure error is not propagated on cancellation and onNext race (#3665) this adds extra `if (cancelled)` statement which ensures the `RejectedExecutionException` is not appearing --------- Signed-off-by: OlegDokuka --- .../publisher/FluxPublishOnStressTest.java | 112 ++++++++++++++++++ .../reactor/core/publisher/FluxPublishOn.java | 14 ++- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 reactor-core/src/jcstress/java/reactor/core/publisher/FluxPublishOnStressTest.java diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxPublishOnStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxPublishOnStressTest.java new file mode 100644 index 0000000000..be4361e5e1 --- /dev/null +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxPublishOnStressTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.ForkJoinPool; + +import org.openjdk.jcstress.annotations.Actor; +import org.openjdk.jcstress.annotations.Arbiter; +import org.openjdk.jcstress.annotations.JCStressTest; +import org.openjdk.jcstress.annotations.Outcome; +import org.openjdk.jcstress.annotations.State; +import org.openjdk.jcstress.infra.results.II_Result; +import reactor.core.scheduler.Schedulers; +import reactor.core.scheduler.Scheduler; +import reactor.util.concurrent.Queues; + +import static org.openjdk.jcstress.annotations.Expect.ACCEPTABLE; + +public class FluxPublishOnStressTest { + + @JCStressTest + @Outcome(id = {"0, 1", "0, 0"}, expect = ACCEPTABLE, desc = "no errors propagated after cancellation because of disposed worker") + @State + public static class FluxPublishOnOnNextAndCancelRaceStressTest { + final StressSubscription upstream = new StressSubscription<>(null); + final StressSubscriber downstream = new StressSubscriber<>(); + final Scheduler scheduler = + Schedulers.fromExecutorService(ForkJoinPool.commonPool()); + + final FluxPublishOn.PublishOnSubscriber publishOnSubscriber = + new FluxPublishOn.PublishOnSubscriber<>(downstream, + scheduler, + scheduler.createWorker(), true, 32, 12, Queues.get(32)); + + + { + publishOnSubscriber.onSubscribe(upstream); + } + + @Actor + public void produce() { + publishOnSubscriber.onNext(1); + publishOnSubscriber.onNext(2); + publishOnSubscriber.onNext(3); + publishOnSubscriber.onNext(4); + } + + @Actor + public void cancel() { + publishOnSubscriber.cancel(); + } + + @Arbiter + public void arbiter(II_Result result) { + result.r1 = downstream.onErrorCalls.get(); + result.r2 = downstream.droppedErrors.size(); + } + } + + @JCStressTest + @Outcome(id = {"0, 1", "0, 0"}, expect = ACCEPTABLE, desc = "no errors propagated after cancellation because of disposed worker") + @State + public static class FluxPublishOnConditionalOnNextAndCancelRaceStressTest { + final StressSubscription upstream = new StressSubscription<>(null); + final ConditionalStressSubscriber downstream = new ConditionalStressSubscriber<>(); + final Scheduler scheduler = + Schedulers.fromExecutorService(ForkJoinPool.commonPool()); + + final FluxPublishOn.PublishOnConditionalSubscriber publishOnSubscriber = + new FluxPublishOn.PublishOnConditionalSubscriber<>(downstream, + scheduler, + scheduler.createWorker(), true, 32, 12, Queues.get(32)); + + + { + publishOnSubscriber.onSubscribe(upstream); + } + + @Actor + public void produce() { + publishOnSubscriber.onNext(1); + publishOnSubscriber.onNext(2); + publishOnSubscriber.onNext(3); + publishOnSubscriber.onNext(4); + } + + @Actor + public void cancel() { + publishOnSubscriber.cancel(); + } + + @Arbiter + public void arbiter(II_Result result) { + result.r1 = downstream.onErrorCalls.get(); + result.r2 = downstream.droppedErrors.size(); + } + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxPublishOn.java b/reactor-core/src/main/java/reactor/core/publisher/FluxPublishOn.java index ffdcad17a6..2e92b14610 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxPublishOn.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxPublishOn.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -324,6 +324,12 @@ void trySchedule( // In all other modes we are free to discard queue immediately since there is no racing on pooling Operators.onDiscardQueueWithClear(queue, actual.currentContext(), null); } + + if (cancelled) { + Operators.onErrorDropped(ree, actual.currentContext()); + return; + } + actual.onError(Operators.onRejectedExecution(ree, subscription, suppressed, dataSignal, actual.currentContext())); } @@ -884,6 +890,12 @@ void trySchedule( // In all other modes we are free to discard queue immediately since there is no racing on pooling Operators.onDiscardQueueWithClear(queue, actual.currentContext(), null); } + + if (cancelled) { + Operators.onErrorDropped(ree, actual.currentContext()); + return; + } + actual.onError(Operators.onRejectedExecution(ree, subscription, suppressed, dataSignal, actual.currentContext())); } From 856011d4138f2b6fe684f3bcb91c618052bc495b Mon Sep 17 00:00:00 2001 From: Nicolas125841 <39074410+Nicolas125841@users.noreply.github.com> Date: Thu, 14 Dec 2023 06:31:16 -0800 Subject: [PATCH 227/312] Fix pending tasks timer in TimedScheduler upon task rejection (#3660) `TimedScheduler` pending tasks count kept increasing when the underlying `Scheduler` threw `RejectedExecutionException`. This change catches the exception and immediately stops the `pendingTasks` sample. Fixes #3642 --- .../micrometer/TimedScheduler.java | 47 ++++++++++-- .../micrometer/TimedSchedulerTest.java | 76 ++++++++++++++++++- 2 files changed, 113 insertions(+), 10 deletions(-) diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java index 862d4b012b..83b261ef45 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package reactor.core.observability.micrometer; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import io.micrometer.core.instrument.Counter; @@ -76,24 +77,40 @@ final class TimedScheduler implements Scheduler { } - Runnable wrap(Runnable task) { + TimedRunnable wrap(Runnable task) { return new TimedRunnable(registry, this, task); } - Runnable wrapPeriodic(Runnable task) { + TimedRunnable wrapPeriodic(Runnable task) { return new TimedRunnable(registry, this, task, true); } @Override public Disposable schedule(Runnable task) { this.submittedDirect.increment(); - return delegate.schedule(wrap(task)); + TimedRunnable timedTask = wrap(task); + + try { + return delegate.schedule(timedTask); + } + catch (RejectedExecutionException exception) { + timedTask.pendingSample.stop(); + throw exception; + } } @Override public Disposable schedule(Runnable task, long delay, TimeUnit unit) { this.submittedDelayed.increment(); - return delegate.schedule(wrap(task), delay, unit); + TimedRunnable timedTask = wrap(task); + + try { + return delegate.schedule(timedTask, delay, unit); + } + catch (RejectedExecutionException exception) { + timedTask.pendingSample.stop(); + throw exception; + } } @Override @@ -150,13 +167,29 @@ public boolean isDisposed() { @Override public Disposable schedule(Runnable task) { parent.submittedDirect.increment(); - return delegate.schedule(parent.wrap(task)); + TimedRunnable timedTask = parent.wrap(task); + + try { + return delegate.schedule(timedTask); + } + catch (RejectedExecutionException exception) { + timedTask.pendingSample.stop(); + throw exception; + } } @Override public Disposable schedule(Runnable task, long delay, TimeUnit unit) { parent.submittedDelayed.increment(); - return delegate.schedule(parent.wrap(task), delay, unit); + TimedRunnable timedTask = parent.wrap(task); + + try { + return delegate.schedule(timedTask, delay, unit); + } + catch (RejectedExecutionException exception) { + timedTask.pendingSample.stop(); + throw exception; + } } @Override diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java index c288626799..b8242e68ac 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,16 @@ import java.time.Duration; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import io.micrometer.core.instrument.LongTaskTimer; import io.micrometer.core.instrument.MockClock; import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.search.RequiredSearch; import io.micrometer.core.instrument.simple.SimpleConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.tck.MeterRegistryAssert; @@ -37,8 +43,7 @@ import reactor.core.scheduler.Schedulers; import reactor.test.AutoDisposingExtension; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.*; /** * @author Simon Baslé @@ -324,4 +329,69 @@ void workerSchedulePeriodicallyIsCorrectlyMetered() throws InterruptedException + testScheduler.submittedPeriodicInitial.count() + testScheduler.submittedPeriodicIteration.count(), "completed tasks == sum of all timer counts"); } + + @Test + void pendingTaskRemovedOnScheduleRejection() { + CountDownLatch cdl = new CountDownLatch(1); + ExecutorService executorService = new ThreadPoolExecutor(1, 2, 0L, TimeUnit.MILLISECONDS, + new SynchronousQueue<>()); + Scheduler original = Schedulers.fromExecutorService(executorService); + TimedScheduler testScheduler = new TimedScheduler(original, registry, "test", Tags.empty()); + RequiredSearch requiredSearch = registry.get("test.scheduler.tasks.pending"); + LongTaskTimer longTaskTimer = requiredSearch.longTaskTimer(); + + Runnable supp = () -> { + try { + cdl.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + + assertThatNoException().isThrownBy(() -> testScheduler.schedule(supp)); + assertThatNoException().isThrownBy(() -> testScheduler.schedule(supp)); + assertThat(longTaskTimer.activeTasks()).as("longTaskTimer.activeTasks()").isOne(); + assertThatExceptionOfType(RejectedExecutionException.class).isThrownBy(() -> testScheduler.schedule(supp)); + assertThatExceptionOfType(RejectedExecutionException.class) + .isThrownBy(() -> testScheduler.schedule(supp, 0, TimeUnit.SECONDS)); + + cdl.countDown(); + + assertThat(longTaskTimer.activeTasks()) + .as("longTaskTimer.activeTasks()") + .isZero(); + } + + @Test + void workerPendingTaskRemovedOnScheduleRejection() { + CountDownLatch cdl = new CountDownLatch(1); + ExecutorService executorService = new ThreadPoolExecutor(1, 2, 0L, TimeUnit.MILLISECONDS, + new SynchronousQueue<>()); + Scheduler original = Schedulers.fromExecutorService(executorService); + TimedScheduler testScheduler = new TimedScheduler(original, registry, "test", Tags.empty()); + RequiredSearch requiredSearch = registry.get("test.scheduler.tasks.pending"); + LongTaskTimer longTaskTimer = requiredSearch.longTaskTimer(); + Scheduler.Worker worker = testScheduler.createWorker(); + + Runnable supp = () -> { + try { + cdl.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + + assertThatNoException().isThrownBy(() -> worker.schedule(supp)); + assertThatNoException().isThrownBy(() -> worker.schedule(supp)); + assertThat(longTaskTimer.activeTasks()).as("longTaskTimer.activeTasks()").isOne(); + assertThatExceptionOfType(RejectedExecutionException.class).isThrownBy(() -> worker.schedule(supp)); + assertThatExceptionOfType(RejectedExecutionException.class) + .isThrownBy(() -> worker.schedule(supp, 0, TimeUnit.SECONDS)); + + cdl.countDown(); + + assertThat(longTaskTimer.activeTasks()) + .as("longTaskTimer.activeTasks()") + .isZero(); + } } \ No newline at end of file From c87b2d91391921ae9cb98c57ee975f40cd207eab Mon Sep 17 00:00:00 2001 From: valery1707 Date: Fri, 15 Dec 2023 15:04:16 +0300 Subject: [PATCH 228/312] Fix sample code typos in Mono javadocs (#3657) --- reactor-core/src/main/java/reactor/core/publisher/Mono.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index cc5752426d..9ec1df5172 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -4521,7 +4521,7 @@ public final void subscribe(Subscriber actual) { * * *
    -	 * {@code mono.subscribeOn(Schedulers.parallel()).subscribe()) }
    +	 * {@code mono.subscribeOn(Schedulers.parallel()).subscribe() }
     	 * 
    * * @param scheduler a {@link Scheduler} providing the {@link Worker} where to subscribe @@ -5324,7 +5324,7 @@ static Mono doOnTerminalSignal(Mono source, * Note that this bypasses {@link Hooks#onEachOperator(String, Function) assembly hooks}. * * @param source the {@link Publisher} to wrap - * @param enforceMonoContract {@code} true to wrap publishers without assumption about their cardinality + * @param enforceMonoContract {@code true} to wrap publishers without assumption about their cardinality * (first {@link Subscriber#onNext(Object)} will cancel the source), {@code false} to behave like {@link #fromDirect(Publisher)}. * @param input upstream type * @return a wrapped {@link Mono} From 0b8f69f8d7b816a29f4a047043710b136582c235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 15 Dec 2023 14:52:57 +0100 Subject: [PATCH 229/312] Remove unintended dependency on gradle enterprise plugin (#3670) After removing the com.gradle.enterprise plugin, we discovered unused dependency on it in the 3.6.x line. This change removes this dependency from the multirelease jar setup. --- gradle/toolchains.gradle | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle index 89740bdf73..98598d14b2 100644 --- a/gradle/toolchains.gradle +++ b/gradle/toolchains.gradle @@ -109,21 +109,3 @@ pluginManager.withPlugin("io.github.reyerizo.gradle.jcstress") { } } } - -// Store resolved Toolchain JVM information as custom values in the build scan. -rootProject.ext { - resolvedMainToolchain = false - resolvedTestToolchain = false -} -gradle.taskGraph.afterTask { Task task, TaskState state -> - if (!resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) { - def metadata = task.javaCompiler.get().metadata - task.project.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") - resolvedMainToolchain = true - } - if (testToolchainConfigured() && !resolvedTestToolchain && task instanceof Test && task.javaLauncher.isPresent()) { - def metadata = task.javaLauncher.get().metadata - task.project.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") - resolvedTestToolchain = true - } -} From 336049242a595ecb19fa0900452930da3b126353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 15 Dec 2023 15:36:59 +0100 Subject: [PATCH 230/312] Fix flaky test for pending tasks count validation (#3669) Follow up to #3660 that improves the test stability. --- .../micrometer/TimedSchedulerTest.java | 138 ++++++++++++------ 1 file changed, 91 insertions(+), 47 deletions(-) diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java index b8242e68ac..b0b16fe8fb 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java @@ -17,10 +17,10 @@ package reactor.core.observability.micrometer; import java.time.Duration; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -331,67 +331,111 @@ void workerSchedulePeriodicallyIsCorrectlyMetered() throws InterruptedException } @Test - void pendingTaskRemovedOnScheduleRejection() { - CountDownLatch cdl = new CountDownLatch(1); - ExecutorService executorService = new ThreadPoolExecutor(1, 2, 0L, TimeUnit.MILLISECONDS, - new SynchronousQueue<>()); + void pendingTaskRemovedOnScheduleRejection() throws InterruptedException { + CountDownLatch activeTaskLatch = new CountDownLatch(1); + CountDownLatch pendingTaskLatch = new CountDownLatch(1); + CountDownLatch countPendingLatch = new CountDownLatch(1); + ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(1)); Scheduler original = Schedulers.fromExecutorService(executorService); TimedScheduler testScheduler = new TimedScheduler(original, registry, "test", Tags.empty()); + testScheduler.init(); + RequiredSearch requiredSearch = registry.get("test.scheduler.tasks.pending"); LongTaskTimer longTaskTimer = requiredSearch.longTaskTimer(); - Runnable supp = () -> { - try { - cdl.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }; + try { + Runnable activeTask = () -> { + try { + countPendingLatch.countDown(); + activeTaskLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + + Runnable pendingTask = pendingTaskLatch::countDown; + Runnable rejectedTask = () -> {}; + + // Schedule two tasks: one will execute, the other will wait in the queue + assertThatNoException().isThrownBy(() -> testScheduler.schedule(activeTask)); + assertThatNoException().isThrownBy(() -> testScheduler.schedule(pendingTask)); - assertThatNoException().isThrownBy(() -> testScheduler.schedule(supp)); - assertThatNoException().isThrownBy(() -> testScheduler.schedule(supp)); - assertThat(longTaskTimer.activeTasks()).as("longTaskTimer.activeTasks()").isOne(); - assertThatExceptionOfType(RejectedExecutionException.class).isThrownBy(() -> testScheduler.schedule(supp)); - assertThatExceptionOfType(RejectedExecutionException.class) - .isThrownBy(() -> testScheduler.schedule(supp, 0, TimeUnit.SECONDS)); + // Wait till first one is picked up -> exactly one is pending now + countPendingLatch.await(1, TimeUnit.SECONDS); + assertThat(longTaskTimer.activeTasks()).as("active pending") + .isOne(); - cdl.countDown(); + assertThatExceptionOfType(RejectedExecutionException.class).isThrownBy( + () -> testScheduler.schedule(rejectedTask)); + assertThatExceptionOfType(RejectedExecutionException.class).isThrownBy( + () -> testScheduler.schedule(rejectedTask, 0, TimeUnit.SECONDS)); - assertThat(longTaskTimer.activeTasks()) - .as("longTaskTimer.activeTasks()") - .isZero(); + activeTaskLatch.countDown(); + pendingTaskLatch.await(1, TimeUnit.SECONDS); + + assertThat(longTaskTimer.activeTasks()).as("active pending") + .isZero(); + } finally { + testScheduler.disposeGracefully().block(Duration.ofSeconds(1)); + } } @Test - void workerPendingTaskRemovedOnScheduleRejection() { - CountDownLatch cdl = new CountDownLatch(1); - ExecutorService executorService = new ThreadPoolExecutor(1, 2, 0L, TimeUnit.MILLISECONDS, - new SynchronousQueue<>()); + void workerPendingTaskRemovedOnScheduleRejection() throws InterruptedException { + CountDownLatch activeTaskLatch = new CountDownLatch(1); + CountDownLatch pendingTaskLatch = new CountDownLatch(1); + CountDownLatch countPendingLatch = new CountDownLatch(1); + ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(1)); Scheduler original = Schedulers.fromExecutorService(executorService); TimedScheduler testScheduler = new TimedScheduler(original, registry, "test", Tags.empty()); - RequiredSearch requiredSearch = registry.get("test.scheduler.tasks.pending"); - LongTaskTimer longTaskTimer = requiredSearch.longTaskTimer(); + testScheduler.init(); Scheduler.Worker worker = testScheduler.createWorker(); - Runnable supp = () -> { - try { - cdl.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }; - - assertThatNoException().isThrownBy(() -> worker.schedule(supp)); - assertThatNoException().isThrownBy(() -> worker.schedule(supp)); - assertThat(longTaskTimer.activeTasks()).as("longTaskTimer.activeTasks()").isOne(); - assertThatExceptionOfType(RejectedExecutionException.class).isThrownBy(() -> worker.schedule(supp)); - assertThatExceptionOfType(RejectedExecutionException.class) - .isThrownBy(() -> worker.schedule(supp, 0, TimeUnit.SECONDS)); - - cdl.countDown(); + RequiredSearch requiredSearch = registry.get("test.scheduler.tasks.pending"); + LongTaskTimer longTaskTimer = requiredSearch.longTaskTimer(); - assertThat(longTaskTimer.activeTasks()) - .as("longTaskTimer.activeTasks()") - .isZero(); + try { + Runnable activeTask = () -> { + try { + countPendingLatch.countDown(); + activeTaskLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + }; + + Runnable pendingTask = pendingTaskLatch::countDown; + Runnable rejectedTask = () -> { + }; + + // Schedule two tasks: one will execute, the other will wait in the queue + assertThatNoException().isThrownBy(() -> worker.schedule(activeTask)); + assertThatNoException().isThrownBy(() -> worker.schedule(pendingTask)); + + // Wait till first one is picked up -> exactly one is pending now + countPendingLatch.await(1, TimeUnit.SECONDS); + assertThat(longTaskTimer.activeTasks()).as("active pending") + .isOne(); + + assertThatExceptionOfType(RejectedExecutionException.class).isThrownBy( + () -> worker.schedule(rejectedTask)); + assertThatExceptionOfType(RejectedExecutionException.class).isThrownBy( + () -> worker.schedule(rejectedTask, 0, TimeUnit.SECONDS)); + + activeTaskLatch.countDown(); + pendingTaskLatch.await(1, TimeUnit.SECONDS); + + assertThat(longTaskTimer.activeTasks()).as("active pending") + .isZero(); + } + finally { + worker.dispose(); + testScheduler.disposeGracefully() + .block(Duration.ofSeconds(1)); + } } } \ No newline at end of file From 378543d338be75a13c1e2bbf5c4f1213598f1029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 2 Jan 2024 10:09:58 +0100 Subject: [PATCH 231/312] Restore where onLastOperatorHook is applied (#3673) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recent improvements in the automatic context propagation (#3549) resulted in a regression – some sites where the "last operator" hook was previously applied no longer saw that behaviour. This change restores it. Implementation wise, it's worth noting that the "last operator" functionality relies on executing the subscribe(Subscriber) method from the base reactive-streams Publisher instead of the overloads that come from CorePublisher. The implementations of the reactive-streams base method in reactor-core apply this hook and that is when something is considered a "last operator". The wrapping of the Publisher when a non-internal producer is encountered to restore ThreadLocal values has changed the compiler's inference of the signature to use the CoreSubscriber argument variant, breaking the behaviour. This commit does not bring any tests as the functionality was not extensively tested before. The issue was discovered in spring-security in https://github.com/spring-projects/spring-security/issues/14207 and the change has been validated against the actual use case. --- .../core/publisher/FluxBufferWhen.java | 3 ++- .../core/publisher/FluxConcatArray.java | 11 ++++++---- .../core/publisher/FluxConcatIterable.java | 3 ++- .../reactor/core/publisher/FluxConcatMap.java | 6 ++++-- .../publisher/FluxConcatMapNoPrefetch.java | 3 ++- .../core/publisher/FluxFilterWhen.java | 3 ++- .../core/publisher/FluxFirstWithSignal.java | 3 ++- .../core/publisher/FluxFirstWithValue.java | 4 +++- .../reactor/core/publisher/FluxGroupJoin.java | 6 ++++-- .../java/reactor/core/publisher/FluxJoin.java | 7 ++++--- .../core/publisher/FluxRepeatWhen.java | 3 ++- .../reactor/core/publisher/FluxRetryWhen.java | 3 ++- .../core/publisher/FluxSampleFirst.java | 3 ++- .../core/publisher/FluxSampleTimeout.java | 3 ++- .../reactor/core/publisher/FluxSwitchMap.java | 3 ++- .../publisher/FluxSwitchMapNoPrefetch.java | 3 ++- .../core/publisher/FluxSwitchOnFirst.java | 20 +++++++++++-------- .../reactor/core/publisher/FluxUsingWhen.java | 20 +++++++++++-------- .../core/publisher/MonoDelayUntil.java | 5 +++-- .../core/publisher/MonoFilterWhen.java | 3 ++- .../core/publisher/MonoFirstWithSignal.java | 3 ++- .../core/publisher/MonoFlatMapMany.java | 3 ++- .../core/publisher/MonoIgnoreThen.java | 9 +++++---- .../reactor/core/publisher/MonoUsingWhen.java | 7 +++---- 24 files changed, 85 insertions(+), 52 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java index b9379073a1..22186cad29 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferWhen.java @@ -360,7 +360,8 @@ void open(OPEN token) { BufferWhenCloseSubscriber bc = new BufferWhenCloseSubscriber<>(this, idx); subscribers.add(bc); - Operators.toFluxOrMono(p).subscribe(bc); + p = Operators.toFluxOrMono(p); + p.subscribe(bc); } void openComplete(BufferWhenOpenSubscriber os) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatArray.java b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatArray.java index 4a775aa3e0..399147b659 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatArray.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatArray.java @@ -61,7 +61,8 @@ public void subscribe(CoreSubscriber actual) { if (p == null) { Operators.error(actual, new NullPointerException("The single source Publisher is null")); } else { - Operators.toFluxOrMono(p).subscribe(actual); + p = Operators.toFluxOrMono(p); + p.subscribe(actual); } return; } @@ -255,7 +256,8 @@ public void onComplete() { if (this.cancelled) { return; } - Operators.toFluxOrMono(p).subscribe(this); + p = Operators.toFluxOrMono(p); + p.subscribe(this); final Object state = this.get(); if (state != DONE) { @@ -404,7 +406,7 @@ public void onComplete() { return; } - final Publisher p = a[i]; + Publisher p = a[i]; if (p == null) { this.remove(); @@ -440,7 +442,8 @@ public void onComplete() { return; } - Operators.toFluxOrMono(p).subscribe(this); + p = Operators.toFluxOrMono(p); + p.subscribe(this); final Object state = this.get(); if (state != DONE) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatIterable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatIterable.java index cd59babd64..cc5d16022b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatIterable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatIterable.java @@ -144,7 +144,8 @@ public void onComplete() { produced(c); } - Operators.toFluxOrMono(p).subscribe(this); + p = Operators.toFluxOrMono(p); + p.subscribe(this); if (isCancelled()) { return; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMap.java index b07d8e87e2..33b4b36f77 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMap.java @@ -448,7 +448,8 @@ void drain() { } else { active = true; - Operators.toFluxOrMono(p).subscribe(inner); + p = Operators.toFluxOrMono(p); + p.subscribe(inner); } } } @@ -805,7 +806,8 @@ void drain() { } else { active = true; - Operators.toFluxOrMono(p).subscribe(inner); + p = Operators.toFluxOrMono(p); + p.subscribe(inner); } } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMapNoPrefetch.java b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMapNoPrefetch.java index 59ba901353..cfe38a994b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMapNoPrefetch.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxConcatMapNoPrefetch.java @@ -203,7 +203,8 @@ public void onNext(T t) { return; } - Operators.toFluxOrMono(p).subscribe(inner); + p = Operators.toFluxOrMono(p); + p.subscribe(inner); } catch (Throwable e) { Context ctx = actual.currentContext(); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFilterWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFilterWhen.java index 3d2bec6dac..eff1d22aae 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFilterWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFilterWhen.java @@ -279,7 +279,8 @@ void drain() { FilterWhenInner inner = new FilterWhenInner(this, !(p instanceof Mono)); if (CURRENT.compareAndSet(this,null, inner)) { state = STATE_RUNNING; - Operators.toFluxOrMono(p).subscribe(inner); + p = Operators.toFluxOrMono(p); + p.subscribe(inner); break; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithSignal.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithSignal.java index 336a8b6182..e2e9270c87 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithSignal.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithSignal.java @@ -127,7 +127,8 @@ public void subscribe(CoreSubscriber actual) { new NullPointerException("The single source Publisher is null")); } else { - Operators.toFluxOrMono(p).subscribe(actual); + p = Operators.toFluxOrMono(p); + p.subscribe(actual); } return; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithValue.java b/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithValue.java index bd20447ef6..acfb4de636 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithValue.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxFirstWithValue.java @@ -227,6 +227,8 @@ void subscribe(Publisher[] sources, actual.onSubscribe(this); + Operators.toFluxOrMono(sources); + for (int i = 0; i < n; i++) { if (cancelled || winner != Integer.MIN_VALUE) { return; @@ -237,7 +239,7 @@ void subscribe(Publisher[] sources, return; } - Operators.toFluxOrMono(sources[i]).subscribe(subscribers[i]); + sources[i].subscribe(subscribers[i]); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxGroupJoin.java b/reactor-core/src/main/java/reactor/core/publisher/FluxGroupJoin.java index c88650b462..1737f85383 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxGroupJoin.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxGroupJoin.java @@ -336,7 +336,8 @@ void drain() { new LeftRightEndSubscriber(this, true, idx); cancellations.add(end); - Operators.toFluxOrMono(p).subscribe(end); + p = Operators.toFluxOrMono(p); + p.subscribe(end); ex = error; if (ex != null) { @@ -404,7 +405,8 @@ else if (mode == RIGHT_VALUE) { new LeftRightEndSubscriber(this, false, idx); cancellations.add(end); - Operators.toFluxOrMono(p).subscribe(end); + p = Operators.toFluxOrMono(p); + p.subscribe(end); ex = error; if (ex != null) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxJoin.java b/reactor-core/src/main/java/reactor/core/publisher/FluxJoin.java index b01678a6fc..ac680182be 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxJoin.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxJoin.java @@ -26,7 +26,6 @@ import java.util.function.BiFunction; import java.util.function.BiPredicate; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Stream; import org.reactivestreams.Publisher; @@ -289,7 +288,8 @@ void drain() { new LeftRightEndSubscriber(this, true, idx); cancellations.add(end); - Operators.toFluxOrMono(p).subscribe(end); + p = Operators.toFluxOrMono(p); + p.subscribe(end); ex = error; if (ex != null) { @@ -366,7 +366,8 @@ else if (mode == RIGHT_VALUE) { new LeftRightEndSubscriber(this, false, idx); cancellations.add(end); - Operators.toFluxOrMono(p).subscribe(end); + p = Operators.toFluxOrMono(p); + p.subscribe(end); ex = error; if (ex != null) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java index 2d240732b6..82e7e5943c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRepeatWhen.java @@ -76,7 +76,8 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act return null; } - Operators.toFluxOrMono(p).subscribe(other); + p = Operators.toFluxOrMono(p); + p.subscribe(other); if (!main.cancelled) { return main; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRetryWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRetryWhen.java index e953e8ad03..0a94a9e14b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRetryWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRetryWhen.java @@ -74,7 +74,8 @@ static void subscribe(CoreSubscriber s, return; } - Operators.toFluxOrMono(p).subscribe(other); + p = Operators.toFluxOrMono(p); + p.subscribe(other); if (!main.cancelled) { wrapped.subscribe(main); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSampleFirst.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSampleFirst.java index a3027c86d0..4b1f7f02f9 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSampleFirst.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSampleFirst.java @@ -191,7 +191,8 @@ public void onNext(T t) { SampleFirstOther other = new SampleFirstOther<>(this); if (Operators.replace(OTHER, this, other)) { - Operators.toFluxOrMono(p).subscribe(other); + p = Operators.toFluxOrMono(p); + p.subscribe(other); } } else { diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSampleTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSampleTimeout.java index ccd51f7200..98d294ff60 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSampleTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSampleTimeout.java @@ -207,7 +207,8 @@ public void onNext(T t) { SampleTimeoutOther os = new SampleTimeoutOther<>(this, t, idx); if (Operators.replace(OTHER, this, os)) { - Operators.toFluxOrMono(p).subscribe(os); + p = Operators.toFluxOrMono(p); + p.subscribe(os); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMap.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMap.java index 39c3f8a7cb..1b3ee17244 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMap.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMap.java @@ -233,7 +233,8 @@ public void onNext(T t) { if (INNER.compareAndSet(this, si, innerSubscriber)) { ACTIVE.getAndIncrement(this); - Operators.toFluxOrMono(p).subscribe(innerSubscriber); + p = Operators.toFluxOrMono(p); + p.subscribe(innerSubscriber); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMapNoPrefetch.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMapNoPrefetch.java index 06620047f8..6881f21719 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMapNoPrefetch.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchMapNoPrefetch.java @@ -215,7 +215,8 @@ void subscribeInner(T nextElement, SwitchMapInner nextInner, int nextIndex return; } - Operators.toFluxOrMono(p).subscribe(nextInner); + p = Operators.toFluxOrMono(p); + p.subscribe(nextInner); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchOnFirst.java b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchOnFirst.java index 1e3f20c294..45526259f0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchOnFirst.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxSwitchOnFirst.java @@ -519,7 +519,7 @@ public final void onNext(T t) { return; } - final Publisher outboundPublisher; + Publisher outboundPublisher; final SwitchOnFirstControlSubscriber o = this.outboundSubscriber; try { @@ -542,7 +542,8 @@ public final void onNext(T t) { return; } - Operators.toFluxOrMono(outboundPublisher).subscribe(o); + outboundPublisher = Operators.toFluxOrMono(outboundPublisher); + outboundPublisher.subscribe(o); return; } @@ -575,7 +576,7 @@ public final void onError(Throwable t) { } if (!hasFirstValueReceived(previousState)) { - final Publisher result; + Publisher result; final CoreSubscriber o = this.outboundSubscriber; try { final Signal signal = Signal.error(t, o.currentContext()); @@ -586,7 +587,8 @@ public final void onError(Throwable t) { return; } - Operators.toFluxOrMono(result).subscribe(o); + result = Operators.toFluxOrMono(result); + result.subscribe(o); } } @@ -611,7 +613,7 @@ public final void onComplete() { } if (!hasFirstValueReceived(previousState)) { - final Publisher result; + Publisher result; final CoreSubscriber o = this.outboundSubscriber; try { @@ -623,7 +625,8 @@ public final void onComplete() { return; } - Operators.toFluxOrMono(result).subscribe(o); + result = Operators.toFluxOrMono(result); + result.subscribe(o); } } @@ -844,7 +847,7 @@ public boolean tryOnNext(T t) { return true; } - final Publisher result; + Publisher result; final SwitchOnFirstControlSubscriber o = this.outboundSubscriber; try { @@ -868,7 +871,8 @@ public boolean tryOnNext(T t) { return true; } - Operators.toFluxOrMono(result).subscribe(o); + result = Operators.toFluxOrMono(result); + result.subscribe(o); return true; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxUsingWhen.java b/reactor-core/src/main/java/reactor/core/publisher/FluxUsingWhen.java index ac9906638b..d32d156501 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxUsingWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxUsingWhen.java @@ -19,7 +19,6 @@ import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.BiFunction; import java.util.function.Function; @@ -91,7 +90,8 @@ public void subscribe(CoreSubscriber actual) { asyncCancel, null); - Operators.toFluxOrMono(p).subscribe(subscriber); + p = Operators.toFluxOrMono(p); + p.subscribe(subscriber); } } catch (Throwable e) { @@ -101,8 +101,9 @@ public void subscribe(CoreSubscriber actual) { } //trigger the resource creation and delay the subscription to actual - Operators.toFluxOrMono(resourceSupplier).subscribe(new ResourceSubscriber(actual, - resourceClosure, asyncComplete, asyncError, asyncCancel, resourceSupplier instanceof Mono)); + // + ensure onLastOperatorHook is called by invoking Publisher::subscribe(Subscriber) + ((Publisher) Operators.toFluxOrMono(resourceSupplier)).subscribe( + new ResourceSubscriber(actual, resourceClosure, asyncComplete, asyncError, asyncCancel, resourceSupplier instanceof Mono)); } @Override @@ -191,9 +192,10 @@ public void onNext(S resource) { } resourceProvided = true; - final Publisher p = deriveFluxFromResource(resource, resourceClosure); + Publisher p = deriveFluxFromResource(resource, resourceClosure); - Operators.toFluxOrMono(p).subscribe(FluxUsingWhen.prepareSubscriberForResource(resource, + p = Operators.toFluxOrMono(p); + p.subscribe(FluxUsingWhen.prepareSubscriberForResource(resource, this.actual, this.asyncComplete, this.asyncError, @@ -362,7 +364,8 @@ public void onError(Throwable t) { return; } - Operators.toFluxOrMono(p).subscribe(new RollbackInner(this, t)); + p = Operators.toFluxOrMono(p); + p.subscribe(new RollbackInner(this, t)); } } @@ -382,7 +385,8 @@ public void onComplete() { return; } - Operators.toFluxOrMono(p).subscribe(new CommitInner(this)); + p = Operators.toFluxOrMono(p); + p.subscribe(new CommitInner(this)); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoDelayUntil.java b/reactor-core/src/main/java/reactor/core/publisher/MonoDelayUntil.java index 51eb2610e6..780c6c43a0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoDelayUntil.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoDelayUntil.java @@ -287,7 +287,7 @@ void subscribeNextTrigger() { final Function> generator = this.otherGenerators[this.index]; - final Publisher p; + Publisher p; try { p = generator.apply(this.value); @@ -304,7 +304,8 @@ void subscribeNextTrigger() { this.triggerSubscriber = triggerSubscriber; } - Operators.toFluxOrMono(p).subscribe(triggerSubscriber); + p = Operators.toFluxOrMono(p); + p.subscribe(triggerSubscriber); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java index 093c54c25a..d2ddabdc79 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFilterWhen.java @@ -143,7 +143,8 @@ public void onNext(T t) { } else { FilterWhenInner inner = new FilterWhenInner<>(this, !(p instanceof Mono), t); - Operators.toFluxOrMono(p).subscribe(inner); + p = Operators.toFluxOrMono(p); + p.subscribe(inner); } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithSignal.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithSignal.java index 29fa86f816..fd97fe6ef7 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithSignal.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFirstWithSignal.java @@ -136,7 +136,8 @@ public void subscribe(CoreSubscriber actual) { actual.currentContext())); } else { - Operators.toFluxOrMono(p).subscribe(actual); + p = Operators.toFluxOrMono(p); + p.subscribe(actual); } return; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMapMany.java b/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMapMany.java index 08d6d1dd9a..880a7e22d5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMapMany.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoFlatMapMany.java @@ -192,7 +192,8 @@ public void onNext(T t) { return; } - Operators.toFluxOrMono(p).subscribe(new FlatMapManyInner<>(this, actual)); + p = Operators.toFluxOrMono(p); + p.subscribe(new FlatMapManyInner<>(this, actual)); } @Override diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoIgnoreThen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoIgnoreThen.java index 4627a406c2..92681d08c8 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoIgnoreThen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoIgnoreThen.java @@ -242,15 +242,15 @@ void subscribeNext() { } return; } else { - final Publisher m = a[i]; + Publisher p = a[i]; - if (m instanceof Callable) { + if (p instanceof Callable) { if (isCancelled(this.state)) { //NB: in the non-callable case, this is handled by activeSubscription.cancel() return; } try { - Operators.onDiscard(((Callable) m).call(), currentContext()); + Operators.onDiscard(((Callable) p).call(), currentContext()); } catch (Throwable ex) { onError(Operators.onOperatorError(ex, currentContext())); @@ -261,7 +261,8 @@ void subscribeNext() { continue; } - Operators.toFluxOrMono(m).subscribe((CoreSubscriber) this); + p = Operators.toFluxOrMono(p); + p.subscribe((CoreSubscriber) this); return; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoUsingWhen.java b/reactor-core/src/main/java/reactor/core/publisher/MonoUsingWhen.java index 507c78be77..bb72ea3e95 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoUsingWhen.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoUsingWhen.java @@ -93,10 +93,9 @@ public void subscribe(CoreSubscriber actual) { return; } - Operators.toFluxOrMono(resourceSupplier).subscribe(new ResourceSubscriber(actual, - resourceClosure, - asyncComplete, asyncError, asyncCancel, - resourceSupplier instanceof Mono)); + // Ensure onLastOperatorHook is called by invoking Publisher::subscribe(Subscriber) + ((Publisher) Operators.toFluxOrMono(resourceSupplier)).subscribe( + new ResourceSubscriber(actual, resourceClosure, asyncComplete, asyncError, asyncCancel, resourceSupplier instanceof Mono)); } @Override From baaeee06f012d9c00d4056b3d2cfb768534f5aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 5 Jan 2024 15:18:52 +0100 Subject: [PATCH 232/312] Fix flaky test for BoundedElasticThreadPerTaskScheduler (#3679) One of the tests for `BoundedElasticThreadPerTaskScheduler` had a race in it and is now predictably exercised with one more latch. --- ...ndedElasticThreadPerTaskSchedulerTest.java | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java b/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java index a91b4b88b2..3f0f8c5ab8 100644 --- a/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java +++ b/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2023-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -323,25 +323,44 @@ public void ensuresConcurrentPeriodicTasksSchedulingSingleWorker() throws Interr @Test public void ensuresConcurrentWorkerTaskDisposure() throws InterruptedException { for (int i = 0; i < 100; i++) { - CountDownLatch latch = new CountDownLatch(1); - CountDownLatch latch2 = new CountDownLatch(1); + CountDownLatch latchNeverReleased = new CountDownLatch(1); + CountDownLatch firstTaskLatch = new CountDownLatch(1); + CountDownLatch secondTaskLatch = new CountDownLatch(1); Scheduler.Worker worker = scheduler.createWorker(); - worker.schedule(()-> { + Disposable firstTask = worker.schedule(() -> { try { - latch2.await(); + firstTaskLatch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); - Disposable disposable = worker.schedule(latch::countDown); - RaceTestUtils.race(() -> worker.dispose(), () -> disposable.dispose()); - latch2.countDown(); - Assertions.assertThat(latch.getCount()) - .isOne(); + Disposable secondTask = worker.schedule(() -> { + try { + secondTaskLatch.await(); + // The below release is never to be reached. + // However, we need the above latch to protect against a situation + // in which during the race: + // 1. worker is disposed + // 2. firstTask is cancelled, gets interrupted + // 3. secondTask is pulled by the worker and executed, so + // latchNeverReleased.countDown() is executed + // 4. secondTask.dispose() is called after the task has already run + latchNeverReleased.countDown(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + RaceTestUtils.race(worker::dispose, secondTask::dispose); + + firstTaskLatch.countDown(); + secondTaskLatch.countDown(); + + Assertions.assertThat(latchNeverReleased.getCount()).isOne(); Assertions.assertThat(worker.isDisposed()).isTrue(); - Assertions.assertThat(disposable.isDisposed()).isTrue(); + Assertions.assertThat(firstTask.isDisposed()).isTrue(); + Assertions.assertThat(secondTask.isDisposed()).isTrue(); } } From 292b21be0a25edee9b0686b3023e071c2306d26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 5 Jan 2024 15:20:56 +0100 Subject: [PATCH 233/312] Improve RaceTestUtils to yield when CPUs don't suffice (#3678) When `RaceTestUtils`-based concurrency tests involving more tasks than available CPUs were run, they'd time-out when run in the Java 21 setup with loom-based boundedElastic `Scheduler`. This change should eliminate such situations. Addresses related issues below and potentially more. Fixes #3629 Fixes #3628 --- .../ReactorTestBlockHoundIntegration.java | 29 +++++++++++++++++++ ...ockhound.integration.BlockHoundIntegration | 14 +++++++++ .../java/reactor/test/util/RaceTestUtils.java | 16 ++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 reactor-core/src/blockHoundTest/java/reactor/core/scheduler/ReactorTestBlockHoundIntegration.java create mode 100644 reactor-core/src/blockHoundTest/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration diff --git a/reactor-core/src/blockHoundTest/java/reactor/core/scheduler/ReactorTestBlockHoundIntegration.java b/reactor-core/src/blockHoundTest/java/reactor/core/scheduler/ReactorTestBlockHoundIntegration.java new file mode 100644 index 0000000000..f6ce1e87e1 --- /dev/null +++ b/reactor-core/src/blockHoundTest/java/reactor/core/scheduler/ReactorTestBlockHoundIntegration.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.scheduler; + +import reactor.blockhound.BlockHound; +import reactor.blockhound.integration.BlockHoundIntegration; +import reactor.test.util.RaceTestUtils; + +public final class ReactorTestBlockHoundIntegration implements BlockHoundIntegration { + + @Override + public void applyTo(BlockHound.Builder builder) { + builder.allowBlockingCallsInside(RaceTestUtils.class.getName(), "lambda$race$2"); + } +} diff --git a/reactor-core/src/blockHoundTest/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration b/reactor-core/src/blockHoundTest/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration new file mode 100644 index 0000000000..e205afa4d0 --- /dev/null +++ b/reactor-core/src/blockHoundTest/resources/META-INF/services/reactor.blockhound.integration.BlockHoundIntegration @@ -0,0 +1,14 @@ +# Copyright (c) 2024 VMware Inc. or its affiliates, All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +reactor.core.scheduler.ReactorTestBlockHoundIntegration \ No newline at end of file diff --git a/reactor-test/src/main/java/reactor/test/util/RaceTestUtils.java b/reactor-test/src/main/java/reactor/test/util/RaceTestUtils.java index bf327a374d..89f64f3bb2 100644 --- a/reactor-test/src/main/java/reactor/test/util/RaceTestUtils.java +++ b/reactor-test/src/main/java/reactor/test/util/RaceTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -160,7 +160,19 @@ public static void race(int timeoutSeconds, Scheduler s, final Runnable... rs) { final int index = i; s.schedule(() -> { if (count.decrementAndGet() != 0) { - while (count.get() != 0) { } + while (count.get() != 0) { + // If there are less CPUs than Runnable instances, this spins + // forever. In case of platform Threads, things should be fine + // as the OS can interrupt their work and make progress. With + // VirtualThreads, they are run on the common ForkJoinPool, + // which allocates as many Threads as there are CPUs available. + // In such a case, the Runnables pin the platform Threads + // disallowing a continuation to be replaced by a pending one. + // This call creates the opportunity to swap the Runnables and + // make progress. For platform Threads it is just a hint to the + // runtime and might be ignored. + Thread.yield(); + } } try { From bf3bf26885991d1796b5f75fff252dc0fbeb1bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 5 Jan 2024 16:43:55 +0100 Subject: [PATCH 234/312] =?UTF-8?q?Remove=20overridden=20methods=20from=20?= =?UTF-8?q?GenericBoundedElasticThreadPerTask=E2=80=A6=20(#3685)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overridden methods from `GenericBoundedElasticThreadPerTaskShedulerTest` were not previously executed but are now due to [a regression in junit](https://github.com/junit-team/junit5/issues/3600#issuecomment-1857783336). This change restores the original behaviour of assumptions and excludes such tests using the parent class' facility. --- ...BoundedElasticThreadPerTaskSchedulerTest.java | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/reactor-core/src/test/java/reactor/core/scheduler/GenericBoundedElasticThreadPerTaskSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/GenericBoundedElasticThreadPerTaskSchedulerTest.java index e2339e9d3d..db604b52f7 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/GenericBoundedElasticThreadPerTaskSchedulerTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/GenericBoundedElasticThreadPerTaskSchedulerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2023-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,17 +64,7 @@ protected boolean shouldCheckMultipleDisposeGracefully() { } @Override - public void acceptTaskAfterStartStopStart() { - Assertions.fail("no restart supported"); - } - - @Override - public void restartSupport() { - Assertions.fail("no restart supported"); - } - - @Override - void multipleRestarts() { - Assertions.fail("no restart supported"); + protected boolean shouldCheckSupportRestart() { + return false; } } \ No newline at end of file From c89828192840651a1584451416a236f35e715bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 5 Jan 2024 22:25:44 +0100 Subject: [PATCH 235/312] Exclude loom boundedElastic from restart validation (#3686) The new loom-based boundedElastic Scheduler does not implement the start method and should be treated specially in restart behaviour validation. Follow-up to #3685 --- .../core/scheduler/AbstractSchedulerTest.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/reactor-core/src/test/java/reactor/core/scheduler/AbstractSchedulerTest.java b/reactor-core/src/test/java/reactor/core/scheduler/AbstractSchedulerTest.java index de5a8f959b..822ab3e2d6 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/AbstractSchedulerTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/AbstractSchedulerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,12 +144,18 @@ public void restartSupport() { Scheduler s = scheduler(); s.dispose(); // TODO: in 3.6.x: remove restart capability and this validation - s.start(); if (supportsRestart) { + s.start(); assertThat(s.isDisposed()).as("restart supported").isFalse(); } else { + try { + s.start(); + } catch (Exception e) { + assertThat(e).isInstanceOf(UnsupportedOperationException.class).as( + "start not supported for " + s.getClass().getName()); + } assertThat(s.isDisposed()).as("restart not supported").isTrue(); } } @@ -348,9 +354,7 @@ void multipleDisposeGracefully() { @Test void multipleRestarts() { - if (!shouldCheckSupportRestart()) { - return; - } + Assumptions.assumeThat(shouldCheckSupportRestart()).as("scheduler supports restart").isTrue(); Scheduler s = scheduler(); From da92f2205c95b7938629fe49b751771f7e0238ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Jan 2024 13:05:19 +0100 Subject: [PATCH 236/312] [release] Prepare and release 3.5.14 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cc51b0f72b..0b22f91c4a 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.13" - testCompile "io.projectreactor:reactor-test:3.5.13" + compile "io.projectreactor:reactor-core:3.5.14" + testCompile "io.projectreactor:reactor-test:3.5.14" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.14-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.14-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.15-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.15-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.13" + // implementation "io.projectreactor:reactor-tools:3.5.14" } ``` diff --git a/gradle.properties b/gradle.properties index 8d75c7fbae..cf8003144a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.14-SNAPSHOT -bomVersion=2022.0.14 -metricsMicrometerVersion=1.0.14-SNAPSHOT +version=3.5.14 +bomVersion=2022.0.15 +metricsMicrometerVersion=1.0.14 From 71271eb83275f54d8982b478a870c1bc6aaf8acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Jan 2024 13:54:12 +0100 Subject: [PATCH 237/312] [release] Next development version 3.5.15-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- settings.gradle | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index cf8003144a..eae398fa25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.14 +version=3.5.15-SNAPSHOT bomVersion=2022.0.15 -metricsMicrometerVersion=1.0.14 +metricsMicrometerVersion=1.0.15-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7f035d4844..ac744a157a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.13" -baselinePerfCore = "3.5.13" +baseline-core-api = "3.5.14" +baselinePerfCore = "3.5.14" baselinePerfExtra = "3.5.1" # Other shared versions diff --git a/settings.gradle b/settings.gradle index 5c13988ddf..999c02d60a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,9 +25,9 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.10.13-SNAPSHOT') + version('micrometer', '1.10.14-SNAPSHOT') version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") - version('micrometerTracingTest', "1.0.12-SNAPSHOT") + version('micrometerTracingTest', "1.0.13-SNAPSHOT") version('contextPropagation', "1.0.7-SNAPSHOT") } } From 38c8501e8bc1802a84fdb34955c5af89ee94c46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Jan 2024 16:16:31 +0100 Subject: [PATCH 238/312] [release] Prepare and release 3.6.2 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0953bbb2ed..b221e6d654 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.1" - testCompile "io.projectreactor:reactor-test:3.6.1" + compile "io.projectreactor:reactor-core:3.6.2" + testCompile "io.projectreactor:reactor-test:3.6.2" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.2-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.2-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.3-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.3-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.1" + // implementation "io.projectreactor:reactor-tools:3.6.2" } ``` diff --git a/gradle.properties b/gradle.properties index 5abf0e8a74..e4dccb0422 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.2-SNAPSHOT -bomVersion=2023.0.1 -metricsMicrometerVersion=1.1.2-SNAPSHOT +version=3.6.2 +bomVersion=2023.0.2 +metricsMicrometerVersion=1.1.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74ef4138f9..bdf4dd0eb3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,9 +14,9 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.1" +micrometer = "1.12.2" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.1" +micrometerTracingTest="1.2.2" contextPropagation="1.1.0" kotlin = "1.5.32" reactiveStreams = "1.0.4" From 60c088c6f6ccb9a3547d7dc0b50b2e3aa99ba1b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Jan 2024 16:51:20 +0100 Subject: [PATCH 239/312] [release] Next development version 3.6.3-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- settings.gradle | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index e4dccb0422..f25a05a79d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.2 +version=3.6.3-SNAPSHOT bomVersion=2023.0.2 -metricsMicrometerVersion=1.1.2 +metricsMicrometerVersion=1.1.3-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bdf4dd0eb3..90a84b84d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.1" -baselinePerfCore = "3.6.1" +baseline-core-api = "3.6.2" +baselinePerfCore = "3.6.2" baselinePerfExtra = "3.5.1" # Other shared versions diff --git a/settings.gradle b/settings.gradle index cdf207ab68..e9da3ea41c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,9 +25,9 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.12.2-SNAPSHOT') + version('micrometer', '1.12.3-SNAPSHOT') version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") - version('micrometerTracingTest', "1.2.2-SNAPSHOT") + version('micrometerTracingTest', "1.2.3-SNAPSHOT") version('contextPropagation', "1.1.1-SNAPSHOT") } } From 5304ecff64739524437f8ae8aef7194146c99a78 Mon Sep 17 00:00:00 2001 From: Nicolas Rist <39074410+Nicolas125841@users.noreply.github.com> Date: Thu, 25 Jan 2024 04:24:50 -0800 Subject: [PATCH 240/312] Fix flaky `TimedScheduler` periodic scheduling tests (#3677) Tests `schedulePeriodicallyTimesOneRunInActiveAndAllRunsInCompleted` and `schedulePeriodicallyIsCorrectlyMetered` are flaky due to a race between the main thread testing the meter data and the scheduled runnable recording itself on the meter. A `Scheduler` hook is used to properly synchronise when a validation can happen. Fixes #3604 --- .../micrometer/TimedSchedulerTest.java | 64 ++++++++++++++----- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java index b0b16fe8fb..2144399a44 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -192,20 +192,25 @@ void timingOfActiveAndPendingTasks() throws InterruptedException { void schedulePeriodicallyTimesOneRunInActiveAndAllRunsInCompleted() throws InterruptedException { MockClock virtualClock = new MockClock(); SimpleMeterRegistry registryWithVirtualClock = new SimpleMeterRegistry(SimpleConfig.DEFAULT, virtualClock); - TimedScheduler test = new TimedScheduler(Schedulers.single(), registryWithVirtualClock, "test", Tags.empty()); + TimedScheduler test = new TimedScheduler(Schedulers.single(), registryWithVirtualClock, "test", + Tags.empty()); //schedule a periodic task for which one run takes 500ms. we cancel after 3 runs CountDownLatch latch = new CountDownLatch(3); - Disposable d = test.schedulePeriodically( - () -> { - try { - virtualClock.add(Duration.ofMillis(500)); - } - finally { - latch.countDown(); - } - }, - 100, 100, TimeUnit.MILLISECONDS); + + //decrement latch after all task & wrapper actions are performed + Schedulers.onScheduleHook("test", task -> () -> { + try { + task.run(); + } + finally { + latch.countDown(); + } + }); + + Disposable d = test.schedulePeriodically(() -> virtualClock.add(Duration.ofMillis(500)), + 100, 100, TimeUnit.MILLISECONDS); + latch.await(1, TimeUnit.SECONDS); d.dispose(); @@ -219,6 +224,9 @@ void schedulePeriodicallyTimesOneRunInActiveAndAllRunsInCompleted() throws Inter assertThat(test.completedTasks.totalTime(TimeUnit.MILLISECONDS)) .as("total duration of tasks") .isEqualTo(1500); + + Schedulers.resetOnScheduleHook("test"); + test.disposeGracefully().block(Duration.ofSeconds(1)); } @Test @@ -250,7 +258,16 @@ void schedulePeriodicallyIsCorrectlyMetered() throws InterruptedException { CountDownLatch latch = new CountDownLatch(5); TimedScheduler test = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); - Disposable d = test.schedulePeriodically(latch::countDown, 100, 100, TimeUnit.MILLISECONDS); + Schedulers.onScheduleHook("test", task -> () -> { + try { + task.run(); + } + finally { + latch.countDown(); + } + }); + + Disposable d = test.schedulePeriodically(() -> {}, 100, 100, TimeUnit.MILLISECONDS); latch.await(10, TimeUnit.SECONDS); d.dispose(); @@ -264,6 +281,9 @@ void schedulePeriodicallyIsCorrectlyMetered() throws InterruptedException { .isEqualTo(5) .matches(l -> l == test.submittedDirect.count() + test.submittedDelayed.count() + test.submittedPeriodicInitial.count() + test.submittedPeriodicIteration.count(), "completed tasks == sum of all timer counts"); + + Schedulers.resetOnScheduleHook("test"); + test.disposeGracefully().block(Duration.ofSeconds(1)); } @Test @@ -307,12 +327,20 @@ void workerScheduleDelayIncrementsDelayedCounter() throws InterruptedException { @Test void workerSchedulePeriodicallyIsCorrectlyMetered() throws InterruptedException { - Scheduler original = Schedulers.single(); CountDownLatch latch = new CountDownLatch(5); - TimedScheduler testScheduler = new TimedScheduler(original, registry, "test", Tags.empty()); + TimedScheduler testScheduler = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); Scheduler.Worker test = testScheduler.createWorker(); - Disposable d = test.schedulePeriodically(latch::countDown, 100, 100, TimeUnit.MILLISECONDS); + Schedulers.onScheduleHook("test", task -> () -> { + try { + task.run(); + } + finally { + latch.countDown(); + } + }); + + Disposable d = test.schedulePeriodically(() -> {}, 100, 100, TimeUnit.MILLISECONDS); latch.await(10, TimeUnit.SECONDS); d.dispose(); @@ -328,6 +356,10 @@ void workerSchedulePeriodicallyIsCorrectlyMetered() throws InterruptedException + testScheduler.submittedDelayed.count() + testScheduler.submittedPeriodicInitial.count() + testScheduler.submittedPeriodicIteration.count(), "completed tasks == sum of all timer counts"); + + test.dispose(); + Schedulers.resetOnScheduleHook("test"); + testScheduler.disposeGracefully().block(Duration.ofSeconds(1)); } @Test From 67b2f25a30a85c5c0fb005cbfecd92b10a737763 Mon Sep 17 00:00:00 2001 From: injae kim Date: Fri, 2 Feb 2024 21:14:59 +0900 Subject: [PATCH 241/312] Add AutoCloseable shourtcut on Flux#using, Mono#using (#3704) Make `Flux, Mono#using` to close `AutoCloseable` resource, so that users don't have to pass resourceCleanup consumer. Fixes #3333. --- reactor-core/build.gradle | 4 + .../main/java/reactor/core/Exceptions.java | 16 +- .../java/reactor/core/publisher/Flux.java | 61 ++- .../java/reactor/core/publisher/Mono.java | 53 ++- .../reactor/core/publisher/FluxUsingTest.java | 406 ++++++++++++++--- .../reactor/core/publisher/MonoUsingTest.java | 413 +++++++++++++++--- 6 files changed, 834 insertions(+), 119 deletions(-) diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 863ba333eb..0394410e4c 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -256,6 +256,10 @@ task japicmp(type: JapicmpTask) { classExcludes = [ ] methodExcludes = [ + "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function)", + "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function, boolean)", + "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function)", + "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function, boolean)" ] } diff --git a/reactor-core/src/main/java/reactor/core/Exceptions.java b/reactor-core/src/main/java/reactor/core/Exceptions.java index 8328ca05a6..79c18fd462 100644 --- a/reactor-core/src/main/java/reactor/core/Exceptions.java +++ b/reactor-core/src/main/java/reactor/core/Exceptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.util.Objects; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.Consumer; import reactor.core.publisher.Flux; import reactor.util.Logger; @@ -34,6 +35,7 @@ * Global Reactor Core Exception handling and utils to operate on. * * @author Stephane Maldini + * @author Injae Kim * @see Reactive-Streams-Commons */ public abstract class Exceptions { @@ -861,4 +863,16 @@ static final class StaticThrowable extends Error { } } + /** + * A general-purpose {@link Consumer} that closes {@link AutoCloseable} resource. + * If exception is thrown during closing the resource, it will be propagated by {@link Exceptions#propagate(Throwable)}. + */ + public static final Consumer AUTO_CLOSE = resource -> { + try { + resource.close(); + } catch (Throwable t) { + throw Exceptions.propagate(t); + } + }; + } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index b9a3213cb2..e4e9640e66 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,6 +119,7 @@ * @author Stephane Maldini * @author David Karnok * @author Simon Baslé + * @author Injae Kim * * @see Mono */ @@ -2132,6 +2133,64 @@ public static Flux using(Callable resourceSupplier, Funct eager)); } + /** + * Uses an {@link AutoCloseable} resource, generated by a supplier for each individual Subscriber, + * while streaming the values from a Publisher derived from the same resource and makes sure + * the resource is released if the sequence terminates or the Subscriber cancels. + *

    + * Eager {@link AutoCloseable} resource cleanup happens just before the source termination and exceptions raised + * by the cleanup Consumer may override the terminal event. + *

    + * + *

    + * For an asynchronous version of the cleanup, with distinct path for onComplete, onError + * and cancel terminations, see {@link #usingWhen(Publisher, Function, Function, BiFunction, Function)}. + * + * @param resourceSupplier a {@link Callable} that is called on subscribe to generate the resource + * @param sourceSupplier a factory to derive a {@link Publisher} from the supplied resource + * @param emitted type + * @param resource type + * + * @return a new {@link Flux} built around a disposable resource + * @see #usingWhen(Publisher, Function, Function, BiFunction, Function) + * @see #usingWhen(Publisher, Function, Function) + */ + public static Flux using(Callable resourceSupplier, + Function> sourceSupplier) { + return using(resourceSupplier, sourceSupplier, true); + } + + /** + * Uses an {@link AutoCloseable} resource, generated by a supplier for each individual Subscriber, + * while streaming the values from a Publisher derived from the same resource and makes sure + * the resource is released if the sequence terminates or the Subscriber cancels. + *

    + *

      + *
    • Eager {@link AutoCloseable} resource cleanup happens just before the source termination and exceptions raised + * by the cleanup Consumer may override the terminal event.
    • + *
    • Non-eager cleanup will drop any exception.
    • + *
    + *

    + * + *

    + * For an asynchronous version of the cleanup, with distinct path for onComplete, onError + * and cancel terminations, see {@link #usingWhen(Publisher, Function, Function, BiFunction, Function)}. + * + * @param resourceSupplier a {@link Callable} that is called on subscribe to generate the resource + * @param sourceSupplier a factory to derive a {@link Publisher} from the supplied resource + * @param eager true to clean before terminating downstream subscribers + * @param emitted type + * @param resource type + * + * @return a new {@link Flux} built around a disposable resource + * @see #usingWhen(Publisher, Function, Function, BiFunction, Function) + * @see #usingWhen(Publisher, Function, Function) + */ + public static Flux using(Callable resourceSupplier, + Function> sourceSupplier, boolean eager) { + return using(resourceSupplier, sourceSupplier, Exceptions.AUTO_CLOSE, eager); + } + /** * Uses a resource, generated by a {@link Publisher} for each individual {@link Subscriber}, * while streaming the values from a {@link Publisher} derived from the same resource. diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 083fe2e6dc..847cf3fcaa 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -115,6 +115,7 @@ * @author Stephane Maldini * @author David Karnok * @author Simon Baslé + * @author Injae Kim * @see Flux */ public abstract class Mono implements CorePublisher { @@ -912,6 +913,56 @@ public static Mono using(Callable resourceSupplier, return using(resourceSupplier, sourceSupplier, resourceCleanup, true); } + /** + * Uses an {@link AutoCloseable} resource, generated by a supplier for each individual Subscriber, + * while streaming the value from a Mono derived from the same resource and makes sure + * the resource is released if the sequence terminates or the Subscriber cancels. + *

    + * Unlike in {@link Flux#using(Callable, Function, Consumer) Flux}, in the case of a valued {@link Mono} the cleanup + * happens just before passing the value to downstream. In all cases, exceptions raised by the cleanup + * {@link Consumer} may override the terminal event, discarding the element if the derived {@link Mono} was valued. + *

    + * + * + * @param resourceSupplier a {@link Callable} that is called on subscribe to create the resource + * @param sourceSupplier a {@link Mono} factory to create the Mono depending on the created resource + * @param emitted type + * @param resource type + * + * @return new {@link Mono} + */ + public static Mono using(Callable resourceSupplier, + Function> sourceSupplier) { + return using(resourceSupplier, sourceSupplier, true); + } + + /** + * Uses an {@link AutoCloseable} resource, generated by a supplier for each individual Subscriber, + * while streaming the value from a Mono derived from the same resource and makes sure + * the resource is released if the sequence terminates or the Subscriber cancels. + *

    + *

      + *
    • For eager cleanup, Unlike in {@link Flux#using(Callable, Function, Consumer) Flux}, + * in the case of a valued {@link Mono} the cleanup happens just before passing the value to downstream. + * In all cases, exceptions raised by the cleanup {@link Consumer} may override the terminal event, + * discarding the element if the derived {@link Mono} was valued.
    • + *
    • Non-eager cleanup will drop any exception.
    • + *
    + *

    + * + * + * @param resourceSupplier a {@link Callable} that is called on subscribe to create the resource + * @param sourceSupplier a {@link Mono} factory to create the Mono depending on the created resource + * @param eager set to true to clean before any signal (including onNext) is passed downstream + * @param emitted type + * @param resource type + * + * @return new {@link Mono} + */ + public static Mono using(Callable resourceSupplier, + Function> sourceSupplier, boolean eager) { + return using(resourceSupplier, sourceSupplier, Exceptions.AUTO_CLOSE, eager); + } /** * Uses a resource, generated by a {@link Publisher} for each individual {@link Subscriber}, diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxUsingTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxUsingTest.java index 31e3e16b42..71ab689ff3 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxUsingTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxUsingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,12 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.assertj.core.api.Assertions; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.reactivestreams.Subscription; @@ -31,6 +33,7 @@ import reactor.core.Fuseable; import reactor.core.Scannable; import reactor.test.MockUtils; +import reactor.test.ParameterizedTestWithName; import reactor.test.StepVerifier; import reactor.test.publisher.FluxOperatorTest; import reactor.test.subscriber.AssertSubscriber; @@ -41,6 +44,246 @@ public class FluxUsingTest extends FluxOperatorTest { + public static List> sourcesNonEager() { + return Arrays.asList( + new CleanupCase("sourceNonEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set, false); + } + }, + new CleanupCase("autocloseableNonEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.range(1, 10), false); + } + } + ); + } + + public static List> sourcesEager() { + return Arrays.asList( + new CleanupCase("sourceEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set); + } + }, + new CleanupCase("sourceEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set, true); + } + }, + new CleanupCase("autocloseableEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.range(1, 10)); + } + }, + new CleanupCase("autocloseableEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.range(1, 10), true); + } + } + ); + } + + public static List> sourcesFailNonEager() { + return Arrays.asList( + new CleanupCase("sourceFailNonEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.error(new RuntimeException("forced failure")), cleanup::set, false); + } + }, + new CleanupCase("autocloseableFailNonEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.error(new RuntimeException("forced failure")), false); + } + } + ); + } + + public static List> sourcesFailEager() { + return Arrays.asList( + new CleanupCase("sourceFailEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.error(new RuntimeException("forced failure")), cleanup::set); + } + }, + new CleanupCase("sourceFailEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.error(new RuntimeException("forced failure")), cleanup::set, true); + } + }, + new CleanupCase("autocloseableFailEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.error(new RuntimeException("forced failure"))); + } + }, + new CleanupCase("autocloseableFailEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> Flux.error(new RuntimeException("forced failure")), true); + } + } + ); + } + + public static List> resourcesThrow() { + return Arrays.asList( + new CleanupCase("resourceThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(r, 10), cleanup::set, false); + } + }, + new CleanupCase("autocloseableResourceThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(1, 10), false); + } + }, + new CleanupCase("resourceThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(r, 10), cleanup::set); + } + }, + new CleanupCase("resourceThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(r, 10), cleanup::set, true); + } + }, + new CleanupCase("autocloseableResourceThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(1, 10)); + } + }, + new CleanupCase("autocloseableResourceThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> { throw new RuntimeException("forced failure"); }, r -> Flux.range(1, 10), true); + } + } + ); + } + + public static List> sourcesThrowNonEager() { + return Arrays.asList( + new CleanupCase("sourceThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set, false); + } + }, + new CleanupCase("autocloseableThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }, false); + } + } + ); + } + + public static List> sourcesThrowEager() { + return Arrays.asList( + new CleanupCase("sourceThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set); + } + }, + new CleanupCase("sourceThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set, true); + } + }, + new CleanupCase("autocloseableThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }); + } + }, + new CleanupCase("autocloseableThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }, true); + } + } + ); + } + + public static List> resourcesCleanupThrowNonEager() { + return Arrays.asList( + new CleanupCase("resourceCleanupThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), r -> { throw new IllegalStateException("resourceCleanup"); }, false); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowNonEager") { + @Override + public Flux get() { + return Flux.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Flux.range(1, 10), false); + } + } + ); + } + + public static List> resourcesCleanupThrowEager() { + return Arrays.asList( + new CleanupCase("resourceCleanupThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), r -> { throw new IllegalStateException("resourceCleanup"); }); + } + }, + new CleanupCase("resourceCleanupThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> 1, r -> Flux.range(r, 10), r -> { throw new IllegalStateException("resourceCleanup"); }, true); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowEager") { + @Override + public Flux get() { + return Flux.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Flux.range(1, 10)); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowEagerFlag") { + @Override + public Flux get() { + return Flux.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Flux.range(1, 10), true); + } + } + ); + } + @Override protected Scenario defaultScenarioOptions(Scenario defaultOptions) { return defaultOptions.fusionMode(Fuseable.ANY) @@ -114,63 +357,55 @@ public void resourceCleanupNull() { }); } - @Test - public void normal() { + @ParameterizedTestWithName + @MethodSource("sourcesNonEager") + public void normal(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set, false) - .subscribe(ts); + cleanupCase.get().doAfterTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(0)).subscribe(ts); ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .assertComplete() .assertNoError(); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); } - @Test - public void normalEager() { + @ParameterizedTestWithName + @MethodSource("sourcesEager") + public void normalEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Flux.using(() -> 1, r -> Flux.range(r, 10), cleanup::set) - .subscribe(ts); + cleanupCase.get() + .doFinally(event -> assertThat(cleanupCase.cleanup).hasValue(0)) + .doOnTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(1)) + .subscribe(ts); ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .assertComplete() .assertNoError(); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); } - void checkCleanupExecutionTime(boolean eager, boolean fail) { - AtomicInteger cleanup = new AtomicInteger(); + void checkCleanupExecutionTime(CleanupCase cleanupCase, boolean eager, boolean fail) { AtomicBoolean before = new AtomicBoolean(); AssertSubscriber ts = new AssertSubscriber() { @Override public void onError(Throwable t) { super.onError(t); - before.set(cleanup.get() != 0); + before.set(cleanupCase.cleanup.get() != 0); } @Override public void onComplete() { super.onComplete(); - before.set(cleanup.get() != 0); + before.set(cleanupCase.cleanup.get() != 0); } }; - Flux.using(() -> 1, r -> { - if (fail) { - return Flux.error(new RuntimeException("forced failure")); - } - return Flux.range(r, 10); - }, cleanup::set, eager) - .subscribe(ts); + cleanupCase.get().subscribe(ts); if (fail) { ts.assertNoValues() @@ -184,66 +419,110 @@ public void onComplete() { .assertNoError(); } - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); assertThat(before.get()).isEqualTo(eager); } - @Test - public void checkNonEager() { - checkCleanupExecutionTime(false, false); + @ParameterizedTestWithName + @MethodSource("sourcesNonEager") + public void checkNonEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, false, false); } - @Test - public void checkEager() { - checkCleanupExecutionTime(true, false); + @ParameterizedTestWithName + @MethodSource("sourcesEager") + public void checkEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, true, false); } - @Test - public void checkErrorNonEager() { - checkCleanupExecutionTime(false, true); + @ParameterizedTestWithName + @MethodSource("sourcesFailNonEager") + public void checkErrorNonEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, false, true); } - @Test - public void checkErrorEager() { - checkCleanupExecutionTime(true, true); + @ParameterizedTestWithName + @MethodSource("sourcesFailEager") + public void checkErrorEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, true, true); } - @Test - public void resourceThrowsEager() { + @ParameterizedTestWithName + @MethodSource("resourcesThrow") + public void resourceThrows(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Flux.using(() -> { - throw new RuntimeException("forced failure"); - }, r -> Flux.range(1, 10), cleanup::set, false) - .subscribe(ts); + cleanupCase.get().subscribe(ts); ts.assertNoValues() .assertNotComplete() .assertError(RuntimeException.class) .assertErrorMessage("forced failure"); - assertThat(cleanup).hasValue(0); + assertThat(cleanupCase.cleanup).hasValue(0); } - @Test - public void factoryThrowsEager() { + @ParameterizedTestWithName + @MethodSource("sourcesThrowNonEager") + public void factoryThrowsNonEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); + cleanupCase.get().doAfterTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(0)).subscribe(ts); - Flux.using(() -> 1, r -> { - throw new RuntimeException("forced failure"); - }, cleanup::set, false) - .subscribe(ts); + ts.assertNoValues() + .assertNotComplete() + .assertError(RuntimeException.class) + .assertErrorMessage("forced failure"); + + assertThat(cleanupCase.cleanup).hasValue(1); + } + + @ParameterizedTestWithName + @MethodSource("sourcesThrowEager") + public void factoryThrowsEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get() + .doFinally(event -> assertThat(cleanupCase.cleanup).hasValue(0)) + .doOnTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(1)) + .subscribe(ts); ts.assertNoValues() .assertNotComplete() .assertError(RuntimeException.class) .assertErrorMessage("forced failure"); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); + } + + @ParameterizedTestWithName + @MethodSource("resourcesCleanupThrowNonEager") + public void resourcesCleanupThrowNonEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get().subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertComplete() + .assertNoError(); + + assertThat(cleanupCase.cleanup).hasValue(0); + } + + @ParameterizedTestWithName + @MethodSource("resourcesCleanupThrowEager") + public void resourcesCleanupThrowEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get().subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertErrorWith(e -> { + assertThat(e).hasMessage("resourceCleanup"); + assertThat(e).isExactlyInstanceOf(IllegalStateException.class); + }); + + assertThat(cleanupCase.cleanup).hasValue(0); } @Test @@ -386,4 +665,19 @@ public void scanFuseableSubscriber() { Assertions.assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } + static abstract class CleanupCase implements Supplier> { + + final AtomicInteger cleanup = new AtomicInteger(); + final String name; + + CleanupCase(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + } diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java index 20ba9d5a40..620e85da69 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoUsingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,21 @@ package reactor.core.publisher; import java.time.Duration; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.assertj.core.api.Condition; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.MethodSource; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Scannable; +import reactor.test.ParameterizedTestWithName; import reactor.test.StepVerifier; import reactor.test.subscriber.AssertSubscriber; @@ -36,6 +41,246 @@ public class MonoUsingTest { + public static List> sourcesNonEager() { + return Arrays.asList( + new CleanupCase("sourceNonEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, cleanup::set, false); + } + }, + new CleanupCase("autocloseableNonEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.just(1), false); + } + } + ); + } + + public static List> sourcesEager() { + return Arrays.asList( + new CleanupCase("sourceEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, cleanup::set); + } + }, + new CleanupCase("sourceEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, cleanup::set, true); + } + }, + new CleanupCase("autocloseableEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.just(1)); + } + }, + new CleanupCase("autocloseableEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.just(1), true); + } + } + ); + } + + public static List> sourcesFailNonEager() { + return Arrays.asList( + new CleanupCase("sourceFailNonEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> Mono.error(new RuntimeException("forced failure")), cleanup::set, false); + } + }, + new CleanupCase("autocloseableFailNonEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.error(new RuntimeException("forced failure")), false); + } + } + ); + } + + public static List> sourcesFailEager() { + return Arrays.asList( + new CleanupCase("sourceFailEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> Mono.error(new RuntimeException("forced failure")), cleanup::set); + } + }, + new CleanupCase("sourceFailEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> Mono.error(new RuntimeException("forced failure")), cleanup::set, true); + } + }, + new CleanupCase("autocloseableFailEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.error(new RuntimeException("forced failure"))); + } + }, + new CleanupCase("autocloseableFailEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> Mono.error(new RuntimeException("forced failure")), true); + } + } + ); + } + + public static List> resourcesThrow() { + return Arrays.asList( + new CleanupCase("resourceThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, Mono::just, cleanup::set, false); + } + }, + new CleanupCase("autocloseableResourceThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, r -> Mono.just(1), false); + } + }, + new CleanupCase("resourceThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, Mono::just, cleanup::set); + } + }, + new CleanupCase("resourceThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, Mono::just, cleanup::set, true); + } + }, + new CleanupCase("autocloseableResourceThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, r -> Mono.just(1)); + } + }, + new CleanupCase("autocloseableResourceThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> { throw new RuntimeException("forced failure"); }, r -> Mono.just(1), true); + } + } + ); + } + + public static List> sourcesThrowNonEager() { + return Arrays.asList( + new CleanupCase("sourceThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set, false); + } + }, + new CleanupCase("autocloseableThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }, false); + } + } + ); + } + + public static List> sourcesThrowEager() { + return Arrays.asList( + new CleanupCase("sourceThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set); + } + }, + new CleanupCase("sourceThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> 1, r -> { throw new RuntimeException("forced failure"); }, cleanup::set, true); + } + }, + new CleanupCase("autocloseableThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }); + } + }, + new CleanupCase("autocloseableThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> cleanup::incrementAndGet, r -> { throw new RuntimeException("forced failure"); }, true); + } + } + ); + } + + public static List> resourcesCleanupThrowNonEager() { + return Arrays.asList( + new CleanupCase("resourceCleanupThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, r -> { throw new IllegalStateException("resourceCleanup"); }, false); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowNonEager") { + @Override + public Mono get() { + return Mono.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Mono.just(1), false); + } + } + ); + } + + public static List> resourcesCleanupThrowEager() { + return Arrays.asList( + new CleanupCase("resourceCleanupThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, r -> { throw new IllegalStateException("resourceCleanup"); }); + } + }, + new CleanupCase("resourceCleanupThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> 1, Mono::just, r -> { throw new IllegalStateException("resourceCleanup"); }, true); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowEager") { + @Override + public Mono get() { + return Mono.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Mono.just(1)); + } + }, + new CleanupCase("autocloseableResourceCleanupThrowEagerFlag") { + @Override + public Mono get() { + return Mono.using(() -> new AutoCloseable() { + @Override + public void close() { + throw new IllegalStateException("resourceCleanup"); + } + }, r -> Mono.just(1), true); + } + } + ); + } + @Test public void resourceSupplierNull() { assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> { @@ -59,67 +304,55 @@ public void resourceCleanupNull() { }); } - @Test - public void normal() { + @ParameterizedTestWithName + @MethodSource("sourcesNonEager") + public void normal(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Mono.using(() -> 1, r -> Mono.just(1), cleanup::set, false) - .doAfterTerminate(() -> assertThat(cleanup).hasValue(0)) - .subscribe(ts); + cleanupCase.get().doAfterTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(0)).subscribe(ts); ts.assertValues(1) .assertComplete() .assertNoError(); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); } - @Test - public void normalEager() { + @ParameterizedTestWithName + @MethodSource("sourcesEager") + public void normalEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Mono.using(() -> 1, r -> Mono.just(1) - .doOnTerminate(() -> assertThat(cleanup).hasValue(0)), - cleanup::set, - true) - .subscribe(ts); + cleanupCase.get() + .doFinally(event -> assertThat(cleanupCase.cleanup).hasValue(0)) + .doOnTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(1)) + .subscribe(ts); ts.assertValues(1) .assertComplete() .assertNoError(); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); } - void checkCleanupExecutionTime(boolean eager, boolean fail) { - AtomicInteger cleanup = new AtomicInteger(); + void checkCleanupExecutionTime(CleanupCase cleanupCase, boolean eager, boolean fail) { AtomicBoolean before = new AtomicBoolean(); AssertSubscriber ts = new AssertSubscriber() { @Override public void onError(Throwable t) { super.onError(t); - before.set(cleanup.get() != 0); + before.set(cleanupCase.cleanup.get() != 0); } @Override public void onComplete() { super.onComplete(); - before.set(cleanup.get() != 0); + before.set(cleanupCase.cleanup.get() != 0); } }; - Mono.using(() -> 1, r -> { - if (fail) { - return Mono.error(new RuntimeException("forced failure")); - } - return Mono.just(1); - }, cleanup::set, eager) - .subscribe(ts); + cleanupCase.get().subscribe(ts); if (fail) { ts.assertNoValues() @@ -133,66 +366,110 @@ public void onComplete() { .assertNoError(); } - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); assertThat(before.get()).isEqualTo(eager); } - @Test - public void checkNonEager() { - checkCleanupExecutionTime(false, false); + @ParameterizedTestWithName + @MethodSource("sourcesNonEager") + public void checkNonEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, false, false); } - @Test - public void checkEager() { - checkCleanupExecutionTime(true, false); + @ParameterizedTestWithName + @MethodSource("sourcesEager") + public void checkEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, true, false); } - @Test - public void checkErrorNonEager() { - checkCleanupExecutionTime(false, true); + @ParameterizedTestWithName + @MethodSource("sourcesFailNonEager") + public void checkErrorNonEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, false, true); } - @Test - public void checkErrorEager() { - checkCleanupExecutionTime(true, true); + @ParameterizedTestWithName + @MethodSource("sourcesFailEager") + public void checkErrorEager(CleanupCase cleanupCase) { + checkCleanupExecutionTime(cleanupCase, true, true); } - @Test - public void resourceThrowsEager() { + @ParameterizedTestWithName + @MethodSource("resourcesThrow") + public void resourceThrowsEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); - - Mono.using(() -> { - throw new RuntimeException("forced failure"); - }, r -> Mono.just(1), cleanup::set, false) - .subscribe(ts); + cleanupCase.get().subscribe(ts); ts.assertNoValues() .assertNotComplete() .assertError(RuntimeException.class) .assertErrorMessage("forced failure"); - assertThat(cleanup).hasValue(0); + assertThat(cleanupCase.cleanup).hasValue(0); } - @Test - public void factoryThrowsEager() { + @ParameterizedTestWithName + @MethodSource("sourcesThrowNonEager") + public void factoryThrowsNonEager(CleanupCase cleanupCase) { AssertSubscriber ts = AssertSubscriber.create(); - AtomicInteger cleanup = new AtomicInteger(); + cleanupCase.get().doAfterTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(0)).subscribe(ts); - Mono.using(() -> 1, r -> { - throw new RuntimeException("forced failure"); - }, cleanup::set, false) - .subscribe(ts); + ts.assertNoValues() + .assertNotComplete() + .assertError(RuntimeException.class) + .assertErrorMessage("forced failure"); + + assertThat(cleanupCase.cleanup).hasValue(1); + } + + @ParameterizedTestWithName + @MethodSource("sourcesThrowEager") + public void factoryThrowsEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get() + .doFinally(event -> assertThat(cleanupCase.cleanup).hasValue(0)) + .doOnTerminate(() -> assertThat(cleanupCase.cleanup).hasValue(1)) + .subscribe(ts); ts.assertNoValues() .assertNotComplete() .assertError(RuntimeException.class) .assertErrorMessage("forced failure"); - assertThat(cleanup).hasValue(1); + assertThat(cleanupCase.cleanup).hasValue(1); + } + + @ParameterizedTestWithName + @MethodSource("resourcesCleanupThrowNonEager") + public void resourcesCleanupThrowNonEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get().subscribe(ts); + + ts.assertValues(1) + .assertComplete() + .assertNoError(); + + assertThat(cleanupCase.cleanup).hasValue(0); + } + + @ParameterizedTestWithName + @MethodSource("resourcesCleanupThrowEager") + public void resourcesCleanupThrowEager(CleanupCase cleanupCase) { + AssertSubscriber ts = AssertSubscriber.create(); + + cleanupCase.get().subscribe(ts); + + ts.assertNoValues() + .assertErrorWith(e -> { + assertThat(e).hasMessage("resourceCleanup"); + assertThat(e).isExactlyInstanceOf(IllegalStateException.class); + }); + + assertThat(cleanupCase.cleanup).hasValue(0); } @Test @@ -383,4 +660,20 @@ public void scanSubscriber() { test.cancel(); assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); } + + static abstract class CleanupCase implements Supplier> { + + final AtomicInteger cleanup = new AtomicInteger(); + final String name; + + CleanupCase(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + } From 28231e975fb0dd2375aab0578a4ac03eaecf40e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 7 Feb 2024 11:40:02 +0100 Subject: [PATCH 242/312] Bump github actions in nightly workflow --- .github/workflows/nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 66a15dbfdd..31010c3fc0 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -20,7 +20,7 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 + - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 name: gradle with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=!slow -DuseSnapshotMicrometerVersion=true @@ -39,7 +39,7 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 + - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 name: gradle with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=slow -DuseSnapshotMicrometerVersion=true @@ -58,7 +58,7 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 + - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 name: other tests with: arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file From 025033f288b6aa6277c0139c4e30bdf6e08501c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 8 Feb 2024 12:36:39 +0100 Subject: [PATCH 243/312] Nightly build to run jcstress weekly, snapshots daily (#3716) --- .github/workflows/nightly.yml | 2 +- .github/workflows/snapshots.yml | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/snapshots.yml diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 31010c3fc0..1036fb9864 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -2,7 +2,7 @@ name: Nightly Check on: schedule: - - cron: "0 14 * * *" + - cron: "0 14 * * 0" permissions: read-all jobs: core-fast: diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml new file mode 100644 index 0000000000..8816b7c956 --- /dev/null +++ b/.github/workflows/snapshots.yml @@ -0,0 +1,35 @@ +name: Snapshots Check + +on: + schedule: + - cron: "0 20 * * *" +permissions: read-all +jobs: + run-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: [ 3.5.x, main ] + test-type: + - type: core-fast + arguments: ":reactor-core:test --no-daemon -Pjunit-tags=!slow -DuseSnapshotMicrometerVersion=true" + - type: core-slow + arguments: ":reactor-core:test --no-daemon -Pjunit-tags=slow -DuseSnapshotMicrometerVersion=true" + - type: other + arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default" + name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests + steps: + - name: Checkout Repository + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 + with: + ref: ${{ matrix.branch }} + - name: Setup Java8 + uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 + with: + distribution: 'temurin' + java-version: 8 + - name: Run Gradle Tests + uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 + with: + arguments: ${{ matrix.test-type.arguments }} \ No newline at end of file From 6250fd693984388f14302c5df8ac41e2be5e73ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 8 Feb 2024 13:01:39 +0100 Subject: [PATCH 244/312] Adds other JDKs to snapshots workflow (#3720) --- .github/workflows/snapshots.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 8816b7c956..08f5bbd1b6 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -14,6 +14,8 @@ jobs: test-type: - type: core-fast arguments: ":reactor-core:test --no-daemon -Pjunit-tags=!slow -DuseSnapshotMicrometerVersion=true" + - type: core-fast-java21 + arguments: ":reactor-core:java21Test --no-daemon -Pjunit-tags=!slow -DuseSnapshotMicrometerVersion=true" - type: core-slow arguments: ":reactor-core:test --no-daemon -Pjunit-tags=slow -DuseSnapshotMicrometerVersion=true" - type: other @@ -24,6 +26,23 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 with: ref: ${{ matrix.branch }} + - name: Download Java9 + if: ${{ matrix.branch == 'main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup Java9 + if: ${{ matrix.branch == 'main' }} + uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup Java21 + if: ${{ matrix.branch == 'main' }} + uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 + with: + distribution: 'temurin' + java-version: 21 - name: Setup Java8 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 with: From 845a824dc551575e3fb024bf0e2d9095d527a97f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 8 Feb 2024 13:47:24 +0100 Subject: [PATCH 245/312] Restore java21Test back into workflows (#3721) As a follow-up after temporarily disabling java21 reactor-core tests and then fixing the `RaceTestUtils` to accommodate virtual threads limitations with regards to number of CPUs and busy spinning, the java21 tests can be restored. Related #3590 Follow-up to #3678 --- .github/workflows/ci.yml | 66 +++++++++++++++++------------------ .github/workflows/publish.yml | 2 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e5acda9c5..4d37e5ed35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,39 +89,39 @@ jobs: name: gradle with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=!slow -# java-21-core-fast: -# if: ${{ github.base_ref == 'main' }} -# name: Java 21 core fast tests -# runs-on: ubuntu-latest -# needs: preliminary -# steps: -# - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 -# - name: Download JDK 9 -# if: ${{ github.base_ref == 'main' }} -# run: ${GITHUB_WORKSPACE}/.github/setup.sh -# shell: bash -# - name: Setup JDK 9 -# if: ${{ github.base_ref == 'main' }} -# uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 -# with: -# distribution: 'jdkfile' -# java-version: 9.0.4 -# jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz -# - name: Setup JDK 21 -# if: ${{ github.base_ref == 'main' }} -# uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 -# with: -# distribution: 'temurin' -# java-version: 21 -# - name: Setup JDK 8 -# uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 -# with: -# distribution: 'temurin' -# java-version: 8 -# - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 -# name: gradle -# with: -# arguments: :reactor-core:java21Test --no-daemon -Pjunit-tags=!slow + java-21-core-fast: + if: ${{ github.base_ref == 'main' }} + name: Java 21 core fast tests + runs-on: ubuntu-latest + needs: preliminary + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 + - name: Download JDK 9 + if: ${{ github.base_ref == 'main' }} + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup JDK 9 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup JDK 21 + if: ${{ github.base_ref == 'main' }} + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # tag=v3 + with: + distribution: 'temurin' + java-version: 21 + - name: Setup JDK 8 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # tag=v3 + with: + distribution: 'temurin' + java-version: 8 + - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 + name: gradle + with: + arguments: :reactor-core:java21Test --no-daemon -Pjunit-tags=!slow core-slow: name: core slower tests runs-on: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 55ee15ba70..26cfcfaf04 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,7 +51,7 @@ jobs: id: checks uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 with: - arguments: check -x :reactor-core:java21Test -Pjunit-tags=!slow -x jcstress + arguments: check -Pjunit-tags=!slow -x jcstress slowerChecks: # similar limitations as in prepare, but we parallelize slower tests here From 307a85f70398f8c70be7575613e3e726e2cd28b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 8 Feb 2024 14:10:24 +0100 Subject: [PATCH 246/312] Exclude java21 tests from 3.5.x in snapshots workflow (#3722) --- .github/workflows/snapshots.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 08f5bbd1b6..7bed64b61e 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -20,6 +20,9 @@ jobs: arguments: ":reactor-core:test --no-daemon -Pjunit-tags=slow -DuseSnapshotMicrometerVersion=true" - type: other arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default" + exclude: + - branch: 3.5.x + test-type: core-fast-java21 name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests steps: - name: Checkout Repository From 2f84579ffee7694f10d9d3a488de20291b1fac42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 8 Feb 2024 14:30:38 +0100 Subject: [PATCH 247/312] Switch jcstress mode to quick in nightly workflow (#3723) Due to the fact that with the current configuration the JCStress tests in default mode are estimated to run for more than 6 hours, the "nightly" job kept failing. Github Actions runner has a cap at 6 hours. The "quick" JCStress mode should finish in less than that time. --- .github/workflows/nightly.yml | 4 ++-- .github/workflows/snapshots.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f825c666fb..ef5f40d8d7 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -146,9 +146,9 @@ jobs: if: ${{ matrix.branch == 'main' }} name: other tests with: - arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default + arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=quick - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 if: ${{ matrix.branch == '3.5.x' }} name: other tests with: - arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file + arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=quick \ No newline at end of file diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 7bed64b61e..908ca89411 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -19,7 +19,7 @@ jobs: - type: core-slow arguments: ":reactor-core:test --no-daemon -Pjunit-tags=slow -DuseSnapshotMicrometerVersion=true" - type: other - arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default" + arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon -DuseSnapshotMicrometerVersion=true" exclude: - branch: 3.5.x test-type: core-fast-java21 From e2f7c05f64e9b7f4e770613eecf855e616182de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 8 Feb 2024 14:48:06 +0100 Subject: [PATCH 248/312] Polish snapshots workflow to exclude struct type --- .github/workflows/snapshots.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 908ca89411..6fb31c238b 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -22,7 +22,8 @@ jobs: arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon -DuseSnapshotMicrometerVersion=true" exclude: - branch: 3.5.x - test-type: core-fast-java21 + test-type: + type: core-fast-java21 name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests steps: - name: Checkout Repository From 96ab48c74e51129c9ed12302a7ed914fe1eb7398 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 13 Feb 2024 10:58:58 +0100 Subject: [PATCH 249/312] [release] Prepare and release 3.6.3 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b221e6d654..641e22a1f7 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.2" - testCompile "io.projectreactor:reactor-test:3.6.2" + compile "io.projectreactor:reactor-core:3.6.3" + testCompile "io.projectreactor:reactor-test:3.6.3" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.3-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.3-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.4-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.4-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.2" + // implementation "io.projectreactor:reactor-tools:3.6.3" } ``` diff --git a/gradle.properties b/gradle.properties index f25a05a79d..94c21f2f27 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.3-SNAPSHOT -bomVersion=2023.0.2 -metricsMicrometerVersion=1.1.3-SNAPSHOT +version=3.6.3 +bomVersion=2023.0.3 +metricsMicrometerVersion=1.1.3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 90a84b84d7..f5520f7af0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,10 +14,10 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "3.3.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.2" +micrometer = "1.12.3" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.2" -contextPropagation="1.1.0" +micrometerTracingTest="1.2.3" +contextPropagation="1.1.1" kotlin = "1.5.32" reactiveStreams = "1.0.4" From c316327ddc91f6b742eac8896c91da0635ca55cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 13 Feb 2024 14:07:55 +0100 Subject: [PATCH 250/312] Revert "Merge #3700 into 3.6.4" This reverts commit 659b45cb39444b7ce802a60cb0f2645e37ae5ee3, reversing changes made to 96ab48c74e51129c9ed12302a7ed914fe1eb7398. --- .../core/publisher/FluxSwitchMapStressTest.java | 17 ++++------------- .../test/publisher/ColdTestPublisher.java | 5 ++--- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxSwitchMapStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxSwitchMapStressTest.java index b13a2a9a0e..a6e275870a 100644 --- a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxSwitchMapStressTest.java +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxSwitchMapStressTest.java @@ -272,8 +272,7 @@ public void arbiter(II_Result r) { // Ignore, flaky test (https://github.com/reactor/reactor-core/issues/3633) //@JCStressTest - @Outcome(id = {"200, 0, 0", "200, 0, 1"}, expect = ACCEPTABLE, desc = "Should " + - "produced exactly what was requested") + @Outcome(id = {"200, 0", "200, 1"}, expect = ACCEPTABLE, desc = "Should produced exactly what was requested") @State public static class RequestAndProduceStressTest2 extends FluxSwitchMapStressTest { @@ -313,17 +312,9 @@ public void outerRequest() { } @Arbiter - public void arbiter(III_Result r) { - r.r1 = stressSubscriber.onNextCalls.get(); - r.r2 = (int) switchMapMain.requested; - r.r3 = stressSubscriber.onCompleteCalls.get(); - - switch (r.toString()) { - case "200, 0, 0": - case "200, 0, 1": - break; - default: throw new IllegalStateException(r + " " + fastLogger); - } + public void arbiter(II_Result r) { + r.r1 = (int) (stressSubscriber.onNextCalls.get() + switchMapMain.requested); + r.r2 = stressSubscriber.onCompleteCalls.get(); if (stressSubscriber.onNextCalls.get() < 200 && stressSubscriber.onNextDiscarded.get() < switchMapMain.requested) { throw new IllegalStateException(r + " " + fastLogger); diff --git a/reactor-test/src/main/java/reactor/test/publisher/ColdTestPublisher.java b/reactor-test/src/main/java/reactor/test/publisher/ColdTestPublisher.java index bf76c84830..9385149dae 100644 --- a/reactor-test/src/main/java/reactor/test/publisher/ColdTestPublisher.java +++ b/reactor-test/src/main/java/reactor/test/publisher/ColdTestPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2024 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,7 +103,6 @@ public void subscribe(Subscriber s) { } s.onSubscribe(p); // will trigger drain() via request() - p.drain(); // ensures that empty source terminal signal is propagated without waiting for a request from the subscriber } boolean add(ColdTestPublisherSubscription s) { @@ -316,7 +315,7 @@ private void drain() { * @return true if the TestPublisher was terminated, false otherwise */ private boolean emitTerminalSignalIfAny() { - if (parent.done && this.parent.values.size() == index) { + if (parent.done) { parent.remove(this); final Throwable t = parent.error; From bff5cfecfeb709d196f9f45b3b1d5bfee49e7b13 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Wed, 14 Feb 2024 12:23:07 +0100 Subject: [PATCH 251/312] [release] Next development version 3.6.4-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 5 +++-- reactor-core/build.gradle | 4 ---- settings.gradle | 6 +++--- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/gradle.properties b/gradle.properties index 94c21f2f27..55691dc586 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.3 +version=3.6.4-SNAPSHOT bomVersion=2023.0.3 -metricsMicrometerVersion=1.1.3 +metricsMicrometerVersion=1.1.4-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5520f7af0..0162370498 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,9 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.2" -baselinePerfCore = "3.6.2" +#baseline-core-api = "3.6.3" +baseline-core-api = "SKIP" +baselinePerfCore = "3.6.3" baselinePerfExtra = "3.5.1" # Other shared versions diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 0394410e4c..863ba333eb 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -256,10 +256,6 @@ task japicmp(type: JapicmpTask) { classExcludes = [ ] methodExcludes = [ - "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function)", - "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function, boolean)", - "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function)", - "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function, boolean)" ] } diff --git a/settings.gradle b/settings.gradle index e9da3ea41c..0e3ded581e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,10 +25,10 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.12.3-SNAPSHOT') + version('micrometer', '1.12.4-SNAPSHOT') version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") - version('micrometerTracingTest', "1.2.3-SNAPSHOT") - version('contextPropagation', "1.1.1-SNAPSHOT") + version('micrometerTracingTest', "1.2.4-SNAPSHOT") + version('contextPropagation', "1.1.2-SNAPSHOT") } } } From 46e41d4d973379162fedb64436e6b30b450c1938 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Wed, 14 Feb 2024 12:53:25 +0100 Subject: [PATCH 252/312] Re-enable core api baseline. Revert methodExcludes. --- gradle/libs.versions.toml | 3 +-- reactor-core/build.gradle | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0162370498..a96c7bf5df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,7 @@ [versions] # Baselines, should be updated on every release -#baseline-core-api = "3.6.3" -baseline-core-api = "SKIP" +baseline-core-api = "3.6.3" baselinePerfCore = "3.6.3" baselinePerfExtra = "3.5.1" diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 863ba333eb..0394410e4c 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -256,6 +256,10 @@ task japicmp(type: JapicmpTask) { classExcludes = [ ] methodExcludes = [ + "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function)", + "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function, boolean)", + "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function)", + "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function, boolean)" ] } From 7296ed560f4f4b0dfba36c3138768e4078db63c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 19 Feb 2024 13:33:57 +0100 Subject: [PATCH 253/312] Merge #3700 into 3.6.4 This reverts commit c316327ddc91f6b742eac8896c91da0635ca55cf. --- .../core/publisher/FluxSwitchMapStressTest.java | 17 +++++++++++++---- .../test/publisher/ColdTestPublisher.java | 5 +++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxSwitchMapStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxSwitchMapStressTest.java index a6e275870a..b13a2a9a0e 100644 --- a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxSwitchMapStressTest.java +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxSwitchMapStressTest.java @@ -272,7 +272,8 @@ public void arbiter(II_Result r) { // Ignore, flaky test (https://github.com/reactor/reactor-core/issues/3633) //@JCStressTest - @Outcome(id = {"200, 0", "200, 1"}, expect = ACCEPTABLE, desc = "Should produced exactly what was requested") + @Outcome(id = {"200, 0, 0", "200, 0, 1"}, expect = ACCEPTABLE, desc = "Should " + + "produced exactly what was requested") @State public static class RequestAndProduceStressTest2 extends FluxSwitchMapStressTest { @@ -312,9 +313,17 @@ public void outerRequest() { } @Arbiter - public void arbiter(II_Result r) { - r.r1 = (int) (stressSubscriber.onNextCalls.get() + switchMapMain.requested); - r.r2 = stressSubscriber.onCompleteCalls.get(); + public void arbiter(III_Result r) { + r.r1 = stressSubscriber.onNextCalls.get(); + r.r2 = (int) switchMapMain.requested; + r.r3 = stressSubscriber.onCompleteCalls.get(); + + switch (r.toString()) { + case "200, 0, 0": + case "200, 0, 1": + break; + default: throw new IllegalStateException(r + " " + fastLogger); + } if (stressSubscriber.onNextCalls.get() < 200 && stressSubscriber.onNextDiscarded.get() < switchMapMain.requested) { throw new IllegalStateException(r + " " + fastLogger); diff --git a/reactor-test/src/main/java/reactor/test/publisher/ColdTestPublisher.java b/reactor-test/src/main/java/reactor/test/publisher/ColdTestPublisher.java index 9385149dae..bf76c84830 100644 --- a/reactor-test/src/main/java/reactor/test/publisher/ColdTestPublisher.java +++ b/reactor-test/src/main/java/reactor/test/publisher/ColdTestPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,6 +103,7 @@ public void subscribe(Subscriber s) { } s.onSubscribe(p); // will trigger drain() via request() + p.drain(); // ensures that empty source terminal signal is propagated without waiting for a request from the subscriber } boolean add(ColdTestPublisherSubscription s) { @@ -315,7 +316,7 @@ private void drain() { * @return true if the TestPublisher was terminated, false otherwise */ private boolean emitTerminalSignalIfAny() { - if (parent.done) { + if (parent.done && this.parent.values.size() == index) { parent.remove(this); final Throwable t = parent.error; From 8744fb21918ac0b7b4097c723ad84a65015ff216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 19 Feb 2024 15:27:22 +0100 Subject: [PATCH 254/312] Revert "Merge #3727 into 3.5.15" This reverts commit fd19ccd5d49b2264efe40d1f625909d9eb585f63, reversing changes made to cd254fb4b498184a319e9f2f1ada9aa23aa9020f. --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 770117d976..8bbf69bdc6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,7 +41,7 @@ reactor-perfBaseline-core = { module = "io.projectreactor:reactor-core", version reactor-perfBaseline-extra = { module = "io.projectreactor.addons:reactor-extra", version.ref = "baselinePerfExtra" } [plugins] -artifactory = { id = "com.jfrog.artifactory", version = "5.2.0" } +artifactory = { id = "com.jfrog.artifactory", version = "4.31.0" } asciidoctor-convert = { id = "org.asciidoctor.jvm.convert", version.ref = "asciidoctor" } asciidoctor-pdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor" } bnd = { id = "biz.aQute.bnd.builder", version = "6.3.1" } From af8bc3fe473bef385701cf9e4f8d45043a0b0f97 Mon Sep 17 00:00:00 2001 From: Kai Zander <61500114+kzander91@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:47:38 +0100 Subject: [PATCH 255/312] [doc] Improve threading section in coreFeatures.adoc (#3676) --- docs/asciidoc/coreFeatures.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/asciidoc/coreFeatures.adoc b/docs/asciidoc/coreFeatures.adoc index 9225536669..295897ea87 100644 --- a/docs/asciidoc/coreFeatures.adoc +++ b/docs/asciidoc/coreFeatures.adoc @@ -213,8 +213,8 @@ problems and lead to too many threads (see below). give a blocking process its own thread so that it does not tie up other resources. This is a better choice for I/O blocking work. See <>, but doesn't pressure the system too much with new threads. Starting from 3.6.0 this can offer two different implementations depending on the setup: - - `ExecutorService`-based, which reuses Platform `Thread`s between tasks. This -implementation is like its predecessor `elastic()` creates new worker pools as needed + - `ExecutorService`-based, which reuses platform threads between tasks. This +implementation, like its predecessor `elastic()`, creates new worker pools as needed and reuses idle ones. Worker pools that stay idle for too long (the default is 60s) are also disposed. Unlike its `elastic()` predecessor, it has a cap on the number of backing threads it can create (default is number of CPU cores x 10). Up to 100 000 tasks submitted after the cap has been reached are enqueued and will be re-scheduled when a thread becomes available @@ -1159,4 +1159,4 @@ converted.subscribe( [[sinks]] == Sinks -include::processors.adoc[leveloffset=3] \ No newline at end of file +include::processors.adoc[leveloffset=3] From 8331346774c6fd397aa5646949d5d7800312bb60 Mon Sep 17 00:00:00 2001 From: injae kim Date: Tue, 5 Mar 2024 20:07:52 +0900 Subject: [PATCH 256/312] Add TimeoutException as cause on Mono.block* and Flux.block* (#3733) When `IllegalStateException` is thrown due to a timeout, a `TimeoutException` is added as the cause in case of `Mono.block*` and `Flux.block*`. The user can use this instead of digging into `IllegalStateException` message. Fixes #3709. --- .../publisher/BlockingOptionalMonoSubscriber.java | 6 ++++-- .../core/publisher/BlockingSingleSubscriber.java | 6 ++++-- .../src/main/java/reactor/core/publisher/Flux.java | 8 ++++++-- .../src/main/java/reactor/core/publisher/Mono.java | 8 ++++++-- .../BlockingOptionalMonoSubscriberTest.java | 6 ++++-- .../java/reactor/core/publisher/BlockingTests.java | 12 ++++++++---- .../reactor/core/publisher/SinkOneMulticastTest.java | 9 ++++++--- 7 files changed, 38 insertions(+), 17 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/BlockingOptionalMonoSubscriber.java b/reactor-core/src/main/java/reactor/core/publisher/BlockingOptionalMonoSubscriber.java index 31ab7a9991..a42f17f22b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/BlockingOptionalMonoSubscriber.java +++ b/reactor-core/src/main/java/reactor/core/publisher/BlockingOptionalMonoSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.reactivestreams.Subscription; import reactor.core.Disposable; @@ -148,7 +149,8 @@ final Optional blockingGet(long timeout, TimeUnit unit) { try { if (!await(timeout, unit)) { dispose(); - throw new IllegalStateException("Timeout on blocking read for " + timeout + " " + unit); + String errorMessage = "Timeout on blocking read for " + timeout + " " + unit; + throw new IllegalStateException(errorMessage, new TimeoutException(errorMessage)); } } catch (InterruptedException ex) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/BlockingSingleSubscriber.java b/reactor-core/src/main/java/reactor/core/publisher/BlockingSingleSubscriber.java index 7adc8b5292..e0615692b5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/BlockingSingleSubscriber.java +++ b/reactor-core/src/main/java/reactor/core/publisher/BlockingSingleSubscriber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.reactivestreams.Subscription; import reactor.core.Disposable; @@ -124,7 +125,8 @@ final T blockingGet(long timeout, TimeUnit unit) { try { if (!await(timeout, unit)) { dispose(); - throw new IllegalStateException("Timeout on blocking read for " + timeout + " " + unit); + String errorMessage = "Timeout on blocking read for " + timeout + " " + unit; + throw new IllegalStateException(errorMessage, new TimeoutException(errorMessage)); } } catch (InterruptedException ex) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index e4e9640e66..ad5b09838b 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -2771,7 +2771,8 @@ public final T blockFirst() { * signals its first value, completes or a timeout expires. Returns that value, * or null if the Flux completes empty. In case the Flux errors, the original * exception is thrown (wrapped in a {@link RuntimeException} if it was a checked - * exception). If the provided timeout expires, a {@link RuntimeException} is thrown. + * exception). If the provided timeout expires, a {@link RuntimeException} is thrown + * with a {@link TimeoutException} as the cause. *

    * Note that each blockFirst() will trigger a new subscription: in other words, * the result might miss signal from hot publishers. @@ -2780,6 +2781,7 @@ public final T blockFirst() { * * * @param timeout maximum time period to wait for before raising a {@link RuntimeException} + * with a {@link TimeoutException} as the cause * @return the first value or null */ @Nullable @@ -2821,7 +2823,8 @@ public final T blockLast() { * signals its last value, completes or a timeout expires. Returns that value, * or null if the Flux completes empty. In case the Flux errors, the original * exception is thrown (wrapped in a {@link RuntimeException} if it was a checked - * exception). If the provided timeout expires, a {@link RuntimeException} is thrown. + * exception). If the provided timeout expires, a {@link RuntimeException} is thrown + * with a {@link TimeoutException} as the cause. *

    * Note that each blockLast() will trigger a new subscription: in other words, * the result might miss signal from hot publishers. @@ -2830,6 +2833,7 @@ public final T blockLast() { * * * @param timeout maximum time period to wait for before raising a {@link RuntimeException} + * with a {@link TimeoutException} as the cause * @return the last value or null */ @Nullable diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index 847cf3fcaa..220e411485 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -1784,7 +1784,8 @@ public T block() { * received or a timeout expires. Returns that value, or null if the Mono completes * empty. In case the Mono errors, the original exception is thrown (wrapped in a * {@link RuntimeException} if it was a checked exception). - * If the provided timeout expires, a {@link RuntimeException} is thrown. + * If the provided timeout expires, a {@link RuntimeException} is thrown + * with a {@link TimeoutException} as the cause. * *

    * @@ -1793,6 +1794,7 @@ public T block() { * might miss signal from hot publishers. * * @param timeout maximum time period to wait for before raising a {@link RuntimeException} + * with a {@link TimeoutException} as the cause * * @return T the result */ @@ -1836,7 +1838,8 @@ public Optional blockOptional() { * Exception via {@link Optional#orElseThrow(Supplier)}. * In case the Mono itself errors, the original exception is thrown (wrapped in a * {@link RuntimeException} if it was a checked exception). - * If the provided timeout expires, a {@link RuntimeException} is thrown. + * If the provided timeout expires, a {@link RuntimeException} is thrown + * with a {@link TimeoutException} as the cause. * *

    * @@ -1845,6 +1848,7 @@ public Optional blockOptional() { * might miss signal from hot publishers. * * @param timeout maximum time period to wait for before raising a {@link RuntimeException} + * with a {@link TimeoutException} as the cause * * @return T the result */ diff --git a/reactor-core/src/test/java/reactor/core/publisher/BlockingOptionalMonoSubscriberTest.java b/reactor-core/src/test/java/reactor/core/publisher/BlockingOptionalMonoSubscriberTest.java index d9404aa52e..3967b630e8 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/BlockingOptionalMonoSubscriberTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/BlockingOptionalMonoSubscriberTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; @@ -103,7 +104,8 @@ public void timeoutOptionalTimingOut() { // Using sub-millis timeouts after gh-1734 assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> source.blockOptional(Duration.ofNanos(100))) - .withMessage("Timeout on blocking read for 100 NANOSECONDS"); + .withMessage("Timeout on blocking read for 100 NANOSECONDS") + .withCause(new TimeoutException("Timeout on blocking read for 100 NANOSECONDS")); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/BlockingTests.java b/reactor-core/src/test/java/reactor/core/publisher/BlockingTests.java index 9c6be6c177..4ec8342461 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/BlockingTests.java +++ b/reactor-core/src/test/java/reactor/core/publisher/BlockingTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -87,7 +88,8 @@ public void blockingFirstTimeout() { assertThatIllegalStateException().isThrownBy(() -> Flux.just(1).delayElements(Duration.ofSeconds(1)) .blockFirst(Duration.ofMillis(1))) - .withMessage("Timeout on blocking read for 1000000 NANOSECONDS"); + .withMessage("Timeout on blocking read for 1000000 NANOSECONDS") + .withCause(new TimeoutException("Timeout on blocking read for 1000000 NANOSECONDS")); } @Test @@ -115,7 +117,8 @@ public void blockingLastTimeout() { assertThatIllegalStateException().isThrownBy(() -> Flux.just(1).delayElements(Duration.ofMillis(100)) .blockLast(Duration.ofNanos(50))) - .withMessage("Timeout on blocking read for 50 NANOSECONDS"); + .withMessage("Timeout on blocking read for 50 NANOSECONDS") + .withCause(new TimeoutException("Timeout on blocking read for 50 NANOSECONDS")); } @@ -287,7 +290,8 @@ public void monoBlockOptionalDoesntCancel() { @Test public void monoBlockSupportsNanos() { assertThatIllegalStateException().isThrownBy(() -> Mono.never().block(Duration.ofNanos(9_000L))) - .withMessage("Timeout on blocking read for 9000 NANOSECONDS"); + .withMessage("Timeout on blocking read for 9000 NANOSECONDS") + .withCause(new TimeoutException("Timeout on blocking read for 9000 NANOSECONDS")); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/SinkOneMulticastTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinkOneMulticastTest.java index a2e7109db4..b28ec86876 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/SinkOneMulticastTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SinkOneMulticastTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2021-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package reactor.core.publisher; import java.time.Duration; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -247,7 +248,8 @@ void blockNegativeIsImmediateTimeout() { assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> sink.block(Duration.ofNanos(-1))) - .withMessage("Timeout on blocking read for 0 NANOSECONDS"); + .withMessage("Timeout on blocking read for 0 NANOSECONDS") + .withCause(new TimeoutException("Timeout on blocking read for 0 NANOSECONDS")); assertThat(Duration.ofNanos(System.nanoTime() - start)) .isLessThan(Duration.ofMillis(500)); @@ -260,7 +262,8 @@ void blockZeroIsImmediateTimeout() { assertThatExceptionOfType(IllegalStateException.class) .isThrownBy(() -> sink.block(Duration.ZERO)) - .withMessage("Timeout on blocking read for 0 NANOSECONDS"); + .withMessage("Timeout on blocking read for 0 NANOSECONDS") + .withCause(new TimeoutException("Timeout on blocking read for 0 NANOSECONDS")); assertThat(Duration.ofNanos(System.nanoTime() - start)) .isLessThan(Duration.ofMillis(500)); From 8eba91f309c05e853ccc222d6fa8e4aa1867e2ae Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Mon, 11 Mar 2024 19:35:25 +0200 Subject: [PATCH 257/312] Bump `Micrometer` to version `1.12.4` (#3743) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29bed3e486..f6bcf55a4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "4.0.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.3" +micrometer = "1.12.4" micrometerDocsGenerator = "1.0.2" micrometerTracingTest="1.2.3" contextPropagation="1.1.1" From 7b145f0f7f57f53101843a9ed3c1eafbcef9ae18 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Mar 2024 09:42:57 +0200 Subject: [PATCH 258/312] Bump `Micrometer Tracing` to version `1.2.4` (#3744) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6bcf55a4e..0deb8354c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ asciidoctor = "4.0.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below micrometer = "1.12.4" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.3" +micrometerTracingTest="1.2.4" contextPropagation="1.1.1" kotlin = "1.5.32" reactiveStreams = "1.0.4" From 82307fb491fc57f4e466a4650f670372779a1e03 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Mar 2024 10:20:27 +0200 Subject: [PATCH 259/312] [release] Prepare and release 3.5.15 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0b22f91c4a..66cb726d2a 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.14" - testCompile "io.projectreactor:reactor-test:3.5.14" + compile "io.projectreactor:reactor-core:3.5.15" + testCompile "io.projectreactor:reactor-test:3.5.15" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.15-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.15-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.16-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.16-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.14" + // implementation "io.projectreactor:reactor-tools:3.5.15" } ``` diff --git a/gradle.properties b/gradle.properties index eae398fa25..977db87f47 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.15-SNAPSHOT -bomVersion=2022.0.15 -metricsMicrometerVersion=1.0.15-SNAPSHOT +version=3.5.15 +bomVersion=2022.0.17 +metricsMicrometerVersion=1.0.15 From 8ec2cf2dd2f14150faeb5c0ca45f0c82ef5830d8 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Mar 2024 11:10:29 +0200 Subject: [PATCH 260/312] [release] Next development version 3.5.16-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 977db87f47..f6b5dadb1d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.5.15 +version=3.5.16-SNAPSHOT bomVersion=2022.0.17 -metricsMicrometerVersion=1.0.15 +metricsMicrometerVersion=1.0.16-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8bbf69bdc6..3d4889afd5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.14" -baselinePerfCore = "3.5.14" +baseline-core-api = "3.5.15" +baselinePerfCore = "3.5.15" baselinePerfExtra = "3.5.1" # Other shared versions From 5aedeaa34593aef3640f14a904857b9fd4e5ea7a Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Mar 2024 11:17:58 +0200 Subject: [PATCH 261/312] [release] Prepare and release 3.6.4 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 641e22a1f7..56387cd1b4 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.3" - testCompile "io.projectreactor:reactor-test:3.6.3" + compile "io.projectreactor:reactor-core:3.6.4" + testCompile "io.projectreactor:reactor-test:3.6.4" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.4-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.4-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.5-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.5-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.3" + // implementation "io.projectreactor:reactor-tools:3.6.4" } ``` diff --git a/gradle.properties b/gradle.properties index 55691dc586..bfe4ba6271 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.4-SNAPSHOT -bomVersion=2023.0.3 -metricsMicrometerVersion=1.1.4-SNAPSHOT +version=3.6.4 +bomVersion=2023.0.4 +metricsMicrometerVersion=1.1.4 From 8d879a7d8973f2cb198fed880f17c6ab8dd5d37a Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 12 Mar 2024 12:32:12 +0200 Subject: [PATCH 262/312] [release] Next development version 3.6.5-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- reactor-core/build.gradle | 6 ------ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/gradle.properties b/gradle.properties index bfe4ba6271..0194a86b32 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=3.6.4 +version=3.6.5-SNAPSHOT bomVersion=2023.0.4 -metricsMicrometerVersion=1.1.4 +metricsMicrometerVersion=1.1.5-SNAPSHOT diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0deb8354c7..46ff481eee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.3" -baselinePerfCore = "3.6.3" +baseline-core-api = "3.6.4" +baselinePerfCore = "3.6.4" baselinePerfExtra = "3.5.1" # Other shared versions diff --git a/reactor-core/build.gradle b/reactor-core/build.gradle index 0394410e4c..be5d667584 100644 --- a/reactor-core/build.gradle +++ b/reactor-core/build.gradle @@ -251,15 +251,9 @@ task japicmp(type: JapicmpTask) { includeSynthetic = true compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ] - // TODO after a .0 release, bump the gradle.properties baseline - // TODO after a .0 release, remove the reactor-core exclusions below if any classExcludes = [ ] methodExcludes = [ - "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function)", - "reactor.core.publisher.Flux#using(java.util.concurrent.Callable, java.util.function.Function, boolean)", - "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function)", - "reactor.core.publisher.Mono#using(java.util.concurrent.Callable, java.util.function.Function, boolean)" ] } From 99b61f9bf965deda654a2e7062f27b26aea7df5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 15 Mar 2024 11:02:14 +0100 Subject: [PATCH 263/312] [build] Migrate remaining gradle-build-action uses (#3758) --- .github/workflows/nightly.yml | 6 +++--- .github/workflows/snapshots.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 1036fb9864..9ac432ec88 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -20,7 +20,7 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 + - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 name: gradle with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=!slow -DuseSnapshotMicrometerVersion=true @@ -39,7 +39,7 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 + - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 name: gradle with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=slow -DuseSnapshotMicrometerVersion=true @@ -58,7 +58,7 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 + - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 name: other tests with: arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 8816b7c956..6a8fa7d6a9 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -30,6 +30,6 @@ jobs: distribution: 'temurin' java-version: 8 - name: Run Gradle Tests - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # tag=v2 + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 with: arguments: ${{ matrix.test-type.arguments }} \ No newline at end of file From 1d9994cdc42eaed9b90056938005db8d16823ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 15 Mar 2024 12:42:47 +0100 Subject: [PATCH 264/312] [build] #3758 follow-up to update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edda37553b..4a0a57e92b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,7 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # tag=v2 + - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 name: gradle with: arguments: :reactor-core:java21Test --no-daemon -Pjunit-tags=!slow From e6f6d4726e34f70faf50da54a171f97b12cc328b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 15 Mar 2024 12:48:41 +0100 Subject: [PATCH 265/312] [build] #3754 follow-up to update new workflows --- .github/workflows/nightly.yml | 6 +++--- .github/workflows/snapshots.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 9ac432ec88..01793839fd 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 with: ref: ${{ matrix.branch }} - - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 with: ref: ${{ matrix.branch }} - - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' java-version: 8 @@ -54,7 +54,7 @@ jobs: - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 with: ref: ${{ matrix.branch }} - - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' java-version: 8 diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 6a8fa7d6a9..c55b5870c4 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -25,7 +25,7 @@ jobs: with: ref: ${{ matrix.branch }} - name: Setup Java8 - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # tag=v3 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' java-version: 8 From 68e2ca92814260e31085e14635b45c7c4bb03400 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 19 Mar 2024 11:56:07 +0200 Subject: [PATCH 266/312] [build] Fix remote branch used by spotless when running locally (#3761) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 132c5be700..dfdde40f0b 100644 --- a/build.gradle +++ b/build.gradle @@ -107,7 +107,7 @@ spotless { enforceCheck false } else { - String spotlessBranch = "origin/main" + String spotlessBranch = "origin/3.5.x" println "[Spotless] Local run detected, ratchet from $spotlessBranch" ratchetFrom spotlessBranch } From 19fd40a47c7054f7fe4c8f79492081f9ef6e28a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 19 Mar 2024 12:56:03 +0100 Subject: [PATCH 267/312] [build] Include 3.6.x in github workflows (#3763) --- .github/workflows/ci.yml | 32 ++++++++++++++++---------------- .github/workflows/nightly.yml | 26 +++++++++++++++----------- .github/workflows/publish.yml | 31 ++++++++++++++++--------------- .github/workflows/snapshots.yml | 8 ++++---- 4 files changed, 51 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8955d0e5bb..3374a8036b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,18 +11,18 @@ jobs: with: fetch-depth: 0 #needed by spotless - name: Download JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -64,18 +64,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -90,25 +90,25 @@ jobs: with: arguments: :reactor-core:test --no-daemon -Pjunit-tags=!slow java-21-core-fast: - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) name: Java 21 core fast tests runs-on: ubuntu-latest needs: preliminary steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -129,18 +129,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -161,18 +161,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.base_ref == 'main' }} + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index b97acdf5c3..11edaef45d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - branch: [ 3.5.x, main ] + branch: [ 3.5.x, 3.6.x, main ] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 with: @@ -45,10 +45,14 @@ jobs: java-21-core-fast: name: Java 21 core fast tests runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: [ 3.6.x, main ] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 with: - ref: main + ref: ${{ matrix.branch }} - name: Download JDK 9 run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash @@ -78,24 +82,24 @@ jobs: strategy: fail-fast: false matrix: - branch: [ 3.5.x, main ] + branch: [ 3.5.x, 3.6.x, main ] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 with: ref: ${{ matrix.branch }} - name: Download JDK 9 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -115,24 +119,24 @@ jobs: strategy: fail-fast: false matrix: - branch: [ 3.5.x, main ] + branch: [ 3.5.x, 3.6.x, main ] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 with: ref: ${{ matrix.branch }} - name: Download JDK 9 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -143,7 +147,7 @@ jobs: distribution: 'temurin' java-version: 8 - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) name: other tests with: arguments: check -x :reactor-core:test -x :reactor-core:java9Test -x :reactor-core:java21Test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=quick diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 75a02369d5..7e59122ece 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,6 +3,7 @@ on: push: branches: # For branches, better to list them explicitly than regexp include - main + - 3.6.x - 3.5.x - 3.4.x permissions: read-all @@ -20,18 +21,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -60,18 +61,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -97,18 +98,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -135,18 +136,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -175,18 +176,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ github.ref == 'refs/heads/main' }} + if: contains('main 3.6.x', github.ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index d5d5ab6304..e3e26e81fd 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - branch: [ 3.5.x, main ] + branch: [ 3.5.x, 3.6.x, main ] test-type: - type: core-fast arguments: ":reactor-core:test --no-daemon -Pjunit-tags=!slow -DuseSnapshotMicrometerVersion=true" @@ -31,18 +31,18 @@ jobs: with: ref: ${{ matrix.branch }} - name: Download Java9 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup Java9 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup Java21 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' From 8546893aa33b5ec6ec03ca51e2db4c7bced56818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 19 Mar 2024 13:01:18 +0100 Subject: [PATCH 268/312] [build] Follow-up to #3763 - remaining conditional steps --- .github/workflows/nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 11edaef45d..18ec3a7128 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -17,18 +17,18 @@ jobs: with: ref: ${{ matrix.branch }} - name: Download JDK 9 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: ${{ matrix.branch == 'main' }} + if: contains('main 3.6.x', matrix.branch) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' From 2e0e6ce4d62ff89ce189cd17ca9bdfae22dc01ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 20 Mar 2024 16:08:33 +0100 Subject: [PATCH 269/312] Follow-up to #3764 - github.ref -> github.base_ref --- .github/workflows/publish.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 96ed49a9da..3a88b6fac6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,18 +30,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -72,18 +72,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -110,18 +110,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' @@ -150,18 +150,18 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4 - name: Download JDK 9 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) run: ${GITHUB_WORKSPACE}/.github/setup.sh shell: bash - name: Setup JDK 9 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'jdkfile' java-version: 9.0.4 jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - name: Setup JDK 21 - if: contains('main 3.6.x', github.ref) + if: contains('main 3.6.x', github.base_ref) uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' From 4c4f6fa1143f0cb36c551c836663fa3f31d4e60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 20 Mar 2024 16:21:17 +0100 Subject: [PATCH 270/312] Follow-up to #3764 - disable fail-fast --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3a88b6fac6..cc1efc1c9d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,7 @@ jobs: # This includes the tagging and drafting of release notes. Still, when possible we favor plain run of gradle tasks runs-on: ubuntu-20.04 strategy: + fail-fast: false matrix: test-type: - type: core From 70339f5341cbc7825462312e122dfcc9b58e4487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 20 Mar 2024 20:28:40 +0100 Subject: [PATCH 271/312] [test] Ignore bufferTimeout discard validation (#3767) Until #3531 is resolved the bufferTimeout discard support validation can be ignored to avoid build failures. --- .../reactor/core/publisher/OnDiscardShouldNotLeakTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java index 07c478a6d6..22d323bc9f 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -157,7 +157,8 @@ public class OnDiscardShouldNotLeakTest { DiscardScenario.fluxSource("monoFilterWhenFalse", main -> main.last().filterWhen(__ -> Mono.just(false).hide())), DiscardScenario.fluxSource("last", main -> main.last(new Tracked("default")).flatMap(f -> Mono.just(f).hide())), DiscardScenario.fluxSource("flatMapIterable", f -> f.flatMapIterable(Arrays::asList)), - DiscardScenario.fluxSource("bufferTimeout", f -> f.bufferTimeout(2, Duration.ofMillis(1), true).flatMapIterable(Function.identity())), + // FIXME: uncomment once https://github.com/reactor/reactor-core/issues/3531 is resolved +// DiscardScenario.fluxSource("bufferTimeout", f -> f.bufferTimeout(2, Duration.ofMillis(1), true).flatMapIterable(Function.identity())), DiscardScenario.fluxSource("publishOnDelayErrors", f -> f.publishOn(Schedulers.immediate())), DiscardScenario.fluxSource("publishOnImmediateErrors", f -> f.publishOn(Schedulers.immediate(), false, Queues.SMALL_BUFFER_SIZE)), DiscardScenario.fluxSource("publishOnAndPublishOn", main -> main From 3fafbe6680fc8bd4b875f9da2c3c4772b526e0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 20 Mar 2024 21:20:11 +0100 Subject: [PATCH 272/312] [build] Rename and refactor nightly.yml -> full.yml (#3768) Simplifying the nightly.yml workflow to reflect snapshot.yml workflow file. The file is renamed to full.yml. --- .github/workflows/full.yml | 35 +++++++++++++++++++++++++ .github/workflows/nightly.yml | 45 --------------------------------- .github/workflows/snapshots.yml | 2 +- 3 files changed, 36 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/full.yml delete mode 100644 .github/workflows/nightly.yml diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml new file mode 100644 index 0000000000..117f0b0a21 --- /dev/null +++ b/.github/workflows/full.yml @@ -0,0 +1,35 @@ +name: Full Check + +on: + schedule: + - cron: "0 14 * * 0" +permissions: read-all +jobs: + run-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: [ 3.5.x, main ] + test-type: + - type: core + arguments: ":reactor-core:test --no-daemon" + - type: other + arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon" + - type: jcstress + arguments: ":reactor-core:jcstress -Pjcstress.mode=default --no-daemon" + name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests + steps: + - name: Checkout Repository + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 + with: + ref: ${{ matrix.branch }} + - name: Setup Java 8 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 + with: + distribution: 'temurin' + java-version: 8 + - name: Run Gradle Tests + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 + with: + arguments: ${{ matrix.test-type.arguments }} \ No newline at end of file diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index 81dc68d9c8..0000000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Nightly Check - -on: - schedule: - - cron: "0 14 * * 0" -permissions: read-all -jobs: - core: - name: core fast tests - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - branch: [ 3.5.x, main ] - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 - with: - ref: ${{ matrix.branch }} - - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 - with: - distribution: 'temurin' - java-version: 8 - - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 - name: gradle - with: - arguments: :reactor-core:test --no-daemon -DuseSnapshotMicrometerVersion=true - other: - name: other tests - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - branch: [ 3.5.x, main ] - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 - with: - ref: ${{ matrix.branch }} - - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 - with: - distribution: 'temurin' - java-version: 8 - - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 - name: other tests - with: - arguments: check -x :reactor-core:test -x spotlessCheck --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default \ No newline at end of file diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 9e818183c1..85b4e03903 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 with: ref: ${{ matrix.branch }} - - name: Setup Java8 + - name: Setup Java 8 uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 with: distribution: 'temurin' From 4933eb5a4ca9d90682aa17edab077592ea631656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 20 Mar 2024 23:11:43 +0100 Subject: [PATCH 273/312] Follow-up to #3768 - separate 3.5.x runs --- .github/workflows/full.yml | 54 +++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index 1638b19f50..b86442799a 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - branch: [ 3.5.x, 3.6.x, main ] + branch: [ 3.6.x, main ] test-type: - type: core arguments: ":reactor-core:test --no-daemon" @@ -19,17 +19,53 @@ jobs: arguments: ":reactor-core:java21Test --no-daemon" - type: other arguments: "check -x :reactor-core:test -x :reactor-core:java21Test -x :reactor-core:java9Test -x spotlessCheck -x :reactor-core:jcstress --no-daemon" - - type: other-3-5-x + - type: jcstress + arguments: ":reactor-core:jcstress -Pjcstress.mode=default --no-daemon" + name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests + steps: + - name: Checkout Repository + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 + with: + ref: ${{ matrix.branch }} + - name: Download Java 9 + if: contains('main 3.6.x', matrix.branch) + run: ${GITHUB_WORKSPACE}/.github/setup.sh + shell: bash + - name: Setup Java 9 + if: contains('main 3.6.x', matrix.branch) + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 + with: + distribution: 'jdkfile' + java-version: 9.0.4 + jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz + - name: Setup Java 21 + if: contains('main 3.6.x', matrix.branch) + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 + with: + distribution: 'temurin' + java-version: 21 + - name: Setup Java 8 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 + with: + distribution: 'temurin' + java-version: 8 + - name: Run Gradle Tests + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 + with: + arguments: ${{ matrix.test-type.arguments }} + run-tests-3-5-x: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: [ 3.5.x ] + test-type: + - type: core + arguments: ":reactor-core:test --no-daemon" + - type: other arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon" - type: jcstress arguments: ":reactor-core:jcstress -Pjcstress.mode=default --no-daemon" - exclude: - - branch: [ 3.6.x, main ] - test-type: - type: other-3-5-x - - branch: 3.5.x - test-type: - type: [ core-java21, other ] name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests steps: - name: Checkout Repository From d0411a65ac9570b96dd4a998916fce7b575ddd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 21 Mar 2024 12:25:16 +0100 Subject: [PATCH 274/312] [build] Remove redundant jcstress param from snapshots.yml --- .github/workflows/snapshots.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 85b4e03903..15d6ebe9c3 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -15,7 +15,7 @@ jobs: - type: core arguments: ":reactor-core:test --no-daemon -DuseSnapshotMicrometerVersion=true" - type: other - arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon -DuseSnapshotMicrometerVersion=true -Pjcstress.mode=default" + arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress -DuseSnapshotMicrometerVersion=true --no-daemon" name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests steps: - name: Checkout Repository From 3fabcd49a871406afb4682c00133c8e4db5e0489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 21 Mar 2024 12:59:14 +0100 Subject: [PATCH 275/312] [build] Simplify full.yml and snapshots.yml workflows (#3770) --- .github/workflows/full.yml | 55 +++------------------------------ .github/workflows/snapshots.yml | 9 ++---- 2 files changed, 6 insertions(+), 58 deletions(-) diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index b86442799a..e7fadacd33 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -11,61 +11,14 @@ jobs: strategy: fail-fast: false matrix: - branch: [ 3.6.x, main ] + branch: [ 3.5.x, 3.6.x, main ] test-type: - type: core arguments: ":reactor-core:test --no-daemon" - - type: core-java21 - arguments: ":reactor-core:java21Test --no-daemon" - - type: other - arguments: "check -x :reactor-core:test -x :reactor-core:java21Test -x :reactor-core:java9Test -x spotlessCheck -x :reactor-core:jcstress --no-daemon" - - type: jcstress - arguments: ":reactor-core:jcstress -Pjcstress.mode=default --no-daemon" - name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests - steps: - - name: Checkout Repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # tag=v4 - with: - ref: ${{ matrix.branch }} - - name: Download Java 9 - if: contains('main 3.6.x', matrix.branch) - run: ${GITHUB_WORKSPACE}/.github/setup.sh - shell: bash - - name: Setup Java 9 - if: contains('main 3.6.x', matrix.branch) - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 - with: - distribution: 'jdkfile' - java-version: 9.0.4 - jdkFile: /opt/openjdk/java9/OpenJDK9U-jdk_x64_linux_hotspot_9.0.4_11.tar.gz - - name: Setup Java 21 - if: contains('main 3.6.x', matrix.branch) - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 - with: - distribution: 'temurin' - java-version: 21 - - name: Setup Java 8 - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # tag=v3 - with: - distribution: 'temurin' - java-version: 8 - - name: Run Gradle Tests - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 - with: - arguments: ${{ matrix.test-type.arguments }} - run-tests-3-5-x: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - branch: [ 3.5.x ] - test-type: - - type: core - arguments: ":reactor-core:test --no-daemon" - - type: other + - type: other # includes java21Test and java9Test for 3.6+ arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress --no-daemon" - type: jcstress - arguments: ":reactor-core:jcstress -Pjcstress.mode=default --no-daemon" + arguments: ":reactor-core:jcstress -Pjcstress.mode=quick --no-daemon" name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests steps: - name: Checkout Repository @@ -97,4 +50,4 @@ jobs: - name: Run Gradle Tests uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # tag=v3 with: - arguments: ${{ matrix.test-type.arguments }} \ No newline at end of file + arguments: ${{ matrix.test-type.arguments }} diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 0abd7534d4..3baded3c4d 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -1,6 +1,7 @@ name: Snapshots Check on: + workflow_dispatch: schedule: - cron: "0 20 * * *" permissions: read-all @@ -14,14 +15,8 @@ jobs: test-type: - type: core arguments: ":reactor-core:test --no-daemon -DuseSnapshotMicrometerVersion=true" - - type: core-java21 - arguments: ":reactor-core:java21Test --no-daemon -DuseSnapshotMicrometerVersion=true" - - type: other + - type: other # includes java21Test and java9Test for 3.6+ arguments: "check -x :reactor-core:test -x spotlessCheck -x :reactor-core:jcstress -DuseSnapshotMicrometerVersion=true --no-daemon" - exclude: - - branch: 3.5.x - test-type: - type: core-java21 name: Test on ${{ matrix.branch }} - ${{ matrix.test-type.type }} tests steps: - name: Checkout Repository From 001ff79f8141e020a5bf0cddb0744e0ff15cd9f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 21 Mar 2024 13:29:23 +0100 Subject: [PATCH 276/312] [build] Speedup ci.yml workflow --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8ae2fdc26..9601325800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,10 +65,12 @@ jobs: test-type: - type: core arguments: ":reactor-core:test --no-daemon" - - type: other - arguments: "check -x :reactor-core:test -x spotlessCheck -Pjcstress.mode=sanity --no-daemon" - type: core-java21 arguments: ":reactor-core:java21Test --no-daemon" + - type: core-java9 + arguments: ":reactor-core:java9Test --no-daemon" + - type: other + arguments: "check -x :reactor-core:test -x :reactor-core:java21Test -x :reactor-core:java9Test -x spotlessCheck -Pjcstress.mode=sanity --no-daemon" name: ${{ matrix.test-type.type }} tests needs: preliminary steps: From bc092d8b6cd2ccd4d9cd90bcd3d9584e4ee14fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Apr 2024 11:31:10 +0200 Subject: [PATCH 277/312] [release] Prepare and release 3.5.16 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 66cb726d2a..e6a20f38bb 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.15" - testCompile "io.projectreactor:reactor-test:3.5.15" + compile "io.projectreactor:reactor-core:3.5.16" + testCompile "io.projectreactor:reactor-test:3.5.16" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.16-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.16-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.17-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.17-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.15" + // implementation "io.projectreactor:reactor-tools:3.5.16" } ``` diff --git a/gradle.properties b/gradle.properties index acea40c3c4..f06498041a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.16-SNAPSHOT -bomVersion=2022.0.17 -metricsMicrometerVersion=1.0.16-SNAPSHOT +version=3.5.16 +bomVersion=2022.0.18 +metricsMicrometerVersion=1.0.16 org.gradle.parallel=true From baf6140a8a97cfa6b7dcb0a163f1016d38ca247d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Apr 2024 12:20:42 +0200 Subject: [PATCH 278/312] [release] Next development version 3.5.17-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index f06498041a..b8a0624e7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.16 +version=3.5.17-SNAPSHOT bomVersion=2022.0.18 -metricsMicrometerVersion=1.0.16 +metricsMicrometerVersion=1.0.17-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d0b218411..e1c96f1e8f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.15" -baselinePerfCore = "3.5.15" +baseline-core-api = "3.5.16" +baselinePerfCore = "3.5.16" baselinePerfExtra = "3.5.1" # Other shared versions From 826673d4215f34762647a3ce08d3eb6c2a4ea064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Apr 2024 13:43:24 +0200 Subject: [PATCH 279/312] [release] Prepare and release 3.6.5 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 56387cd1b4..65d4b9c840 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.4" - testCompile "io.projectreactor:reactor-test:3.6.4" + compile "io.projectreactor:reactor-core:3.6.5" + testCompile "io.projectreactor:reactor-test:3.6.5" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.5-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.5-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.6-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.6-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.4" + // implementation "io.projectreactor:reactor-tools:3.6.5" } ``` diff --git a/gradle.properties b/gradle.properties index 50980ca257..fadbc1336a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.5-SNAPSHOT -bomVersion=2023.0.4 -metricsMicrometerVersion=1.1.5-SNAPSHOT +version=3.6.5 +bomVersion=2023.0.5 +metricsMicrometerVersion=1.1.5 org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4acbdbd3e4..416bff87f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,9 +14,9 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "4.0.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.4" +micrometer = "1.12.5" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.4" +micrometerTracingTest="1.2.5" contextPropagation="1.1.1" kotlin = "1.8.22" reactiveStreams = "1.0.4" From 8b30803ecf79a4b384e43d5417d71c99bfbafebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Apr 2024 14:26:27 +0200 Subject: [PATCH 280/312] [release] Next development version 3.6.6-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- settings.gradle | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index fadbc1336a..1f09f7edb4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.5 +version=3.6.6-SNAPSHOT bomVersion=2023.0.5 -metricsMicrometerVersion=1.1.5 +metricsMicrometerVersion=1.1.6-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 416bff87f9..42779a6489 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.4" -baselinePerfCore = "3.6.4" +baseline-core-api = "3.6.5" +baselinePerfCore = "3.6.5" baselinePerfExtra = "3.5.1" # Other shared versions diff --git a/settings.gradle b/settings.gradle index 0e3ded581e..e9650f3a9d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,9 +25,9 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.12.4-SNAPSHOT') + version('micrometer', '1.12.6-SNAPSHOT') version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") - version('micrometerTracingTest', "1.2.4-SNAPSHOT") + version('micrometerTracingTest', "1.2.6-SNAPSHOT") version('contextPropagation', "1.1.2-SNAPSHOT") } } From 43bf82d68c172967cd3539ed06f77a65c3c94982 Mon Sep 17 00:00:00 2001 From: kkondratov Date: Thu, 11 Apr 2024 10:21:49 +0200 Subject: [PATCH 281/312] Fix pendingTasks accounting if TimedRunnable disposed before scheduling (#3780) Cancelled or rejected tasks properly decrement the pendingTasks metric. The chosen approach splits existing TimedRunnable into two implementations and moves code from the TimedScheduler into the TimedRunnable to reduce duplication. Two implementations: * WorkerBackedTimedRunnable * SchedulerBackedTimedRunnable are present to differentiate between Scheduler or a Worker that schedules a task (since there is no shared interface between those). In contrast to the initial proposal from #3697 by @nathankooij there are no extra objects being created and the scheduling is delegated to TimedRunnable. Fixes #3697 --------- Co-authored-by: Konstantin Kondratov <> --- .../micrometer/TimedScheduler.java | 178 +++++++++++++----- .../micrometer/TimedSchedulerTest.java | 144 ++++++++++++++ 2 files changed, 277 insertions(+), 45 deletions(-) diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java index 83b261ef45..c6bdc59400 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,53 +70,40 @@ final class TimedScheduler implements Scheduler { this.submittedPeriodicIteration = registry.counter(submittedName, tags.and(SubmittedTags.SUBMISSION.asString(), SubmittedTags.SUBMISSION_PERIODIC_ITERATION)); this.pendingTasks = LongTaskTimer.builder(TASKS_PENDING.getName(metricPrefix)) - .tags(tags).register(registry); + .tags(tags).register(registry); this.activeTasks = LongTaskTimer.builder(TASKS_ACTIVE.getName(metricPrefix)) - .tags(tags).register(registry); + .tags(tags).register(registry); this.completedTasks = registry.timer(TASKS_COMPLETED.getName(metricPrefix), tags); } TimedRunnable wrap(Runnable task) { - return new TimedRunnable(registry, this, task); + return new SchedulerBackedTimedRunnable(registry, this, delegate, task); } TimedRunnable wrapPeriodic(Runnable task) { - return new TimedRunnable(registry, this, task, true); + return new SchedulerBackedTimedRunnable(registry, this, delegate, task, true); } @Override public Disposable schedule(Runnable task) { - this.submittedDirect.increment(); TimedRunnable timedTask = wrap(task); - try { - return delegate.schedule(timedTask); - } - catch (RejectedExecutionException exception) { - timedTask.pendingSample.stop(); - throw exception; - } + return timedTask.schedule(); } @Override public Disposable schedule(Runnable task, long delay, TimeUnit unit) { - this.submittedDelayed.increment(); TimedRunnable timedTask = wrap(task); - try { - return delegate.schedule(timedTask, delay, unit); - } - catch (RejectedExecutionException exception) { - timedTask.pendingSample.stop(); - throw exception; - } + return timedTask.schedule(delay, unit); } @Override public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { - this.submittedPeriodicInitial.increment(); - return delegate.schedulePeriodically(wrapPeriodic(task), initialDelay, period, unit); + TimedRunnable timedTask = wrapPeriodic(task); + + return timedTask.schedulePeriodically(initialDelay, period, unit); } @Override @@ -154,6 +141,14 @@ static final class TimedWorker implements Worker { this.delegate = delegate; } + TimedRunnable wrap(Runnable task) { + return new WorkerBackedTimedRunnable(parent.registry, parent, delegate, task); + } + + TimedRunnable wrapPeriodic(Runnable task) { + return new WorkerBackedTimedRunnable(parent.registry, parent, delegate, task, true); + } + @Override public void dispose() { delegate.dispose(); @@ -166,41 +161,26 @@ public boolean isDisposed() { @Override public Disposable schedule(Runnable task) { - parent.submittedDirect.increment(); - TimedRunnable timedTask = parent.wrap(task); + TimedRunnable timedTask = wrap(task); - try { - return delegate.schedule(timedTask); - } - catch (RejectedExecutionException exception) { - timedTask.pendingSample.stop(); - throw exception; - } + return timedTask.schedule(); } @Override public Disposable schedule(Runnable task, long delay, TimeUnit unit) { - parent.submittedDelayed.increment(); - TimedRunnable timedTask = parent.wrap(task); + TimedRunnable timedTask = wrap(task); - try { - return delegate.schedule(timedTask, delay, unit); - } - catch (RejectedExecutionException exception) { - timedTask.pendingSample.stop(); - throw exception; - } + return timedTask.schedule(delay, unit); } @Override public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { - parent.submittedPeriodicInitial.increment(); - return delegate.schedulePeriodically(parent.wrapPeriodic(task), initialDelay, period, unit); + TimedRunnable timedTask = wrapPeriodic(task); + return timedTask.schedulePeriodically(initialDelay, period, unit); } } - static final class TimedRunnable implements Runnable { - + private static abstract class TimedRunnable implements Runnable, Disposable { final MeterRegistry registry; final TimedScheduler parent; final Runnable task; @@ -209,6 +189,8 @@ static final class TimedRunnable implements Runnable { boolean isRerun; + Disposable disposable; + TimedRunnable(MeterRegistry registry, TimedScheduler parent, Runnable task) { this(registry, parent, task, false); } @@ -245,5 +227,111 @@ public void run() { Runnable completionTrackingTask = parent.completedTasks.wrap(this.task); this.parent.activeTasks.record(completionTrackingTask); } + + public Disposable schedule() { + parent.submittedDirect.increment(); + + try { + disposable = this.internalSchedule(); + return this; + } catch (RejectedExecutionException exception) { + this.dispose(); + throw exception; + } + } + + public Disposable schedule(long delay, TimeUnit unit) { + parent.submittedDelayed.increment(); + + try { + disposable = this.internalSchedule(delay, unit); + return this; + } catch (RejectedExecutionException exception) { + this.dispose(); + throw exception; + } + } + + public Disposable schedulePeriodically(long initialDelay, long period, TimeUnit unit) { + parent.submittedPeriodicInitial.increment(); + return this.internalSchedulePeriodically(initialDelay, period, unit); + } + + @Override + public void dispose() { + if (disposable != null) { + disposable.dispose(); + } + + if (pendingSample != null) { + pendingSample.stop(); + } + } + + abstract Disposable internalSchedule(); + + abstract Disposable internalSchedule(long delay, TimeUnit unit); + + abstract Disposable internalSchedulePeriodically(long initialDelay, long period, TimeUnit unit); + } + + static final class WorkerBackedTimedRunnable extends TimedRunnable { + + final Worker worker; + + WorkerBackedTimedRunnable(MeterRegistry registry, TimedScheduler parent, Worker worker, Runnable task) { + super(registry, parent, task); + this.worker = worker; + } + + WorkerBackedTimedRunnable(MeterRegistry registry, TimedScheduler parent, Worker worker, Runnable task, boolean periodic) { + super(registry, parent, task, periodic); + this.worker = worker; + } + + @Override + Disposable internalSchedule() { + return worker.schedule(this); + } + + @Override + Disposable internalSchedule(long delay, TimeUnit unit) { + return worker.schedule(this, delay, unit); + } + + @Override + Disposable internalSchedulePeriodically(long initialDelay, long period, TimeUnit unit) { + return worker.schedulePeriodically(this, initialDelay, period, unit); + } + } + + static final class SchedulerBackedTimedRunnable extends TimedRunnable { + + final Scheduler scheduler; + + SchedulerBackedTimedRunnable(MeterRegistry registry, TimedScheduler parent, Scheduler scheduler, Runnable task) { + super(registry, parent, task); + this.scheduler = scheduler; + } + + SchedulerBackedTimedRunnable(MeterRegistry registry, TimedScheduler parent, Scheduler scheduler, Runnable task, boolean periodic) { + super(registry, parent, task, periodic); + this.scheduler = scheduler; + } + + @Override + Disposable internalSchedule() { + return scheduler.schedule(this); + } + + @Override + Disposable internalSchedule(long delay, TimeUnit unit) { + return scheduler.schedule(this, delay, unit); + } + + @Override + Disposable internalSchedulePeriodically(long initialDelay, long period, TimeUnit unit) { + return scheduler.schedulePeriodically(this, initialDelay, period, unit); + } } } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java index 2144399a44..6f0a26abf2 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java @@ -20,6 +20,7 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -39,6 +40,7 @@ import org.mockito.Mockito; import reactor.core.Disposable; +import reactor.core.Disposables; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.AutoDisposingExtension; @@ -470,4 +472,146 @@ void workerPendingTaskRemovedOnScheduleRejection() throws InterruptedException { .block(Duration.ofSeconds(1)); } } + + @Test + void pendingTaskRemovedOnCancellation() { + TimedScheduler testScheduler = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); + testScheduler.init(); + + RequiredSearch tasksPendingSearch = registry.get("test.scheduler.tasks.pending"); + RequiredSearch tasksActiveSearch = registry.get("test.scheduler.tasks.active"); + LongTaskTimer tasksPendingLongTaskTimer = tasksPendingSearch.longTaskTimer(); + LongTaskTimer tasksActiveLongTaskTimer = tasksActiveSearch.longTaskTimer(); + + try { + // Schedule a running task and a pending task + final CountDownLatch taskPause = new CountDownLatch(1); + assertThatNoException().isThrownBy(() -> testScheduler.schedule(() -> { + try { + taskPause.await(1, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + Disposable.Swap waitingTask = Disposables.swap(); + assertThatNoException().isThrownBy(() -> waitingTask.update(testScheduler.schedule(() -> {}))); + + // One task is pending to be scheduled and one is active + assertThat(tasksPendingLongTaskTimer.activeTasks()).as("active pending") + .isEqualTo(1); + assertThat(tasksActiveLongTaskTimer.activeTasks()).as("active") + .isEqualTo(1); + + // E.g. a `Mono#timeout` was never hit, so the task gets disposed. + waitingTask.dispose(); + + // The task should no longer be considered pending, as it was disposed. + assertThat(tasksPendingLongTaskTimer.activeTasks()).as("active pending") + .isZero(); + } finally { + testScheduler.disposeGracefully().block(Duration.ofSeconds(1)); + } + } + + @Test + void pendingTaskDelayedRemovedOnCancellation() { + TimedScheduler testScheduler = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); + testScheduler.init(); + + RequiredSearch requiredSearch = registry.get("test.scheduler.tasks.pending"); + LongTaskTimer longTaskTimer = requiredSearch.longTaskTimer(); + + try { + // Schedule a task far in the future. + Disposable.Swap waitingTask = Disposables.swap(); + assertThatNoException().isThrownBy(() -> waitingTask.update(testScheduler.schedule(() -> {}, 10_000, TimeUnit.SECONDS))); + + // It's pending to be scheduled. + assertThat(longTaskTimer.activeTasks()).as("active pending") + .isOne(); + + // E.g. a `Mono#timeout` was never hit, so the task gets disposed. + waitingTask.dispose(); + + // The task should no longer be considered pending, as it was disposed. + assertThat(longTaskTimer.activeTasks()).as("active pending") + .isZero(); + } finally { + testScheduler.disposeGracefully().block(Duration.ofSeconds(1)); + } + } + + @Test + void workerPendingTaskRemovedOnCancellation() { + TimedScheduler testScheduler = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); + testScheduler.init(); + Scheduler.Worker worker = testScheduler.createWorker(); + + RequiredSearch tasksPendingSearch = registry.get("test.scheduler.tasks.pending"); + RequiredSearch tasksActiveSearch = registry.get("test.scheduler.tasks.active"); + LongTaskTimer tasksPendingLongTaskTimer = tasksPendingSearch.longTaskTimer(); + LongTaskTimer tasksActiveLongTaskTimer = tasksActiveSearch.longTaskTimer(); + + try { + // Schedule a running task and a pending task + final CountDownLatch taskPause = new CountDownLatch(1); + assertThatNoException().isThrownBy(() -> worker.schedule(() -> { + try { + taskPause.await(1, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + Disposable.Swap waitingTask = Disposables.swap(); + assertThatNoException().isThrownBy(() -> waitingTask.update(worker.schedule(() -> {}))); + + // One task is pending to be scheduled and one is active + assertThat(tasksPendingLongTaskTimer.activeTasks()).as("active pending") + .isEqualTo(1); + assertThat(tasksActiveLongTaskTimer.activeTasks()).as("active") + .isEqualTo(1); + + // E.g. a `Mono#timeout` was never hit, so the task gets disposed. + waitingTask.dispose(); + + // The task should no longer be considered pending, as it was disposed. + assertThat(tasksPendingLongTaskTimer.activeTasks()).as("active pending") + .isZero(); + } finally { + worker.dispose(); + testScheduler.disposeGracefully().block(Duration.ofSeconds(1)); + } + } + + @Test + void workerPendingTaskDelayedRemovedOnCancellation() { + TimedScheduler testScheduler = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); + testScheduler.init(); + Scheduler.Worker worker = testScheduler.createWorker(); + + RequiredSearch requiredSearch = registry.get("test.scheduler.tasks.pending"); + LongTaskTimer longTaskTimer = requiredSearch.longTaskTimer(); + + try { + // Schedule a task far in the future. + Disposable.Swap waitingTask = Disposables.swap(); + assertThatNoException().isThrownBy(() -> waitingTask.update(worker.schedule(() -> {}, 10_000, TimeUnit.SECONDS))); + + // It's pending to be scheduled. + assertThat(longTaskTimer.activeTasks()).as("active pending") + .isOne(); + + // E.g. a `Mono#timeout` was never hit, so the task gets disposed. + waitingTask.dispose(); + + // The task should no longer be considered pending, as it was disposed. + assertThat(longTaskTimer.activeTasks()).as("active pending") + .isZero(); + } finally { + worker.dispose(); + testScheduler.disposeGracefully().block(Duration.ofSeconds(1)); + } + } } \ No newline at end of file From d11ea71b5c28271a2d1b5cbb0b11ee8dc84c9b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 11 Apr 2024 11:04:10 +0200 Subject: [PATCH 282/312] Follow-up to #3780 - fix flaky tests --- .../micrometer/TimedSchedulerTest.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java index 6f0a26abf2..b0c22467b4 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java @@ -474,7 +474,7 @@ void workerPendingTaskRemovedOnScheduleRejection() throws InterruptedException { } @Test - void pendingTaskRemovedOnCancellation() { + void pendingTaskRemovedOnCancellation() throws InterruptedException { TimedScheduler testScheduler = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); testScheduler.init(); @@ -486,17 +486,22 @@ void pendingTaskRemovedOnCancellation() { try { // Schedule a running task and a pending task final CountDownLatch taskPause = new CountDownLatch(1); + final CountDownLatch taskStarted = new CountDownLatch(1); assertThatNoException().isThrownBy(() -> testScheduler.schedule(() -> { try { - taskPause.await(1, TimeUnit.SECONDS); + taskStarted.countDown(); + taskPause.await(); } catch (InterruptedException e) { - throw new RuntimeException(e); + // Expected as Scheduler is disposed at the end and the task is interrupted } })); Disposable.Swap waitingTask = Disposables.swap(); assertThatNoException().isThrownBy(() -> waitingTask.update(testScheduler.schedule(() -> {}))); + // We need to wait for the task to start to properly account active vs pending + assertThat(taskStarted.await(1, TimeUnit.SECONDS)).isTrue(); + // One task is pending to be scheduled and one is active assertThat(tasksPendingLongTaskTimer.activeTasks()).as("active pending") .isEqualTo(1); @@ -543,7 +548,7 @@ void pendingTaskDelayedRemovedOnCancellation() { } @Test - void workerPendingTaskRemovedOnCancellation() { + void workerPendingTaskRemovedOnCancellation() throws InterruptedException { TimedScheduler testScheduler = new TimedScheduler(Schedulers.single(), registry, "test", Tags.empty()); testScheduler.init(); Scheduler.Worker worker = testScheduler.createWorker(); @@ -556,17 +561,22 @@ void workerPendingTaskRemovedOnCancellation() { try { // Schedule a running task and a pending task final CountDownLatch taskPause = new CountDownLatch(1); + final CountDownLatch taskStarted = new CountDownLatch(1); assertThatNoException().isThrownBy(() -> worker.schedule(() -> { try { - taskPause.await(1, TimeUnit.SECONDS); + taskStarted.countDown(); + taskPause.await(); } catch (InterruptedException e) { - throw new RuntimeException(e); + // Expected as Scheduler is disposed at the end and the task is interrupted } })); Disposable.Swap waitingTask = Disposables.swap(); assertThatNoException().isThrownBy(() -> waitingTask.update(worker.schedule(() -> {}))); + // We need to wait for the task to start to properly account active vs pending + assertThat(taskStarted.await(1, TimeUnit.SECONDS)).isTrue(); + // One task is pending to be scheduled and one is active assertThat(tasksPendingLongTaskTimer.activeTasks()).as("active pending") .isEqualTo(1); From c9fdc640830e86dc908e40af1d275d1317a48ab2 Mon Sep 17 00:00:00 2001 From: ChickenchickenLove Date: Thu, 11 Apr 2024 18:51:29 +0900 Subject: [PATCH 283/312] Fix BoundedElasticThreadPerTaskSchedulerTest flakiness (#3779) BoundedElasticThreadPerTaskSchedulerTest test cases could randomly fail due to asynchronous nature of disposing a task wrapper and not coordinating that with assertions. Using Awaitility.await().untilAsserted to wrap the assertions resolves the issue. Fixes https://github.com/reactor/reactor-core/issues/3772 --- .../BoundedElasticThreadPerTaskSchedulerTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java b/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java index 3f0f8c5ab8..d19dd4c7e6 100644 --- a/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java +++ b/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java @@ -75,7 +75,7 @@ public void ensuresTasksScheduling() throws InterruptedException { Disposable disposable = scheduler.schedule(latch::countDown); Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - Assertions.assertThat(disposable.isDisposed()).isTrue(); + Awaitility.await().untilAsserted(() -> Assertions.assertThat(disposable.isDisposed()).isTrue()); } @Test @@ -108,7 +108,7 @@ public void ensuresTasksDelayedScheduling() throws InterruptedException { .until(() -> ((ScheduledThreadPoolExecutor) resource.sharedDelayedTasksScheduler).getQueue().isEmpty()); Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - Assertions.assertThat(disposable.isDisposed()).isTrue(); + Awaitility.await().untilAsserted(() -> Assertions.assertThat(disposable.isDisposed()).isTrue()); } @Test @@ -135,7 +135,7 @@ public void ensuresTasksDelayedZeroDelayScheduling() throws InterruptedException Assertions.assertThat(((ScheduledThreadPoolExecutor) resource.sharedDelayedTasksScheduler).getQueue().size()).isZero(); Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - Assertions.assertThat(disposable.isDisposed()).isTrue(); + Awaitility.await().untilAsserted(() -> Assertions.assertThat(disposable.isDisposed()).isTrue()); // unblock scheduler awaiter.countDown(); @@ -153,7 +153,7 @@ public void ensuresTasksPeriodicScheduling() throws InterruptedException { Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); Assertions.assertThat(disposable.isDisposed()).isFalse(); disposable.dispose(); - Assertions.assertThat(disposable.isDisposed()).isTrue(); + Awaitility.await().untilAsserted(() -> Assertions.assertThat(disposable.isDisposed()).isTrue()); } @Test @@ -168,7 +168,7 @@ public void ensuresTasksPeriodicZeroInitialDelayScheduling() throws InterruptedE Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); Assertions.assertThat(disposable.isDisposed()).isFalse(); disposable.dispose(); - Assertions.assertThat(disposable.isDisposed()).isTrue(); + Awaitility.await().untilAsserted(() -> Assertions.assertThat(disposable.isDisposed()).isTrue()); } @Test @@ -183,7 +183,7 @@ public void ensuresTasksPeriodicWithInitialDelayAndInstantPeriodScheduling() thr Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); Assertions.assertThat(disposable.isDisposed()).isFalse(); disposable.dispose(); - Assertions.assertThat(disposable.isDisposed()).isTrue(); + Awaitility.await().untilAsserted(() -> Assertions.assertThat(disposable.isDisposed()).isTrue()); } @Test @@ -198,7 +198,7 @@ public void ensuresTasksPeriodicWithZeroInitialDelayAndInstantPeriodScheduling() Assertions.assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); Assertions.assertThat(disposable.isDisposed()).isFalse(); disposable.dispose(); - Assertions.assertThat(disposable.isDisposed()).isTrue(); + Awaitility.await().untilAsserted(() -> Assertions.assertThat(disposable.isDisposed()).isTrue()); } @Test From c760a0a4d9201ec104ba1a3dfbd1a7464e4212ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 6 May 2024 13:34:25 +0200 Subject: [PATCH 284/312] Improve auto context propagation in lifting and ConnectableFlux intersection (#3787) Combining ConnectableFlux or Fuseable operators and wrapping via Hooks could lead to ClassCastException or lack of ThreadLocal restoration. ConnectableFlux and Fuseable handling has been improved. Also, lifting now avoids unnecessary multiple-wrapping. Fixes #3762 --- .../core/publisher/ConnectableFlux.java | 9 +- .../ConnectableFluxRestoringThreadLocals.java | 42 ++++ .../core/publisher/ConnectableLift.java | 6 +- .../publisher/ConnectableLiftFuseable.java | 6 +- .../core/publisher/ContextPropagation.java | 9 +- .../java/reactor/core/publisher/Flux.java | 10 +- .../core/publisher/FluxAutoConnect.java | 5 +- .../publisher/FluxAutoConnectFuseable.java | 5 +- ...FluxContextWriteRestoringThreadLocals.java | 44 +--- ...extWriteRestoringThreadLocalsFuseable.java | 202 ++++++++++++++++++ .../java/reactor/core/publisher/FluxLift.java | 6 +- .../core/publisher/FluxLiftFuseable.java | 8 +- .../reactor/core/publisher/FluxPublish.java | 4 +- .../reactor/core/publisher/FluxRefCount.java | 8 +- .../core/publisher/FluxRefCountGrace.java | 6 +- .../reactor/core/publisher/FluxReplay.java | 4 +- .../reactor/core/publisher/GroupedLift.java | 6 +- .../core/publisher/GroupedLiftFuseable.java | 6 +- .../InternalConnectableFluxOperator.java | 6 +- .../java/reactor/core/publisher/MonoLift.java | 6 +- .../core/publisher/MonoLiftFuseable.java | 5 +- .../reactor/core/publisher/Operators.java | 30 ++- .../reactor/core/publisher/ParallelLift.java | 6 +- .../core/publisher/ParallelLiftFuseable.java | 6 +- .../AutomaticContextPropagationTest.java | 108 ++++++++-- .../ThreadSwitchingConnectableFlux.java | 78 +++++++ 26 files changed, 519 insertions(+), 112 deletions(-) create mode 100644 reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxRestoringThreadLocals.java create mode 100644 reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsFuseable.java create mode 100644 reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingConnectableFlux.java diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableFlux.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableFlux.java index 629fa4e57d..7f4aaedfe5 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableFlux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableFlux.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,13 @@ */ public abstract class ConnectableFlux extends Flux { + static ConnectableFlux from(ConnectableFlux source) { + if (ContextPropagationSupport.shouldWrapPublisher(source)) { + return new ConnectableFluxRestoringThreadLocals<>(source); + } + return source; + } + /** * Connects this {@link ConnectableFlux} to the upstream source when the first {@link org.reactivestreams.Subscriber} * subscribes. diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxRestoringThreadLocals.java new file mode 100644 index 0000000000..98bc3bf7a2 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableFluxRestoringThreadLocals.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.function.Consumer; + +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; + +class ConnectableFluxRestoringThreadLocals extends ConnectableFlux { + + private final ConnectableFlux source; + + public ConnectableFluxRestoringThreadLocals(ConnectableFlux source) { + this.source = Objects.requireNonNull(source, "source"); + } + + @Override + public void connect(Consumer cancelSupport) { + source.connect(cancelSupport); + } + + @Override + public void subscribe(CoreSubscriber actual) { + source.subscribe(Operators.restoreContextOnSubscriber(source, actual)); + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java index 9523098638..e650a2a252 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,8 +68,8 @@ public String stepName() { @Override public final CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - CoreSubscriber input = - liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + CoreSubscriber input = liftFunction.lifter.apply(source, actual); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java index 60b748292d..651b54b596 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ConnectableLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,8 +70,8 @@ public String stepName() { @Override public final CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - CoreSubscriber input = - liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + CoreSubscriber input = liftFunction.lifter.apply(source, actual); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java index bb7cc28774..ebd2d4dffa 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ContextPropagation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import io.micrometer.context.ContextSnapshotFactory; import io.micrometer.context.ThreadLocalAccessor; +import reactor.core.Fuseable; import reactor.core.observability.SignalListener; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -60,8 +61,10 @@ final class ContextPropagation { } } - static Flux fluxRestoreThreadLocals(Flux flux) { - return new FluxContextWriteRestoringThreadLocals<>(flux, Function.identity()); + static Flux fluxRestoreThreadLocals(Flux flux, boolean fuseable) { + return fuseable ? + new FluxContextWriteRestoringThreadLocalsFuseable<>(flux, Function.identity()) + : new FluxContextWriteRestoringThreadLocals<>(flux, Function.identity()); } static Mono monoRestoreThreadLocals(Mono mono) { diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index c6c9f8810b..d28f3fccce 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -11132,7 +11132,8 @@ static Flux wrap(Publisher source) { if (!shouldWrap) { return (Flux) source; } - return ContextPropagation.fluxRestoreThreadLocals((Flux) source); + return ContextPropagation.fluxRestoreThreadLocals( + (Flux) source, source instanceof Fuseable); } //for scalars we'll instantiate the operators directly to avoid onAssembly @@ -11151,19 +11152,20 @@ static Flux wrap(Publisher source) { } Flux target; + boolean fuseable = source instanceof Fuseable; if (source instanceof Mono) { - if (source instanceof Fuseable) { + if (fuseable) { target = new FluxSourceMonoFuseable<>((Mono) source); } else { target = new FluxSourceMono<>((Mono) source); } - } else if (source instanceof Fuseable) { + } else if (fuseable) { target = new FluxSourceFuseable<>(source); } else { target = new FluxSource<>(source); } if (shouldWrap) { - return ContextPropagation.fluxRestoreThreadLocals(target); + return ContextPropagation.fluxRestoreThreadLocals(target, fuseable); } return target; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxAutoConnect.java b/reactor-core/src/main/java/reactor/core/publisher/FluxAutoConnect.java index 5bc767e944..bbcfdc8443 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxAutoConnect.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxAutoConnect.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ final class FluxAutoConnect extends Flux if (n <= 0) { throw new IllegalArgumentException("n > required but it was " + n); } - this.source = Objects.requireNonNull(source, "source"); + this.source = ConnectableFlux.from(Objects.requireNonNull(source, "source")); this.cancelSupport = Objects.requireNonNull(cancelSupport, "cancelSupport"); REMAINING.lazySet(this, n); } @@ -75,6 +75,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.CAPACITY) return remaining; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxAutoConnectFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxAutoConnectFuseable.java index dc38c30832..49d4550106 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxAutoConnectFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxAutoConnectFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ final class FluxAutoConnectFuseable extends Flux if (n <= 0) { throw new IllegalArgumentException("n > required but it was " + n); } - this.source = Objects.requireNonNull(source, "source"); + this.source = ConnectableFlux.from(Objects.requireNonNull(source, "source")); this.cancelSupport = Objects.requireNonNull(cancelSupport, "cancelSupport"); REMAINING.lazySet(this, n); } @@ -76,6 +76,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return source; if (key == Attr.CAPACITY) return remaining; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java index c425db10c8..f2ee976b5a 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocals.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -173,46 +173,4 @@ public void cancel() { } } } - - static final class FuseableContextWriteRestoringThreadLocalsSubscriber - extends ContextWriteRestoringThreadLocalsSubscriber - implements Fuseable.QueueSubscription { - - FuseableContextWriteRestoringThreadLocalsSubscriber( - CoreSubscriber actual, Context context) { - super(actual, context); - } - - // Required for - // FuseableBestPracticesTest.coreFuseableSubscribersShouldNotExtendNonFuseableOnNext - @Override - public void onNext(T t) { - super.onNext(t); - } - - @Override - public T poll() { - throw new UnsupportedOperationException("Nope"); - } - - @Override - public int requestFusion(int requestedMode) { - return Fuseable.NONE; - } - - @Override - public int size() { - throw new UnsupportedOperationException("Nope"); - } - - @Override - public boolean isEmpty() { - throw new UnsupportedOperationException("Nope"); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("Nope"); - } - } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsFuseable.java new file mode 100644 index 0000000000..215f558136 --- /dev/null +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxContextWriteRestoringThreadLocalsFuseable.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2024 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.Objects; +import java.util.function.Function; + +import io.micrometer.context.ContextSnapshot; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +final class FluxContextWriteRestoringThreadLocalsFuseable extends FluxOperator + implements Fuseable { + + final Function doOnContext; + + FluxContextWriteRestoringThreadLocalsFuseable(Flux source, + Function doOnContext) { + super(source); + this.doOnContext = Objects.requireNonNull(doOnContext, "doOnContext"); + } + + @SuppressWarnings("try") + @Override + public void subscribe(CoreSubscriber actual) { + Context c = doOnContext.apply(actual.currentContext()); + + try (ContextSnapshot.Scope ignored = ContextPropagation.setThreadLocals(c)) { + source.subscribe(new FuseableContextWriteRestoringThreadLocalsSubscriber<>(actual, c)); + } + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; + return super.scanUnsafe(key); + } + + static class FuseableContextWriteRestoringThreadLocalsSubscriber + implements ConditionalSubscriber, InnerOperator, + Fuseable.QueueSubscription { + + final CoreSubscriber actual; + final ConditionalSubscriber actualConditional; + final Context context; + + Subscription s; + + @SuppressWarnings("unchecked") + FuseableContextWriteRestoringThreadLocalsSubscriber(CoreSubscriber actual, Context context) { + this.actual = actual; + this.context = context; + if (actual instanceof ConditionalSubscriber) { + this.actualConditional = (ConditionalSubscriber) actual; + } + else { + this.actualConditional = null; + } + } + + @Override + @Nullable + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) { + return s; + } + if (key == Attr.RUN_STYLE) { + return Attr.RunStyle.SYNC; + } + return InnerOperator.super.scanUnsafe(key); + } + + @Override + public Context currentContext() { + return this.context; + } + + @SuppressWarnings("try") + @Override + public void onSubscribe(Subscription s) { + // This is needed, as the downstream can then switch threads, + // continue the subscription using different primitives and omit this operator + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (Operators.validate(this.s, s)) { + this.s = s; + actual.onSubscribe(this); + } + } + } + + @SuppressWarnings("try") + @Override + public void onNext(T t) { + // We probably ended up here from a request, which set thread locals to + // current context, but we need to clean up and restore thread locals for + // the actual subscriber downstream, as it can expect TLs to match the + // different context. + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onNext(t); + } + } + + @SuppressWarnings("try") + @Override + public boolean tryOnNext(T t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + if (actualConditional != null) { + return actualConditional.tryOnNext(t); + } + actual.onNext(t); + return true; + } + } + + @SuppressWarnings("try") + @Override + public void onError(Throwable t) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onError(t); + } + } + + @SuppressWarnings("try") + @Override + public void onComplete() { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(actual.currentContext())) { + actual.onComplete(); + } + } + + @Override + public CoreSubscriber actual() { + return actual; + } + + @SuppressWarnings("try") + @Override + public void request(long n) { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(context)) { + s.request(n); + } + } + + @SuppressWarnings("try") + @Override + public void cancel() { + try (ContextSnapshot.Scope ignored = + ContextPropagation.setThreadLocals(context)) { + s.cancel(); + } + } + + @Override + public T poll() { + throw new UnsupportedOperationException("Nope"); + } + + @Override + public int requestFusion(int requestedMode) { + return Fuseable.NONE; + } + + @Override + public int size() { + throw new UnsupportedOperationException("Nope"); + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException("Nope"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Nope"); + } + } +} diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxLift.java b/reactor-core/src/main/java/reactor/core/publisher/FluxLift.java index f127974310..31a79db18f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,8 +52,8 @@ public Object scanUnsafe(Attr key) { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - CoreSubscriber input = - liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + CoreSubscriber input = liftFunction.lifter.apply(source, actual); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/FluxLiftFuseable.java index a3aedc88eb..72f41cb7b1 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,8 +54,8 @@ public Object scanUnsafe(Attr key) { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - CoreSubscriber input = - liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + CoreSubscriber input = liftFunction.lifter.apply(source, actual); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); @@ -65,6 +65,6 @@ public CoreSubscriber subscribeOrReturn(CoreSubscriber act input = new FluxHide.SuppressFuseableSubscriber<>(input); } //otherwise QS is not required or user already made a compatible conversion - return Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, input); + return input; } } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java b/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java index 18bee0a33c..582071d52c 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxPublish.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,7 +75,7 @@ final class FluxPublish extends ConnectableFlux implements Scannable { if (prefetch <= 0) { throw new IllegalArgumentException("bufferSize > 0 required but it was " + prefetch); } - this.source = Objects.requireNonNull(source, "source"); + this.source = Flux.from(Objects.requireNonNull(source, "source")); this.prefetch = prefetch; this.queueSupplier = Objects.requireNonNull(queueSupplier, "queueSupplier"); this.resetUponSourceTermination = resetUponSourceTermination; diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRefCount.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRefCount.java index d707372ff9..7ccf891909 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRefCount.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRefCount.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ final class FluxRefCount extends Flux implements Scannable, Fuseable { if (n <= 0) { throw new IllegalArgumentException("n > 0 required but it was " + n); } - this.source = Objects.requireNonNull(source, "source"); + this.source = ConnectableFlux.from(Objects.requireNonNull(source, "source")); this.n = n; } @@ -64,6 +64,9 @@ public void subscribe(CoreSubscriber actual) { RefCountMonitor conn; RefCountInner inner = new RefCountInner<>(actual); + // This call assumes that inner.onSubscribe(Subscription) is delivered + // synchronously, because later inner.setRefCountMonitor() triggers the actual + // Subscriber to request. source.subscribe(inner); boolean connect = false; @@ -125,6 +128,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxRefCountGrace.java b/reactor-core/src/main/java/reactor/core/publisher/FluxRefCountGrace.java index eacf1831e7..6ec4c5e7e0 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxRefCountGrace.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxRefCountGrace.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package reactor.core.publisher; import java.time.Duration; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -46,7 +47,7 @@ final class FluxRefCountGrace extends Flux implements Scannable, Fuseable RefConnection connection; FluxRefCountGrace(ConnectableFlux source, int n, Duration gracePeriod, Scheduler scheduler) { - this.source = source; + this.source = ConnectableFlux.from(Objects.requireNonNull(source, "source")); this.n = n; this.gracePeriod = gracePeriod; this.scheduler = scheduler; @@ -64,6 +65,7 @@ public Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) return getPrefetch(); if (key == Attr.PARENT) return source; if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + if (key == InternalProducerAttr.INSTANCE) return true; return null; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxReplay.java b/reactor-core/src/main/java/reactor/core/publisher/FluxReplay.java index d264e7f1cb..746f544696 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxReplay.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxReplay.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1072,7 +1072,7 @@ public int size() { int history, long ttl, @Nullable Scheduler scheduler) { - this.source = Objects.requireNonNull(source, "source"); + this.source = Operators.toFluxOrMono(Objects.requireNonNull(source, "source")); if (source instanceof OptimizableOperator) { @SuppressWarnings("unchecked") OptimizableOperator optimSource = (OptimizableOperator) source; diff --git a/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java b/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java index 4f82af4696..1347c3fa80 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/GroupedLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,8 +79,8 @@ public String stepName() { @Override public void subscribe(CoreSubscriber actual) { - CoreSubscriber input = - liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + CoreSubscriber input = liftFunction.lifter.apply(source, actual); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java index 4eaf1f0618..f30fcc3b2d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/GroupedLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,8 +81,8 @@ public String stepName() { @Override public void subscribe(CoreSubscriber actual) { - CoreSubscriber input = - liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + CoreSubscriber input = liftFunction.lifter.apply(source, actual); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/InternalConnectableFluxOperator.java b/reactor-core/src/main/java/reactor/core/publisher/InternalConnectableFluxOperator.java index 1e8f412c63..ce65f0647e 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/InternalConnectableFluxOperator.java +++ b/reactor-core/src/main/java/reactor/core/publisher/InternalConnectableFluxOperator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,9 @@ public final void subscribe(CoreSubscriber subscriber) { } OptimizableOperator newSource = operator.nextOptimizableSource(); if (newSource == null) { - operator.source().subscribe(subscriber); + CorePublisher operatorSource = operator.source(); + subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(operatorSource, subscriber); + operatorSource.subscribe(subscriber); return; } operator = newSource; diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoLift.java b/reactor-core/src/main/java/reactor/core/publisher/MonoLift.java index 697804dbf4..60923edde4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,8 @@ final class MonoLift extends InternalMonoOperator { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - CoreSubscriber input = - liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + CoreSubscriber input = liftFunction.lifter.apply(source, actual); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/MonoLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/MonoLiftFuseable.java index 93da526312..e937acc628 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/MonoLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/MonoLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,8 @@ public Object scanUnsafe(Attr key) { @Override public CoreSubscriber subscribeOrReturn(CoreSubscriber actual) { - CoreSubscriber input = liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + CoreSubscriber input = liftFunction.lifter.apply(source, actual); Objects.requireNonNull(input, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/main/java/reactor/core/publisher/Operators.java b/reactor-core/src/main/java/reactor/core/publisher/Operators.java index 0763de1185..06e1c3464f 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Operators.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Operators.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1024,7 +1024,7 @@ static CoreSubscriber restoreContextOnSubscriberIfAutoCPEnabled( static CoreSubscriber restoreContextOnSubscriber(Publisher publisher, CoreSubscriber subscriber) { if (publisher instanceof Fuseable) { - return new FluxContextWriteRestoringThreadLocals.FuseableContextWriteRestoringThreadLocalsSubscriber<>( + return new FluxContextWriteRestoringThreadLocalsFuseable.FuseableContextWriteRestoringThreadLocalsSubscriber<>( subscriber, subscriber.currentContext()); } else { return new FluxContextWriteRestoringThreadLocals.ContextWriteRestoringThreadLocalsSubscriber<>( @@ -2701,7 +2701,17 @@ static final LiftFunction liftScannable( } BiFunction, ? extends CoreSubscriber> - effectiveLifter = (pub, sub) -> lifter.apply(Scannable.from(pub), restoreContextOnSubscriberIfAutoCPEnabled(pub, sub)); + effectiveLifter = + // For CP, we wrap the result that the user-provided lifter + // returns, but we also wrap the actual sub if lifting happens on + // top of a custom Publisher so that user's lifter can also see + // have the Context properly restored to ThreadLocal values. + (pub, sub) -> { + CoreSubscriber userLiftedSub = + lifter.apply(Scannable.from(pub), + restoreContextOnSubscriberIfAutoCPEnabled(pub, sub)); + return restoreContextOnSubscriberIfPublisherNonInternal(pub, userLiftedSub); + }; return new LiftFunction<>(effectiveFilter, effectiveLifter, lifter.toString()); } @@ -2710,7 +2720,19 @@ static final LiftFunction liftPublisher( @Nullable Predicate filter, BiFunction, ? extends CoreSubscriber> lifter) { Objects.requireNonNull(lifter, "lifter"); - return new LiftFunction<>(filter, lifter, lifter.toString()); + BiFunction, ? extends CoreSubscriber> + effectiveLifter = + // For CP, we wrap the result that the user-provided lifter + // returns, but we also wrap the actual sub if lifting happens on + // top of a custom Publisher so that user's lifter can also see + // have the Context properly restored to ThreadLocal values. + (pub, sub) -> { + CoreSubscriber userLiftedSub = + lifter.apply(pub, + restoreContextOnSubscriberIfAutoCPEnabled(pub, sub)); + return restoreContextOnSubscriberIfPublisherNonInternal(pub, userLiftedSub); + }; + return new LiftFunction<>(filter, effectiveLifter, lifter.toString()); } private LiftFunction(@Nullable Predicate filter, diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java index 29375a0535..a47a16f32d 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelLift.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,8 +84,8 @@ public void subscribe(CoreSubscriber[] s) { int i = 0; while (i < subscribers.length) { subscribers[i] = - Objects.requireNonNull(liftFunction.lifter.apply(source, - Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, s[i])), + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + Objects.requireNonNull(liftFunction.lifter.apply(source, s[i]), "Lifted subscriber MUST NOT be null"); i++; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java b/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java index c345fbb93d..bf77b2e2ae 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java +++ b/reactor-core/src/main/java/reactor/core/publisher/ParallelLiftFuseable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,8 +88,8 @@ public void subscribe(CoreSubscriber[] s) { while (i < subscribers.length) { CoreSubscriber actual = s[i]; CoreSubscriber converted = - Objects.requireNonNull(liftFunction.lifter.apply(source, Operators.restoreContextOnSubscriberIfAutoCPEnabled(source, actual)), - "Lifted subscriber MUST NOT be null"); + // No need to wrap actual for CP, the Operators$LiftFunction handles it. + Objects.requireNonNull(liftFunction.lifter.apply(source, actual), "Lifted subscriber MUST NOT be null"); Objects.requireNonNull(converted, "Lifted subscriber MUST NOT be null"); diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java index 61e45b7057..27c5927d91 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2023-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,6 @@ import java.util.stream.Stream; import io.micrometer.context.ContextRegistry; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -50,10 +49,9 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import reactor.core.CorePublisher; import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.scheduler.Schedulers; import reactor.test.publisher.TestPublisher; @@ -340,6 +338,10 @@ private ThreadSwitchingMono threadSwitchingMono() { return new ThreadSwitchingMono<>("Hello", executorService); } + private ThreadSwitchingConnectableFlux threadSwitchingConnectableFlux() { + return new ThreadSwitchingConnectableFlux<>("Hello", executorService); + } + void assertThreadLocalsPresentInFlux(Supplier> chainSupplier) { assertThreadLocalsPresentInFlux(chainSupplier, false); } @@ -391,7 +393,7 @@ else if (signal.isOnComplete()) { .contextWrite(Context.of(KEY, "present")) .blockLast(Duration.ofMillis(5000)); } catch (Exception e) { - if (e instanceof IllegalStateException) { + if (!(e instanceof ExpectedException || e.getCause() instanceof TimeoutException)) { throw e; } assertThat(e).satisfiesAnyOf( @@ -431,7 +433,7 @@ else if (signal.isOnComplete()) { } }) .contextWrite(Context.of(KEY, "present")) - .onErrorComplete() + .onErrorComplete(e -> e instanceof ExpectedException || e instanceof TimeoutException) .block(); if (hadNext.get()) { @@ -459,7 +461,7 @@ void assertThatThreadLocalsPresentDirectCoreSubscribe( executorService.submit(asyncAction) .get(100, TimeUnit.MILLISECONDS); - if (!subscriberWithContext.latch.await(500, TimeUnit.MILLISECONDS)) { + if (!subscriberWithContext.latch.await(2, TimeUnit.SECONDS)) { throw new TimeoutException("timed out"); } @@ -472,6 +474,7 @@ void assertThatThreadLocalsPresentDirectCoreSubscribe( assertThat(subscriberWithContext.complete).isTrue(); } else { + assertThat(subscriberWithContext.error.get()).isInstanceOfAny(ExpectedException.class, TimeoutException.class); assertThat(subscriberWithContext.valueInOnError.get()).isEqualTo("present"); } }); @@ -496,7 +499,7 @@ void assertThatThreadLocalsPresentDirectRawSubscribe( executorService.submit(asyncAction) .get(100, TimeUnit.MILLISECONDS); - if (!subscriberWithContext.latch.await(500, TimeUnit.MILLISECONDS)) { + if (!subscriberWithContext.latch.await(2, TimeUnit.SECONDS)) { throw new TimeoutException("timed out"); } @@ -508,6 +511,7 @@ void assertThatThreadLocalsPresentDirectRawSubscribe( assertThat(subscriberWithContext.complete).isTrue(); } else { + assertThat(subscriberWithContext.error.get()).isInstanceOfAny(ExpectedException.class, TimeoutException.class); assertThat(subscriberWithContext.valueInOnError.get()).isEqualTo("present"); } }); @@ -660,7 +664,7 @@ void fluxRetryWhen() { @Test void fluxRetryWhenSwitchingThread() { assertThreadLocalsPresentInFlux(() -> - Flux.error(new RuntimeException("Oops")) + Flux.error(new ExpectedException("Oops")) .retryWhen(Retry.from(f -> threadSwitchingFlux()))); } @@ -971,6 +975,20 @@ public void onComplete() { }); } + // see https://github.com/reactor/reactor-core/issues/3762 + @Test + void fluxLiftOnEveryOperator() { + Function, ? extends Publisher> + everyOperatorLift = Operators.lift((a, b) -> b); + + Hooks.onEachOperator("testEveryOperatorLift", everyOperatorLift); + + assertThreadLocalsPresentInFlux(() -> Flux.just("Hello").hide() + .publish().refCount().map(s -> s)); + + Hooks.resetOnEachOperator(); + } + @Test void fluxFlatMapSequential() { assertThreadLocalsPresentInFlux(() -> @@ -981,7 +999,7 @@ void fluxFlatMapSequential() { @Test void fluxOnErrorResume() { assertThreadLocalsPresentInFlux(() -> - Flux.error(new RuntimeException("Oops")) + Flux.error(new ExpectedException("Oops")) .onErrorResume(t -> threadSwitchingFlux())); } @@ -1014,7 +1032,7 @@ void fluxSampleFirst() { // missing along the way in the chain. assertThreadLocalsPresent( Flux.just("Hello").concatWith(Flux.never()) - .sampleFirst(s -> new ThreadSwitchingFlux<>(new RuntimeException("oops"), executorService))); + .sampleFirst(s -> new ThreadSwitchingFlux<>(new ExpectedException("oops"), executorService))); } @Test @@ -1220,7 +1238,7 @@ void monoRetryWhen() { @Test void monoRetryWhenSwitchingThread() { assertThreadLocalsPresentInMono(() -> - Mono.error(new RuntimeException("Oops")) + Mono.error(new ExpectedException("Oops")) .retryWhen(Retry.from(f -> threadSwitchingMono()))); } @@ -1378,7 +1396,7 @@ public void onComplete() { @Test void monoOnErrorResume() { assertThreadLocalsPresentInMono(() -> - Mono.error(new RuntimeException("oops")) + Mono.error(new ExpectedException("oops")) .onErrorResume(e -> threadSwitchingMono())); } @@ -1595,6 +1613,63 @@ void threadSwitchingParallelFluxSort() { .sorted(Comparator.naturalOrder())); } + // ConnectableFlux tests + + @Test + void threadSwitchingAutoConnect() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingConnectableFlux().autoConnect()); + } + + @Test + void threadSwitchingRefCount() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingConnectableFlux().refCount()); + } + + @Test + void threadSwitchingRefCountGrace() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingConnectableFlux().refCount(1, Duration.ofMillis(100))); + } + + @Test + void threadSwitchingPublishAutoConnect() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingFlux().publish().autoConnect()); + } + + @Test + void threadSwitchingPublishRefCount() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingFlux().publish().refCount()); + } + + @Test + void threadSwitchingPublishRefCountGrace() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingFlux().publish().refCount(1, Duration.ofMillis(100))); + } + + @Test + void threadSwitchingMonoPublish() { + assertThreadLocalsPresentInMono(() -> threadSwitchingMono().publish(Function.identity())); + } + + @Test + void threadSwitchingMonoPublishSwitchingThread() { + assertThreadLocalsPresentInMono(() -> threadSwitchingMono().publish(m -> threadSwitchingMono())); + } + + @Test + void threadSwitchingReplayAutoConnect() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingFlux().replay(1).autoConnect()); + } + + @Test + void threadSwitchingReplayRefCount() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingFlux().replay(1).refCount()); + } + + @Test + void threadSwitchingReplayRefCountGrace() { + assertThreadLocalsPresentInFlux(() -> threadSwitchingFlux().replay(1).refCount(1, Duration.ofMillis(100))); + } + // Sinks tests @Test @@ -1875,6 +1950,13 @@ void printInterestingClasses() throws Exception { } } + private class ExpectedException extends RuntimeException { + + public ExpectedException(String message) { + super(message); + } + + } private class CoreSubscriberWithContext implements CoreSubscriber { final AtomicReference valueInOnNext; diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingConnectableFlux.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingConnectableFlux.java new file mode 100644 index 0000000000..db8e6a2a16 --- /dev/null +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/ThreadSwitchingConnectableFlux.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; + +public class ThreadSwitchingConnectableFlux extends ConnectableFlux + implements Subscription { + + private final ExecutorService executorService; + private final T item; + private final Throwable error; + private CoreSubscriber actual; + AtomicBoolean done = new AtomicBoolean(); + + public ThreadSwitchingConnectableFlux(T item, ExecutorService executorService) { + this.executorService = executorService; + this.item = item; + this.error = null; + } + + @Override + public void connect(Consumer cancelSupport) { + // Assuming just one Subscriber + if (!done.get()) { + this.executorService.submit(this::deliver); + } + } + + @Override + public void subscribe(CoreSubscriber actual) { + this.actual = actual; + this.actual.onSubscribe(this); + } + + private void deliver() { + if (done.compareAndSet(false, true)) { + if (this.item != null) { + this.actual.onNext(this.item); + } + if (this.error != null) { + this.actual.onError(this.error); + } + this.executorService.submit(this.actual::onComplete); + } + } + + @Override + public void request(long n) { + // ignore, assume there's always request + Operators.validate(n); + } + + @Override + public void cancel() { + done.set(true); + } +} From 1bc046f8754bc204eb247d204d3d956385076819 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 14 May 2024 10:00:26 +0200 Subject: [PATCH 285/312] [release] Prepare and release 3.5.17 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e6a20f38bb..42ce0472e0 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.16" - testCompile "io.projectreactor:reactor-test:3.5.16" + compile "io.projectreactor:reactor-core:3.5.17" + testCompile "io.projectreactor:reactor-test:3.5.17" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.17-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.17-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.18-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.18-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.16" + // implementation "io.projectreactor:reactor-tools:3.5.17" } ``` diff --git a/gradle.properties b/gradle.properties index b8a0624e7e..bc9f949433 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.17-SNAPSHOT -bomVersion=2022.0.18 -metricsMicrometerVersion=1.0.17-SNAPSHOT +version=3.5.17 +bomVersion=2022.0.19 +metricsMicrometerVersion=1.0.17 org.gradle.parallel=true From 69871e9aabae11f5bea8c4cd63753088dc231d62 Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 14 May 2024 10:54:04 +0200 Subject: [PATCH 286/312] [release] Prepare and release 3.6.6 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 4 ++-- settings.gradle | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 65d4b9c840..dec49b5c4b 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.5" - testCompile "io.projectreactor:reactor-test:3.6.5" + compile "io.projectreactor:reactor-core:3.6.6" + testCompile "io.projectreactor:reactor-test:3.6.6" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.6-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.6-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.7-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.7-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.5" + // implementation "io.projectreactor:reactor-tools:3.6.6" } ``` diff --git a/gradle.properties b/gradle.properties index 1f09f7edb4..08c4414d11 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.6-SNAPSHOT -bomVersion=2023.0.5 -metricsMicrometerVersion=1.1.6-SNAPSHOT +version=3.6.6 +bomVersion=2023.0.6 +metricsMicrometerVersion=1.1.6 org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 42779a6489..f18f3327aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,9 +14,9 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "4.0.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.5" +micrometer = "1.12.6" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.5" +micrometerTracingTest="1.2.6" contextPropagation="1.1.1" kotlin = "1.8.22" reactiveStreams = "1.0.4" diff --git a/settings.gradle b/settings.gradle index e9650f3a9d..d0bcfb0273 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,9 +25,9 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.12.6-SNAPSHOT') + version('micrometer', '1.12.7-SNAPSHOT') version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") - version('micrometerTracingTest', "1.2.6-SNAPSHOT") + version('micrometerTracingTest', "1.2.7-SNAPSHOT") version('contextPropagation', "1.1.2-SNAPSHOT") } } From e3ee99f23633a01925fc3c25ac289803cb3756be Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 14 May 2024 11:19:38 +0200 Subject: [PATCH 287/312] [release] Next development version 3.5.18-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index bc9f949433..9a29e23f2f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.17 +version=3.5.18-SNAPSHOT bomVersion=2022.0.19 -metricsMicrometerVersion=1.0.17 +metricsMicrometerVersion=1.0.18-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1c96f1e8f..c6272eb281 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.16" -baselinePerfCore = "3.5.16" +baseline-core-api = "3.5.17" +baselinePerfCore = "3.5.17" baselinePerfExtra = "3.5.1" # Other shared versions From fc57389a3b9fb695a892b69fe3ce802555e01bab Mon Sep 17 00:00:00 2001 From: Pierre De Rop Date: Tue, 14 May 2024 12:28:22 +0200 Subject: [PATCH 288/312] [release] Next development version 3.6.7-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 08c4414d11..5e0ff41d0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.6 +version=3.6.7-SNAPSHOT bomVersion=2023.0.6 -metricsMicrometerVersion=1.1.6 +metricsMicrometerVersion=1.1.7-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f18f3327aa..34a68b3b44 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.5" -baselinePerfCore = "3.6.5" +baseline-core-api = "3.6.6" +baselinePerfCore = "3.6.6" baselinePerfExtra = "3.5.1" # Other shared versions From 0814abc13b9a35dc78422a6c21dc2fb16c2557e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 5 Jun 2024 14:58:50 +0200 Subject: [PATCH 289/312] Rework BufferTimeout with fair backpressure (#3634) The fair backpressure variant of the bufferTimeout operator has been reworked to use a state machine with a minimum number of volatile variables eliminating potential data races, such as skipping the delivery when onNext and timeout happen concurrently or cancellation happens while onNext is delivered, etc. The changes include an improved JCstress suite to cover discarded values and more racing scenarios. A serial JMH benchmark can also be exercised. Also, the implementation contains comments to aid in future understanding of the algorithm and a set of utility methods which can form a basis for a generic state machine utility toolset. --- .../publisher/FluxBufferTimeoutBenchmark.java | 116 +++ .../FluxBufferTimeoutStressTest.java | 202 +++-- .../core/publisher/FluxBufferTimeout.java | 793 +++++++++++------- .../reactor/core/publisher/StateLogger.java | 4 +- ...FluxBufferTimeoutFairBackpressureTest.java | 11 +- .../publisher/OnDiscardShouldNotLeakTest.java | 3 +- 6 files changed, 760 insertions(+), 369 deletions(-) create mode 100644 benchmarks/src/main/java/reactor/core/publisher/FluxBufferTimeoutBenchmark.java diff --git a/benchmarks/src/main/java/reactor/core/publisher/FluxBufferTimeoutBenchmark.java b/benchmarks/src/main/java/reactor/core/publisher/FluxBufferTimeoutBenchmark.java new file mode 100644 index 0000000000..9da6292ce9 --- /dev/null +++ b/benchmarks/src/main/java/reactor/core/publisher/FluxBufferTimeoutBenchmark.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2024 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.core.publisher; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +@BenchmarkMode({Mode.AverageTime}) +@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class FluxBufferTimeoutBenchmark { + + private static final int TOTAL_VALUES = 100; + + @Param({"1", "10", "100"}) + int bufferSize; + + @Benchmark + public Object unlimited(Blackhole blackhole) throws InterruptedException { + JmhSubscriber subscriber = new JmhSubscriber(blackhole, false); + Flux.range(0, TOTAL_VALUES) + .bufferTimeout(bufferSize, Duration.ofDays(100), true) + .subscribe(subscriber); + subscriber.await(); + return subscriber; + } + + @Benchmark + public Object oneByOne(Blackhole blackhole) throws InterruptedException { + JmhSubscriber subscriber = new JmhSubscriber(blackhole, true); + Flux.range(0, TOTAL_VALUES) + .bufferTimeout(bufferSize, Duration.ofDays(100), true) + .subscribe(subscriber); + subscriber.await(); + return subscriber; + } + + + public static class JmhSubscriber extends CountDownLatch + implements CoreSubscriber> { + + private final Blackhole blackhole; + private final boolean oneByOneRequest; + private Subscription s; + + public JmhSubscriber(Blackhole blackhole, boolean oneByOneRequest) { + super(1); + this.blackhole = blackhole; + this.oneByOneRequest = oneByOneRequest; + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + if (oneByOneRequest) { + s.request(1); + } else { + s.request(Long.MAX_VALUE); + } + } + } + + @Override + public void onNext(List t) { + blackhole.consume(t); + if (oneByOneRequest) { + s.request(1); + } + } + + @Override + public void onError(Throwable t) { + blackhole.consume(t); + countDown(); + } + + @Override + public void onComplete() { + countDown(); + } + } +} diff --git a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxBufferTimeoutStressTest.java b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxBufferTimeoutStressTest.java index d19e1122e4..adf6e2ccb6 100644 --- a/reactor-core/src/jcstress/java/reactor/core/publisher/FluxBufferTimeoutStressTest.java +++ b/reactor-core/src/jcstress/java/reactor/core/publisher/FluxBufferTimeoutStressTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2023-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,12 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.openjdk.jcstress.annotations.Actor; import org.openjdk.jcstress.annotations.Arbiter; @@ -30,23 +32,28 @@ import org.openjdk.jcstress.annotations.Outcome; import org.openjdk.jcstress.annotations.State; import org.openjdk.jcstress.infra.results.LLL_Result; +import org.openjdk.jcstress.infra.results.LL_Result; import reactor.core.util.FastLogger; import reactor.test.scheduler.VirtualTimeScheduler; +import static java.util.Collections.emptyList; + public class FluxBufferTimeoutStressTest { @JCStressTest - @Outcome(id = "1, 1, 1", expect = Expect.ACCEPTABLE, desc = "") - @Outcome(id = "2, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "2, 1", expect = Expect.ACCEPTABLE, desc = "") @State public static class FluxBufferTimeoutStressTestRaceDeliveryAndTimeout { + final FastLogger fastLogger = new FastLogger(getClass().getName()); + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); final StressSubscriber> subscriber = new StressSubscriber<>(); final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = - new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), null); + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); final StressSubscription subscription = new StressSubscription<>(bufferTimeoutSubscriber); @@ -67,35 +74,42 @@ public void timeout() { } @Arbiter - public void arbiter(LLL_Result result) { + public void arbiter(LL_Result result) { result.r1 = subscriber.onNextCalls.get(); - result.r2 = subscriber.onCompleteCalls.get(); - result.r3 = subscription.requestsCount.get(); + result.r2 = subscription.requestsCount.get(); - if (subscriber.onCompleteCalls.get() > 1) { - throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + if (subscriber.onCompleteCalls.get() != 1) { + fail(fastLogger, + "unexpected completion count " + subscriber.onCompleteCalls.get()); } if (subscriber.concurrentOnComplete.get()) { - throw new IllegalStateException("subscriber concurrent onComplete"); + fail(fastLogger, "subscriber concurrent onComplete"); } if (subscriber.concurrentOnNext.get()) { - throw new IllegalStateException("subscriber concurrent onNext"); + fail(fastLogger, "subscriber concurrent onNext"); + } + if (!subscriber.discardedValues.isEmpty()) { + fail(fastLogger, "Unexpected discarded values " + subscriber.discardedValues); } } } @JCStressTest - @Outcome(id = "3, 1, 1", expect = Expect.ACCEPTABLE, desc = "") - @Outcome(id = "4, 1, 1", expect = Expect.ACCEPTABLE, desc = "") - @Outcome(id = "5, 1, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "3, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "4, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 1", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "3, 2", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "4, 2", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 2", expect = Expect.ACCEPTABLE, desc = "") @State public static class FluxBufferTimeoutStressTestRaceDeliveryAndMoreTimeouts { + final FastLogger fastLogger = new FastLogger(getClass().getName()); + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); final StressSubscriber> subscriber = new StressSubscriber<>(); - final FastLogger fastLogger = new FastLogger(getClass().getName()); final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); @@ -126,22 +140,25 @@ public void timeout() { } @Arbiter - public void arbiter(LLL_Result result) { + public void arbiter(LL_Result result) { result.r1 = subscriber.onNextCalls.get(); - result.r2 = subscriber.onCompleteCalls.get(); - result.r3 = subscription.requestsCount.get(); + result.r2 = subscription.requestsCount.get(); if (subscriber.onCompleteCalls.get() != 1) { - throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + fail(fastLogger, "unexpected completion: " + subscriber.onCompleteCalls.get()); } if (subscriber.concurrentOnComplete.get()) { - throw new IllegalStateException("subscriber concurrent onComplete"); + fail(fastLogger, "subscriber concurrent onComplete"); } if (subscriber.concurrentOnNext.get()) { - throw new IllegalStateException("subscriber concurrent onNext"); + fail(fastLogger, "subscriber concurrent onNext"); } - if (subscriber.receivedValues.stream().anyMatch(List::isEmpty)) { - throw new IllegalStateException("received an empty buffer: " + subscriber.receivedValues + "; result=" + result + "\n" + fastLogger); + if (!subscriber.discardedValues.isEmpty()) { + fail(fastLogger, "Unexpected discarded values " + subscriber.discardedValues); + } + if (!allValuesHandled(fastLogger, 5, emptyList(), + subscriber.receivedValues)) { + fail(fastLogger, "not all values delivered; result=" + result); } } } @@ -155,19 +172,23 @@ public void arbiter(LLL_Result result) { @Outcome(id = "5, 0, 3", expect = Expect.ACCEPTABLE, desc = "") @Outcome(id = "5, 0, 4", expect = Expect.ACCEPTABLE, desc = "") @Outcome(id = "5, 0, 5", expect = Expect.ACCEPTABLE, desc = "") + @Outcome(id = "5, 0, 1", expect = Expect.ACCEPTABLE, desc = "") @State public static class FluxBufferTimeoutStressTestRaceDeliveryAndMoreTimeoutsPossiblyIncomplete { + final FastLogger fastLogger = new FastLogger(getClass().getName()); + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); final StressSubscriber> subscriber = new StressSubscriber<>(1); - final FastLogger fastLogger = new FastLogger(getClass().getName()); final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); Sinks.Many proxy = Sinks.unsafe().many().unicast().onBackpressureBuffer(); + final AtomicLong requested = new AtomicLong(); + { proxy.asFlux() .doOnRequest(r -> requested.incrementAndGet()) @@ -213,29 +234,33 @@ public void arbiter(LLL_Result result) { result.r2 = subscriber.onCompleteCalls.get(); result.r3 = requested.get(); + if (!allValuesHandled(fastLogger, 4, emptyList(), subscriber.receivedValues)) { + fail(fastLogger, "minimum set of values not delivered"); + } + if (subscriber.onCompleteCalls.get() == 0) { - if (subscriber.receivedValues.stream() - .noneMatch(buf -> buf.size() == 1)) { - throw new IllegalStateException("incomplete but received all two " + - "element buffers. received: " + subscriber.receivedValues + "; result=" + result + "\n" + fastLogger); + if (subscriber.receivedValues.size() == 5 && + subscriber.receivedValues.stream().noneMatch(buf -> buf.size() == 1)) { + fail(fastLogger, "incomplete but delivered all two " + + "element buffers. received: " + subscriber.receivedValues + "; result=" + result); } } - if (subscriber.onNextCalls.get() < 5 && subscriber.onCompleteCalls.get() == 0) { - throw new IllegalStateException("incomplete. received: " + subscriber.receivedValues + "; requested=" + requested.get() + "; result=" + result + "\n" + fastLogger); + if (subscriber.onNextCalls.get() < 5) { + fail(fastLogger, "incomplete. received: " + subscriber.receivedValues + "; requested=" + requested.get() + "; result=" + result); } if (subscriber.onCompleteCalls.get() > 1) { - throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + fail(fastLogger, "unexpected completion " + subscriber.onCompleteCalls.get()); } if (subscriber.concurrentOnComplete.get()) { - throw new IllegalStateException("subscriber concurrent onComplete"); + fail(fastLogger, "subscriber concurrent onComplete"); } if (subscriber.concurrentOnNext.get()) { - throw new IllegalStateException("subscriber concurrent onNext"); + fail(fastLogger, "subscriber concurrent onNext"); } if (subscriber.receivedValues.stream().anyMatch(List::isEmpty)) { - throw new IllegalStateException("received an empty buffer: " + subscriber.receivedValues + "; result=" + result + "\n" + fastLogger); + fail(fastLogger, "received an empty buffer: " + subscriber.receivedValues + "; result=" + result); } } } @@ -247,12 +272,14 @@ public void arbiter(LLL_Result result) { @State public static class FluxBufferTimeoutStressTestRaceDeliveryAndCancel { + final FastLogger fastLogger = new FastLogger(getClass().getName()); + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); final StressSubscriber> subscriber = new StressSubscriber<>(); final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = - new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), null); + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); final StressSubscription subscription = new StressSubscription<>(bufferTimeoutSubscriber); @@ -279,13 +306,16 @@ public void arbiter(LLL_Result result) { result.r3 = subscription.requestsCount.get(); if (subscriber.onCompleteCalls.get() > 1) { - throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + fail(fastLogger, "unexpected completion " + subscriber.onCompleteCalls.get()); } if (subscriber.concurrentOnComplete.get()) { - throw new IllegalStateException("subscriber concurrent onComplete"); + fail(fastLogger, "subscriber concurrent onComplete"); } if (subscriber.concurrentOnNext.get()) { - throw new IllegalStateException("subscriber concurrent onNext"); + fail(fastLogger, "subscriber concurrent onNext"); + } + if (!allValuesHandled(fastLogger, 2, subscriber.discardedValues, subscriber.receivedValues)) { + fail(fastLogger, "Not all handled!" + "; result=" + result); } } } @@ -310,6 +340,7 @@ public static class FluxBufferTimeoutStressTestRaceDeliveryAndCancelWithBackpres new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); Sinks.Many proxy = Sinks.unsafe().many().unicast().onBackpressureBuffer(); + AtomicLong emits = new AtomicLong(); final AtomicLong requested = new AtomicLong(); { proxy.asFlux() @@ -319,11 +350,12 @@ public static class FluxBufferTimeoutStressTestRaceDeliveryAndCancelWithBackpres @Actor public void next() { - proxy.tryEmitNext(0L); - proxy.tryEmitNext(1L); - - proxy.tryEmitNext(2L); - proxy.tryEmitNext(3L); + for (long i = 0; i < 4; i++) { + if (proxy.tryEmitNext(i) != Sinks.EmitResult.OK) { + return; + } + emits.set(i + 1); + } proxy.tryEmitComplete(); } @@ -344,16 +376,26 @@ public void arbiter(LLL_Result result) { result.r3 = requested.get(); if (subscriber.onCompleteCalls.get() > 1) { - throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + fail(fastLogger, "unexpected completion " + subscriber.onCompleteCalls.get()); } if (subscriber.concurrentOnComplete.get()) { - throw new IllegalStateException("subscriber concurrent onComplete"); + fail(fastLogger, "subscriber concurrent onComplete"); } if (subscriber.concurrentOnNext.get()) { - throw new IllegalStateException("subscriber concurrent onNext"); + fail(fastLogger, "subscriber concurrent onNext"); } - if (subscriber.receivedValues.stream().anyMatch(List::isEmpty)) { - throw new IllegalStateException("received an empty buffer: " + subscriber.receivedValues + "; result=" + result + "\n" + fastLogger); + int emits = (int) this.emits.get(); + if (subscriber.onCompleteCalls.get() == 1 && !allValuesHandled(fastLogger, 4, + emptyList(), + subscriber.receivedValues)) { + fail(fastLogger, + "Completed but not all values handled!" + "; result=" + result); + } + + if (subscriber.onNextCalls.get() > 0 && !allValuesHandled(fastLogger, emits, + subscriber.discardedValues, + subscriber.receivedValues)) { + fail(fastLogger, "Not all " + emits + " emits handled!" + "; result=" + result); } } } @@ -367,12 +409,14 @@ public void arbiter(LLL_Result result) { @State public static class FluxBufferTimeoutStressTestRaceDeliveryAndCancelAndTimeout { + final FastLogger fastLogger = new FastLogger(getClass().getName()); + final VirtualTimeScheduler virtualTimeScheduler = VirtualTimeScheduler.create(); final StressSubscriber> subscriber = new StressSubscriber<>(); final FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> bufferTimeoutSubscriber = - new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), null); + new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber<>(subscriber, 2, 1, TimeUnit.SECONDS, virtualTimeScheduler.createWorker(), bufferSupplier(), fastLogger); final StressSubscription subscription = new StressSubscription<>(bufferTimeoutSubscriber); @@ -404,17 +448,71 @@ public void arbiter(LLL_Result result) { result.r3 = subscription.requestsCount.get(); if (subscriber.onCompleteCalls.get() > 1) { - throw new IllegalStateException("unexpected completion " + subscriber.onCompleteCalls.get()); + fail(fastLogger, + "unexpected completion " + subscriber.onCompleteCalls.get()); } if (subscriber.concurrentOnComplete.get()) { - throw new IllegalStateException("subscriber concurrent onComplete"); + fail(fastLogger, "subscriber concurrent onComplete"); } if (subscriber.concurrentOnNext.get()) { - throw new IllegalStateException("subscriber concurrent onNext"); + fail(fastLogger, "subscriber concurrent onNext"); + } + + if (!allValuesHandled(fastLogger, 2, subscriber.discardedValues, subscriber.receivedValues)) { + fail(fastLogger, "Not all handled!" + "; result=" + result); } } } + private static void fail(FastLogger fastLogger, String msg) { + throw new IllegalStateException(msg + "\n" + fastLogger); + } + + private static boolean allValuesHandled(FastLogger logger, int range, List discarded, List>... delivered) { + if (delivered.length == 0) { + return false; + } + + List discardedValues = discarded.stream() + .map(o -> (Long) o) + .collect(Collectors.toList()); + + logger.trace("discarded: " + discardedValues); + logger.trace("delivered: " + Arrays.toString(delivered)); + + boolean[] search = new boolean[range]; + for (long l : discardedValues) { + search[(int) l] = true; + } + + List> all = + Arrays.stream(delivered) + .flatMap(lists -> lists.stream()) + .collect(Collectors.toList()); + + for (List buf : all) { + if (buf.isEmpty()) { + fail(logger, "Received empty buffer!"); + } + for (long l : buf) { + if (l >= range) { + // just check within the range + continue; + } + if (search[(int) l]) { + fail(logger, "Duplicate value (both discarded " + + "and delivered, or duplicated in multiple buffers)"); + } + search[(int) l] = true; + } + } + for (boolean b : search) { + if (!b) { + return false; + } + } + return true; + } private static Supplier> bufferSupplier() { return ArrayList::new; } diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java index dcc6f7228f..1fa01762a4 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxBufferTimeout.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,12 +23,14 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.function.Function; import java.util.function.Supplier; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; +import reactor.core.Disposables; import reactor.core.Exceptions; import reactor.core.scheduler.Scheduler; import reactor.util.Logger; @@ -47,6 +49,7 @@ final class FluxBufferTimeout> extends Intern final long timespan; final TimeUnit unit; final boolean fairBackpressure; + final Logger logger; FluxBufferTimeout(Flux source, int maxSize, @@ -68,6 +71,32 @@ final class FluxBufferTimeout> extends Intern this.batchSize = maxSize; this.bufferSupplier = Objects.requireNonNull(bufferSupplier, "bufferSupplier"); this.fairBackpressure = fairBackpressure; + this.logger = null; + } + + // for testing + FluxBufferTimeout(Flux source, + int maxSize, + long timespan, + TimeUnit unit, + Scheduler timer, + Supplier bufferSupplier, + boolean fairBackpressure, + Logger logger) { + super(source); + if (timespan <= 0) { + throw new IllegalArgumentException("Timeout period must be strictly positive"); + } + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize must be strictly positive"); + } + this.timer = Objects.requireNonNull(timer, "Timer"); + this.timespan = timespan; + this.unit = Objects.requireNonNull(unit, "unit"); + this.batchSize = maxSize; + this.bufferSupplier = Objects.requireNonNull(bufferSupplier, "bufferSupplier"); + this.fairBackpressure = fairBackpressure; + this.logger = logger; } @Override @@ -102,62 +131,69 @@ public Object scanUnsafe(Attr key) { final static class BufferTimeoutWithBackpressureSubscriber> implements InnerOperator { - @Nullable - final Logger logger; - final CoreSubscriber actual; - final int batchSize; - final int prefetch; - final long timeSpan; - final TimeUnit unit; - final Scheduler.Worker timer; - final Supplier bufferSupplier; + private final @Nullable Logger logger; + private final @Nullable StateLogger stateLogger; + private final CoreSubscriber actual; + private final int batchSize; + private final int prefetch; + private final int replenishMark; + private final long timeSpan; + private final TimeUnit unit; + private final Scheduler.Worker timer; + private final Supplier bufferSupplier; + private final Disposable.Swap currentTimeoutTask = Disposables.swap(); + private final Queue queue; + + private @Nullable Subscription subscription; + + private @Nullable Throwable error; + /** + * Flag used to mark that the operator is definitely done processing all state + * transitions. + */ + private boolean done; + /** + * Access to outstanding is always guarded by volatile access to state, so it + * needn't be volatile. It is also only ever accessed in the drain method, so + * it needn't be part of state either. + */ + private int outstanding; - // tracks unsatisfied downstream demand (expressed in # of buffers) + /** + * Tracks unsatisfied downstream demand (expressed in # of buffers). Package + * visibility for testing purposes. + */ volatile long requested; @SuppressWarnings("rawtypes") - private AtomicLongFieldUpdater REQUESTED = + private static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "requested"); - // tracks undelivered values in the current buffer - volatile int index; - @SuppressWarnings("rawtypes") - private AtomicIntegerFieldUpdater INDEX = - AtomicIntegerFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "index"); - - // tracks # of values requested from upstream but not delivered yet via this - // .onNext(v) - volatile long outstanding; - @SuppressWarnings("rawtypes") - private AtomicLongFieldUpdater OUTSTANDING = - AtomicLongFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "outstanding"); - - // indicates some thread is draining - volatile int wip; - @SuppressWarnings("rawtypes") - private AtomicIntegerFieldUpdater WIP = - AtomicIntegerFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "wip"); - - private volatile int terminated = NOT_TERMINATED; + /** + * The state field serves as the coordination point for multiple actors. The + * surrounding implementation works as a state machine that provides mutual + * exclusion with lock-free semantics. It uses bit masks to divide a 64-bit + * long value for multiple concerns while maintaining atomicity with CAS operations. + */ + private volatile long state; @SuppressWarnings("rawtypes") - private AtomicIntegerFieldUpdater TERMINATED = - AtomicIntegerFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "terminated"); - - final static int NOT_TERMINATED = 0; - final static int TERMINATED_WITH_SUCCESS = 1; - final static int TERMINATED_WITH_ERROR = 2; - final static int TERMINATED_WITH_CANCEL = 3; - - @Nullable - private Subscription subscription; - - private Queue queue; - - @Nullable - Throwable error; - - boolean completed; - - Disposable currentTimeoutTask; + private static final AtomicLongFieldUpdater STATE = + AtomicLongFieldUpdater.newUpdater(BufferTimeoutWithBackpressureSubscriber.class, "state"); + + static final long CANCELLED_FLAG = + 0b1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long TERMINATED_FLAG = + 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long HAS_WORK_IN_PROGRESS_FLAG = + 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long TIMEOUT_FLAG = + 0b0001_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long REQUESTED_INDEX_MASK = + 0b0000_1111_1111_1111_1111_1111_1111_1111_0000_0000_0000_0000_0000_0000_0000_0000L; + static final long INDEX_MASK = + 0b0000_0000_0000_0000_0000_0000_0000_0000_1111_1111_1111_1111_1111_1111_1111_1111L; + + private static final int INDEX_SHIFT = 0; + private static final int REQUESTED_INDEX_SHIFT = 32; public BufferTimeoutWithBackpressureSubscriber( CoreSubscriber actual, @@ -174,11 +210,13 @@ public BufferTimeoutWithBackpressureSubscriber( this.timer = timer; this.bufferSupplier = bufferSupplier; this.logger = logger; + this.stateLogger = logger != null ? new StateLogger(logger) : null; this.prefetch = batchSize << 2; + this.replenishMark = batchSize << 1; this.queue = Queues.get(prefetch).get(); } - private void trace(Logger logger, String msg) { + private static void trace(Logger logger, String msg) { logger.trace(String.format("[%s][%s]", Thread.currentThread().getId(), msg)); } @@ -191,333 +229,303 @@ public void onSubscribe(Subscription s) { } @Override - public void onNext(T t) { + public CoreSubscriber actual() { + return this.actual; + } + + @Override + public void request(long n) { if (logger != null) { - trace(logger, "onNext: " + t); - } - // check if terminated (cancelled / error / completed) -> discard value if so - - // increment index - // append to buffer - // drain - - if (terminated == NOT_TERMINATED) { - // assume no more deliveries than requested - if (!queue.offer(t)) { - Context ctx = currentContext(); - Throwable error = Operators.onOperatorError(this.subscription, - Exceptions.failWithOverflow(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL), - t, actual.currentContext()); - this.error = error; - if (!TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_ERROR)) { - Operators.onErrorDropped(error, ctx); - return; - } - Operators.onDiscard(t, ctx); - drain(); + trace(logger, "request " + n); + } + if (Operators.validate(n)) { + long previouslyRequested = Operators.addCap(REQUESTED, this, n); + if (previouslyRequested == Long.MAX_VALUE) { return; } - boolean shouldDrain = false; - for (;;) { - int index = this.index; - if (INDEX.compareAndSet(this, index, index + 1)) { - if (index == 0) { - try { - if (logger != null) { - trace(logger, "timerStart"); - } - currentTimeoutTask = timer.schedule(this::bufferTimedOut, - timeSpan, - unit); - } catch (RejectedExecutionException ree) { - if (logger != null) { - trace(logger, "Timer rejected for " + t); - } - Context ctx = actual.currentContext(); - Throwable error = Operators.onRejectedExecution(ree, subscription, null, t, ctx); - this.error = error; - if (!TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_ERROR)) { - Operators.onDiscard(t, ctx); - Operators.onErrorDropped(error, ctx); - return; - } - if (logger != null) { - trace(logger, "Discarding upon timer rejection" + t); - } - Operators.onDiscard(t, ctx); - drain(); - return; - } - } - if ((index + 1) % batchSize == 0) { - shouldDrain = true; - } - break; - } - } - if (shouldDrain) { - if (currentTimeoutTask != null) { - // TODO: it can happen that AFTER I dispose, the timeout - // anyway kicks during/after another onNext(), the buffer is - // delivered, and THEN drain is entered -> - // it would emit a buffer that is too small potentially. - // ALSO: - // It is also possible that here we deliver the buffer, but the - // timeout is happening for a new buffer! - currentTimeoutTask.dispose(); - } - this.index = 0; - drain(); - } - } else { - if (logger != null) { - trace(logger, "Discarding onNext: " + t); + long previousState; + previousState = forceAddWork(this, + BufferTimeoutWithBackpressureSubscriber::incrementRequestIndex); + if (!hasWorkInProgress(previousState)) { + // If there was no demand before - try to fulfill the demand if there + // are buffered values. + drain(previouslyRequested == 0); } - Operators.onDiscard(t, currentContext()); } } @Override - public void onError(Throwable t) { - // set error flag - // set terminated as error + public void onNext(T t) { + if (logger != null) { + trace(logger, "onNext " + t); + } + if (this.done) { + Operators.onNextDropped(t, this.actual.currentContext()); + return; + } - // drain (WIP++ ?) + boolean enqueued = queue.offer(t); + if (!enqueued) { + this.error = Operators.onOperatorError( + this.subscription, + Exceptions.failWithOverflow(Exceptions.BACKPRESSURE_ERROR_QUEUE_FULL), + t, + this.actual.currentContext()); + Operators.onDiscard(t, this.actual.currentContext()); + } - if (currentTimeoutTask != null) { - currentTimeoutTask.dispose(); + long previousState; + + if (enqueued) { + // Only onNext increments the index. Drain can set it to 0 when it + // flushes. However, timeout does not reset it to 0, it has its own + // flag. + previousState = forceAddWork(this, state -> incrementIndex(state, 1)); + + // We can only fire the timer once we increment the index first so that + // the timer doesn't fire first as it would consume the element and try + // to decrement the index below 0. + if (getIndex(previousState) == 0) { + // fire timer, new buffer starts + try { + Disposable disposable = + timer.schedule(this::bufferTimedOut, timeSpan, unit); + currentTimeoutTask.update(disposable); + } catch (RejectedExecutionException e) { + this.error = Operators.onRejectedExecution(e, subscription, null, t, actual.currentContext()); + previousState = forceAddWork(this, + BufferTimeoutWithBackpressureSubscriber::setTerminated); + } + } + } else { + previousState = forceAddWork(this, + BufferTimeoutWithBackpressureSubscriber::setTerminated); } - timer.dispose(); - if (!TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_ERROR)) { - Operators.onErrorDropped(t, currentContext()); - return; + if (!hasWorkInProgress(previousState)) { + drain(false); } - this.error = t; // wip in drain will publish the error - drain(); } - @Override - public void onComplete() { - // set terminated as completed - // drain - if (currentTimeoutTask != null) { - currentTimeoutTask.dispose(); + void bufferTimedOut() { + if (logger != null) { + trace(logger, "timedOut"); } - timer.dispose(); - - if (TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_SUCCESS)) { - drain(); + if (this.done) { + return; } - } - @Override - public CoreSubscriber actual() { - return this.actual; + long previousState = forceAddWork(this, + BufferTimeoutWithBackpressureSubscriber::setTimedOut); + + if (!hasWorkInProgress(previousState)) { + drain(false); + } } @Override - public void request(long n) { - // add cap to currently requested - // if previous requested was 0 -> drain first to deliver outdated values - // if the cap increased, request more ? - - // drain - - if (Operators.validate(n)) { - if (queue.isEmpty() && terminated != NOT_TERMINATED) { - return; - } + public void onError(Throwable t) { + if (logger != null) { + trace(logger, "onError " + t); + } + if (this.done) { + Operators.onErrorDropped(t, actual.currentContext()); + return; + } - if (Operators.addCap(REQUESTED, this, n) == 0) { - // there was no demand before - try to fulfill the demand if there - // are buffered values - drain(); - } + this.error = t; + long previousState = forceAddWork(this, + BufferTimeoutWithBackpressureSubscriber::setTerminated); - if (batchSize == Integer.MAX_VALUE || n == Long.MAX_VALUE) { - requestMore(Long.MAX_VALUE); - } else { - long requestLimit = prefetch; - if (requestLimit > outstanding) { - if (logger != null) { - trace(logger, "requestMore: " + (requestLimit - outstanding) + ", outstanding: " + outstanding); - } - requestMore(requestLimit - outstanding); - } - } + if (!hasWorkInProgress(previousState)) { + drain(false); } } - private void requestMore(long n) { - Subscription s = this.subscription; - if (s != null) { - Operators.addCap(OUTSTANDING, this, n); - s.request(n); + @Override + public void onComplete() { + if (logger != null) { + trace(logger, "onComplete"); + } + if (this.done) { + return; + } + + long previousState = forceAddWork(this, + BufferTimeoutWithBackpressureSubscriber::setTerminated); + + if (!hasWorkInProgress(previousState)) { + drain(false); } } @Override public void cancel() { - // set terminated flag - // cancel upstream subscription - // dispose timer - // drain for proper cleanup - if (logger != null) { trace(logger, "cancel"); } - if (TERMINATED.compareAndSet(this, NOT_TERMINATED, TERMINATED_WITH_CANCEL)) { - if (this.subscription != null) { - this.subscription.cancel(); - } - } - if (currentTimeoutTask != null) { - currentTimeoutTask.dispose(); + if (this.done || isCancelled(this.state)) { + return; } - timer.dispose(); - drain(); - } - void bufferTimedOut() { - // called when buffer times out + if (this.subscription != null) { + subscription.cancel(); + } - // reset index to 0 - // drain + long previousState = forceAddWork(this, + BufferTimeoutWithBackpressureSubscriber::setCancelled); - // TODO: try comparing against current reference and see if it was not - // cancelled -> to do this, replace Disposable timeoutTask with volatile - // and use CAS. - if (logger != null) { - trace(logger, "timerFire"); + if (!hasWorkInProgress(previousState)) { + drain(false); } - this.index = 0; // if currently being drained, it means the buffer is - // delivered due to reaching the batchSize - drain(); } - private void drain() { - // entering this should be guarded by WIP getAndIncrement == 0 - // if we're here it means do a flush if there is downstream demand - // regardless of queue size + /** + * Drain the queue and perform any actions that result from the current state. + * Ths method must only be called when the caller ensured exclusive access. + * That means that it successfully indicated there's work by setting the WIP flag. + * + * @param resumeDemand {@code true} if the previous {@link #requested demand} + * value was 0. + */ + private void drain(boolean resumeDemand) { + if (logger != null) { + trace(logger, "drain start"); + } + for (;;) { + long previousState = this.state; + long currentState = previousState; - // loop: - // if terminated -> check error -> deliver; else complete downstream - // if cancelled + if (done || isCancelled(currentState)) { + if (logger != null) { + trace(logger, "Discarding entire queue of " + queue.size()); + } + Operators.onDiscardQueueWithClear(queue, currentContext(), null); + currentState = tryClearWip(this, currentState); + if (!hasWorkInProgress(currentState)) { + return; + } + } else { + long index = getIndex(currentState); + long currentRequest = this.requested; + boolean shouldFlush = currentRequest > 0 + && (resumeDemand || isTimedOut(currentState) || isTerminated(currentState) || index >= batchSize); - if (WIP.getAndIncrement(this) == 0) { - for (;;) { - int wip = this.wip; + int consumed = 0; if (logger != null) { - trace(logger, "drain. wip: " + wip); + trace(logger, "should flush: " + shouldFlush + + " currentRequest: " + currentRequest + + " index: " + index + + " isTerminated: " + isTerminated(currentState) + + " isTimedOut: " + isTimedOut(currentState)); } - if (terminated == NOT_TERMINATED) { - // is there demand? - while (flushABuffer()) { - // no-op - } - // make another spin if there's more work - } else { - if (completed) { - // if queue is empty, the discard is ignored + if (shouldFlush) { + currentTimeoutTask.update(null); + for (; ; ) { + int consumedNow = flush(); if (logger != null) { - trace(logger, "Discarding entire queue of " + queue.size()); + trace(logger, "flushed: " + consumedNow); } - Operators.onDiscardQueueWithClear(queue, currentContext(), - null); - return; - } - // TODO: potentially the below can be executed twice? - if (terminated == TERMINATED_WITH_CANCEL) { - if (logger != null) { - trace(logger, "Discarding entire queue of " + queue.size()); - } - Operators.onDiscardQueueWithClear(queue, currentContext(), - null); - return; - } - while (flushABuffer()) { - // no-op - } - if (queue.isEmpty()) { - completed = true; - if (this.error != null) { - actual.onError(this.error); + // We need to make sure that if work is added we clear the + // resumeDemand with which we entered the drain loop as the + // state is now different. + resumeDemand = false; + if (consumedNow == 0) { + break; } - else { - actual.onComplete(); + consumed += consumedNow; + if (currentRequest != Long.MAX_VALUE) { + currentRequest = REQUESTED.decrementAndGet(this); } - } else { - if (logger != null) { - trace(logger, "Queue not empty after termination"); + if (currentRequest == 0) { + break; } } } - if (WIP.compareAndSet(this, wip, 0)) { - break; - } - } - } - } - boolean flushABuffer() { - long requested = this.requested; - if (requested != 0) { - T element; - C buffer; + boolean terminated = isTerminated(currentState); - element = queue.poll(); - if (element == null) { - // there is demand, but queue is empty - return false; - } - buffer = bufferSupplier.get(); - int i = 0; - do { - buffer.add(element); - } while ((++i < batchSize) && ((element = queue.poll()) != null)); - - if (requested != Long.MAX_VALUE) { - requested = REQUESTED.decrementAndGet(this); - } - - if (logger != null) { - trace(logger, "flush: " + buffer + ", now requested: " + requested); - } - - actual.onNext(buffer); - - if (requested != Long.MAX_VALUE) { - if (logger != null) { - trace(logger, "outstanding(" + outstanding + ") -= " + i); + if (consumed > 0) { + outstanding -= consumed; } - long remaining = OUTSTANDING.addAndGet(this, -i); - if (terminated == NOT_TERMINATED) { - int replenishMark = prefetch >> 1; // TODO: create field limit instead + if (!terminated && currentRequest > 0) { + // Request more from upstream. + int remaining = this.outstanding; if (remaining < replenishMark) { - if (logger != null) { - trace(logger, "replenish: " + (prefetch - remaining) + ", outstanding: " + outstanding); - } requestMore(prefetch - remaining); } } - if (requested <= 0) { - return false; + if (terminated && queue.isEmpty()) { + done = true; + if (logger != null) { + trace(logger, "terminated! error: " + this.error + " queue size: " + queue.size()); + } + if (this.error != null) { + Operators.onDiscardQueueWithClear(queue, currentContext(), null); + actual.onError(this.error); + } else if (queue.isEmpty()) { + actual.onComplete(); + } + } + + if (consumed > 0) { + int toDecrement = -consumed; + currentState = forceUpdate(this, state -> resetTimeout(incrementIndex(state, toDecrement))); + previousState = resetTimeout(incrementIndex(previousState, toDecrement)); + } + + currentState = tryClearWip(this, previousState); + + // If the state changed (e.g. new item arrived, a request was issued, + // cancellation, error, completion) we will loop again. + if (!hasWorkInProgress(currentState)) { + if (logger != null) { + trace(logger, "drain done"); + } + return; + } + if (logger != null) { + trace(logger, "drain repeat"); } } - // continue to see if there's more - return true; } - return false; + } + + int flush() { + T element; + C buffer; + + element = queue.poll(); + if (element == null) { + // There is demand, but the queue is empty. + return 0; + } + buffer = bufferSupplier.get(); + int i = 0; + do { + buffer.add(element); + } while ((++i < batchSize) && ((element = queue.poll()) != null)); + + actual.onNext(buffer); + + return i; + } + + private void requestMore(int n) { + if (logger != null) { + trace(logger, "requestMore " + n); + } + outstanding += n; + Objects.requireNonNull(this.subscription).request(n); } @Override public Object scanUnsafe(Attr key) { if (key == Attr.PARENT) return this.subscription; - if (key == Attr.CANCELLED) return terminated == TERMINATED_WITH_CANCEL; - if (key == Attr.TERMINATED) return terminated == TERMINATED_WITH_ERROR || terminated == TERMINATED_WITH_SUCCESS; + if (key == Attr.CANCELLED) return isCancelled(this.state); + if (key == Attr.TERMINATED) return isTerminated(this.state); if (key == Attr.REQUESTED_FROM_DOWNSTREAM) return requested; if (key == Attr.CAPACITY) return prefetch; // TODO: revise if (key == Attr.BUFFERED) return queue.size(); @@ -526,6 +534,171 @@ public Object scanUnsafe(Attr key) { return InnerOperator.super.scanUnsafe(key); } + + /* + Below are bit field operations that aid in transitioning the state machine. + An actual update to the state field is achieved with force* method prefix. + The try* prefix indicates that if an update is unsuccessful it will return with + the current state value instead of the intended one. + In these stateful operations we use the StateLogger to indicate transitions + happening. Below are the 3-letter acronyms used: + - faw = forceAddWork + - fup = forceUpdate + - wcl = WIP cleared (meaning work-in-progress and request fields were cleared) + */ + + private static long bitwiseIncrement(long state, long mask, long shift, int amount) { + long shiftAndAdd = ((state & mask) >> shift) + amount; + long shiftBackAndLimit = (shiftAndAdd << shift) & mask; + long clearedState = state & ~mask; + return clearedState | shiftBackAndLimit; + } + + private static boolean isTerminated(long state) { + return (state & TERMINATED_FLAG) == TERMINATED_FLAG; + } + + private static long setTerminated(long state) { + return state | TERMINATED_FLAG; + } + + private static boolean isCancelled(long state) { + return (state & CANCELLED_FLAG) == CANCELLED_FLAG; + } + + private static long setCancelled(long state) { + return state | CANCELLED_FLAG; + } + + private static long incrementRequestIndex(long state) { + return bitwiseIncrement(state, REQUESTED_INDEX_MASK, REQUESTED_INDEX_SHIFT, 1); + } + + private static long getIndex(long state) { + return (state & INDEX_MASK) >> INDEX_SHIFT; + } + + private static long incrementIndex(long state, int amount) { + return bitwiseIncrement(state, INDEX_MASK, INDEX_SHIFT, amount); + } + + private static boolean hasWorkInProgress(long state) { + return (state & HAS_WORK_IN_PROGRESS_FLAG) == HAS_WORK_IN_PROGRESS_FLAG; + } + + private static long setWorkInProgress(long state) { + return state | HAS_WORK_IN_PROGRESS_FLAG; + } + + private static long setTimedOut(long state) { + return state | TIMEOUT_FLAG; + } + + private static long resetTimeout(long state) { + return state & ~TIMEOUT_FLAG; + } + + private static boolean isTimedOut(long state) { + return (state & TIMEOUT_FLAG) == TIMEOUT_FLAG; + } + + /** + * Force a state update and return the state that the update is based upon. + * If the state was replaced but there was work in progress, before leaving the + * protected section the working actor will notice an update and loop again to + * pick up the update (e.g. demand increase). If there was no active actor or + * the active actor was done before our update, the caller of this method is + * obliged to check whether the returned value (the previousState) had WIP flag + * set. In case it was not, it should call the drain procedure. + * + * @param instance target of CAS operations + * @param f transformation to apply to state + * @return state value on which the effective update is based (previousState) + */ + private static long forceAddWork( + BufferTimeoutWithBackpressureSubscriber instance, Function f) { + for (;;) { + long previousState = instance.state; + long nextState = f.apply(previousState) | HAS_WORK_IN_PROGRESS_FLAG; + if (STATE.compareAndSet(instance, previousState, nextState)) { + if (instance.stateLogger != null) { + instance.stateLogger.log(instance.toString(), + "faw", + previousState, + nextState); + } + return previousState; + } + } + } + + /** + * Unconditionally force the state transition and return the new state instead + * of the old one. The caller has no way to know whether the update happened + * while something else had WIP flag set. Therefore, this method can only be + * used in the drain procedure where the caller knows that the WIP flag is set + * and doesn't need to make that inference. + * + * @param instance target of CAS operations + * @param f transformation to apply to state + * @return effective state value (nextState) + */ + private static long forceUpdate( + BufferTimeoutWithBackpressureSubscriber instance, Function f) { + for (;;) { + long previousState = instance.state; + long nextState = f.apply(previousState); + if (STATE.compareAndSet(instance, previousState, nextState)) { + if (instance.stateLogger != null) { + instance.stateLogger.log(instance.toString(), + "fup", + previousState, + nextState); + } + return nextState; + } + } + } + + /** + * Attempt to clear the work-in-progress (WIP) flag. If the current state + * doesn't match the expected state, the current state is returned and the flag + * is not cleared. Otherwise, the effective new state is returned that has: + *
      + *
    • WIP cleared
    • + *
    • Requested index cleared
    • + *
    + * + * @param instance target of CAS operations + * @param expectedState the state reading on which the caller bases the + * intention to remove the WIP flag. In case it's + * currently different, the caller should repeat the + * drain procedure to notice any updates. + * @return current state value (currentState in case of expectations + * mismatch or nextState in case of successful WIP clearing) + */ + private static > long tryClearWip( + BufferTimeoutWithBackpressureSubscriber instance, long expectedState) { + for (;;) { + final long currentState = instance.state; + + if (expectedState != currentState) { + return currentState; + } + + // Remove both WIP and requested_index so that we avoid overflowing + long nextState = currentState & ~HAS_WORK_IN_PROGRESS_FLAG & ~REQUESTED_INDEX_MASK; + if (STATE.compareAndSet(instance, currentState, nextState)) { + if (instance.stateLogger != null) { + instance.stateLogger.log(instance.toString(), + "wcl", + currentState, + nextState); + } + return nextState; + } + } + } } final static class BufferTimeoutSubscriber> diff --git a/reactor-core/src/main/java/reactor/core/publisher/StateLogger.java b/reactor-core/src/main/java/reactor/core/publisher/StateLogger.java index e24a071e45..39e6c0deb6 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/StateLogger.java +++ b/reactor-core/src/main/java/reactor/core/publisher/StateLogger.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2022-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ void log(String instance, formatState(committedState, 64)), new RuntimeException()); } else { - this.logger.trace(String.format("[%s][%s][%s][%s-%s]", + this.logger.trace(String.format("[%s][%s][%s][\n\t%s\n\t%s]", instance, action, Thread.currentThread() diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutFairBackpressureTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutFairBackpressureTest.java index 01b21276a5..20a2237a9a 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutFairBackpressureTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxBufferTimeoutFairBackpressureTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; @@ -62,6 +63,7 @@ public void tearDown() { } @Test + @Tag("slow") void backpressureSupported() throws InterruptedException { final int eventProducerDelayMillis = 200; // Event Producer emits requested items to downstream with a 200ms delay @@ -365,7 +367,7 @@ public void requestedFromUpstreamShouldNotExceedDownstreamDemand() { .assertNext(s -> assertThat(s).containsExactly("a")) .then(() -> assertThat(requestedOutstanding).hasValue(19)) .thenRequest(1) - .then(() -> assertThat(requestedOutstanding).hasValue(20)) + .then(() -> assertThat(requestedOutstanding).hasValue(19)) .thenCancel() .verify(); } @@ -392,6 +394,7 @@ public void scanSubscriberCancelled() { @Test public void bufferTimeoutShouldNotRaceWithNext() { Set seen = new HashSet<>(); + AtomicBoolean complete = new AtomicBoolean(); Consumer> consumer = integers -> { for (Integer i : integers) { if (!seen.add(i)) { @@ -399,7 +402,8 @@ public void bufferTimeoutShouldNotRaceWithNext() { } } }; - CoreSubscriber> actual = new LambdaSubscriber<>(consumer, null, null, null); + CoreSubscriber> actual = new LambdaSubscriber<>( + consumer, null, () -> complete.set(true), null); FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber> test = new FluxBufferTimeout.BufferTimeoutWithBackpressureSubscriber>( @@ -419,6 +423,7 @@ public void bufferTimeoutShouldNotRaceWithNext() { test.onComplete(); assertThat(seen.size()).isEqualTo(500); + assertThat(complete.get()).isTrue(); } //see https://github.com/reactor/reactor-core/issues/1247 diff --git a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java index 22d323bc9f..245c4ddef6 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java @@ -157,8 +157,7 @@ public class OnDiscardShouldNotLeakTest { DiscardScenario.fluxSource("monoFilterWhenFalse", main -> main.last().filterWhen(__ -> Mono.just(false).hide())), DiscardScenario.fluxSource("last", main -> main.last(new Tracked("default")).flatMap(f -> Mono.just(f).hide())), DiscardScenario.fluxSource("flatMapIterable", f -> f.flatMapIterable(Arrays::asList)), - // FIXME: uncomment once https://github.com/reactor/reactor-core/issues/3531 is resolved -// DiscardScenario.fluxSource("bufferTimeout", f -> f.bufferTimeout(2, Duration.ofMillis(1), true).flatMapIterable(Function.identity())), + DiscardScenario.fluxSource("bufferTimeout", f -> f.bufferTimeout(2, Duration.ofMillis(1), true).flatMapIterable(Function.identity())), DiscardScenario.fluxSource("publishOnDelayErrors", f -> f.publishOn(Schedulers.immediate())), DiscardScenario.fluxSource("publishOnImmediateErrors", f -> f.publishOn(Schedulers.immediate(), false, Queues.SMALL_BUFFER_SIZE)), DiscardScenario.fluxSource("publishOnAndPublishOn", main -> main From 8a719081baf8668e8664eaed9c8807fa21e37f1a Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 11 Jun 2024 08:05:11 +0300 Subject: [PATCH 290/312] Bump Micrometer to version 1.12.7 (#3824) Bump Micrometer Tracing to version 1.2.7 --- gradle/libs.versions.toml | 4 ++-- settings.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03779281d8..873f96261e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,9 +14,9 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "4.0.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.6" +micrometer = "1.12.7" micrometerDocsGenerator = "1.0.2" -micrometerTracingTest="1.2.6" +micrometerTracingTest="1.2.7" contextPropagation="1.1.1" kotlin = "1.8.22" reactiveStreams = "1.0.4" diff --git a/settings.gradle b/settings.gradle index d0bcfb0273..5400412184 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,9 +25,9 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.12.7-SNAPSHOT') + version('micrometer', '1.12.8-SNAPSHOT') version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") - version('micrometerTracingTest', "1.2.7-SNAPSHOT") + version('micrometerTracingTest', "1.2.8-SNAPSHOT") version('contextPropagation', "1.1.2-SNAPSHOT") } } From 07a2b246614a21f9fbf5af68b687e85027930c77 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 11 Jun 2024 09:48:15 +0300 Subject: [PATCH 291/312] [release] Prepare and release 3.5.18 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 42ce0472e0..5757bcb081 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.17" - testCompile "io.projectreactor:reactor-test:3.5.17" + compile "io.projectreactor:reactor-core:3.5.18" + testCompile "io.projectreactor:reactor-test:3.5.18" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.18-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.18-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.19-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.19-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.17" + // implementation "io.projectreactor:reactor-tools:3.5.18" } ``` diff --git a/gradle.properties b/gradle.properties index 9a29e23f2f..117f4306ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.18-SNAPSHOT -bomVersion=2022.0.19 -metricsMicrometerVersion=1.0.18-SNAPSHOT +version=3.5.18 +bomVersion=2022.0.20 +metricsMicrometerVersion=1.0.18 org.gradle.parallel=true From 8372361ca2369f4f1b3b469e75f2acc33180c37a Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 11 Jun 2024 11:29:04 +0300 Subject: [PATCH 292/312] [release] Next development version 3.5.19-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 117f4306ce..c4ca34cf13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.18 +version=3.5.19-SNAPSHOT bomVersion=2022.0.20 -metricsMicrometerVersion=1.0.18 +metricsMicrometerVersion=1.0.19-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0bfd8642bd..201827813e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.17" -baselinePerfCore = "3.5.17" +baseline-core-api = "3.5.18" +baselinePerfCore = "3.5.18" baselinePerfExtra = "3.5.1" # Other shared versions From 64487e8a47f362088561495ba33dd72e8c8728f4 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 11 Jun 2024 12:03:48 +0300 Subject: [PATCH 293/312] [release] Prepare and release 3.6.7 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dec49b5c4b..0476b6df78 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.6" - testCompile "io.projectreactor:reactor-test:3.6.6" + compile "io.projectreactor:reactor-core:3.6.7" + testCompile "io.projectreactor:reactor-test:3.6.7" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.7-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.7-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.8-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.8-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.6" + // implementation "io.projectreactor:reactor-tools:3.6.7" } ``` diff --git a/gradle.properties b/gradle.properties index 5e0ff41d0d..a0cf408edd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.7-SNAPSHOT -bomVersion=2023.0.6 -metricsMicrometerVersion=1.1.7-SNAPSHOT +version=3.6.7 +bomVersion=2023.0.7 +metricsMicrometerVersion=1.1.7 org.gradle.parallel=true From 77cdc5297cf24dc5ce1239483113115219c83460 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 11 Jun 2024 12:58:47 +0300 Subject: [PATCH 294/312] [release] Next development version 3.6.8-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index a0cf408edd..f8067087cd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.7 +version=3.6.8-SNAPSHOT bomVersion=2023.0.7 -metricsMicrometerVersion=1.1.7 +metricsMicrometerVersion=1.1.8-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 873f96261e..b1491d98d8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.6" -baselinePerfCore = "3.6.6" +baseline-core-api = "3.6.7" +baselinePerfCore = "3.6.7" baselinePerfExtra = "3.5.1" # Other shared versions From a367c57f2343d2bf0aa604ba22a907c405f0bdf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 8 Jul 2024 14:09:18 +0200 Subject: [PATCH 295/312] Bump micrometer-docs-generator to 1.0.3 --- gradle/libs.versions.toml | 2 +- settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 201827813e..8efb84bc69 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ baselinePerfExtra = "3.5.1" asciidoctor = "4.0.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below micrometer = "1.10.13" -micrometerDocsGenerator = "1.0.2" +micrometerDocsGenerator = "1.0.3" micrometerTracingTest="1.0.12" contextPropagation="1.0.6" kotlin = "1.8.22" diff --git a/settings.gradle b/settings.gradle index d11ad9f5d7..4e92d65ee2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,7 +23,7 @@ dependencyResolutionManagement { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { version('micrometer', '1.10.14-SNAPSHOT') - version('micrometerDocsGenerator', "1.0.3-SNAPSHOT") + version('micrometerDocsGenerator', "1.0.4-SNAPSHOT") version('micrometerTracingTest', "1.0.13-SNAPSHOT") version('contextPropagation', "1.0.7-SNAPSHOT") } From 5d262bc83dc75289872cf849b88eccb7e6df2e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Mon, 8 Jul 2024 14:27:23 +0200 Subject: [PATCH 296/312] Bump Micrometer to version 1.12.8 --- gradle/libs.versions.toml | 4 ++-- settings.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 980007dea6..3670fcf7d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,9 +14,9 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "4.0.2" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.7" +micrometer = "1.12.8" micrometerDocsGenerator = "1.0.3" -micrometerTracingTest="1.2.7" +micrometerTracingTest="1.2.8" contextPropagation="1.1.1" kotlin = "1.8.22" reactiveStreams = "1.0.4" diff --git a/settings.gradle b/settings.gradle index 094e2abdc4..f018a6376e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,9 +25,9 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.12.8-SNAPSHOT') + version('micrometer', '1.12.9-SNAPSHOT') version('micrometerDocsGenerator', "1.0.4-SNAPSHOT") - version('micrometerTracingTest', "1.2.8-SNAPSHOT") + version('micrometerTracingTest', "1.2.9-SNAPSHOT") version('contextPropagation', "1.1.2-SNAPSHOT") } } From c77e53f60d73957971bfc9026a99b3e3ae2cf082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Jul 2024 09:45:35 +0200 Subject: [PATCH 297/312] [release] Prepare and release 3.5.19 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5757bcb081..9d763d23cf 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.18" - testCompile "io.projectreactor:reactor-test:3.5.18" + compile "io.projectreactor:reactor-core:3.5.19" + testCompile "io.projectreactor:reactor-test:3.5.19" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.19-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.19-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.20-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.20-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.18" + // implementation "io.projectreactor:reactor-tools:3.5.19" } ``` diff --git a/gradle.properties b/gradle.properties index c4ca34cf13..8a770cda0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.19-SNAPSHOT -bomVersion=2022.0.20 -metricsMicrometerVersion=1.0.19-SNAPSHOT +version=3.5.19 +bomVersion=2022.0.21 +metricsMicrometerVersion=1.0.19 org.gradle.parallel=true From f1119e5ca5390dd1adde2bbdc362f1703b2f4c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Jul 2024 10:31:21 +0200 Subject: [PATCH 298/312] [release] Next development version 3.5.20-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8a770cda0b..6521c802b3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.19 +version=3.5.20-SNAPSHOT bomVersion=2022.0.21 -metricsMicrometerVersion=1.0.19 +metricsMicrometerVersion=1.0.20-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8efb84bc69..ef96b8d6eb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.18" -baselinePerfCore = "3.5.18" +baseline-core-api = "3.5.19" +baselinePerfCore = "3.5.19" baselinePerfExtra = "3.5.1" # Other shared versions From 5caf58aff4c53998dd7aef60a1ee6d7f68d51859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Jul 2024 11:07:44 +0200 Subject: [PATCH 299/312] [release] Prepare and release 3.6.8 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0476b6df78..1decace91f 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.7" - testCompile "io.projectreactor:reactor-test:3.6.7" + compile "io.projectreactor:reactor-core:3.6.8" + testCompile "io.projectreactor:reactor-test:3.6.8" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.8-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.8-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.9-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.9-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.7" + // implementation "io.projectreactor:reactor-tools:3.6.8" } ``` diff --git a/gradle.properties b/gradle.properties index f8067087cd..3040445f07 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.8-SNAPSHOT -bomVersion=2023.0.7 -metricsMicrometerVersion=1.1.8-SNAPSHOT +version=3.6.8 +bomVersion=2023.0.8 +metricsMicrometerVersion=1.1.8 org.gradle.parallel=true From 1b2b5e9990578de682cfd3c675c0e04ea28e469c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 9 Jul 2024 11:36:47 +0200 Subject: [PATCH 300/312] [release] Next development version 3.6.9-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 3040445f07..ea435ab52d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.8 +version=3.6.9-SNAPSHOT bomVersion=2023.0.8 -metricsMicrometerVersion=1.1.8 +metricsMicrometerVersion=1.1.9-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3670fcf7d4..03ac767ce9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.7" -baselinePerfCore = "3.6.7" +baseline-core-api = "3.6.8" +baselinePerfCore = "3.6.8" baselinePerfExtra = "3.5.1" # Other shared versions From ebded61a33055f7ca9731b54285fcd17b436829c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Fri, 12 Jul 2024 13:51:00 +0200 Subject: [PATCH 301/312] Skip Automatic Context Propagation in special operators (#3845) This change should improve performance of Automatic Context Propagation in certain cases when doOnDiscard, onErrorContinue, and onErrorStop are used. The context-propagation integration requires contextWrite and tap operators to be barriers for restoring ThreadLocal values. Some internal usage of contextWrite does not require us to treat the operators the same way and we can skip the ceremony of restoring ThreadLocal state as we know that no ThreadLocalAccessor can be registered for them. Therefore, a private variant is introduced to avoid unnecessary overhead when not required. Related #3840 --- .../java/reactor/core/publisher/Flux.java | 16 ++- .../java/reactor/core/publisher/Mono.java | 16 ++- .../AutomaticContextPropagationTest.java | 122 ++++++++++++++++++ 3 files changed, 146 insertions(+), 8 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/Flux.java b/reactor-core/src/main/java/reactor/core/publisher/Flux.java index d28f3fccce..b244ff8cdf 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Flux.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Flux.java @@ -4393,6 +4393,14 @@ public final Flux contextWrite(Function contextModifier) { return onAssembly(new FluxContextWrite<>(this, contextModifier)); } + private final Flux contextWriteSkippingContextPropagation(ContextView contextToAppend) { + return contextWriteSkippingContextPropagation(c -> c.putAll(contextToAppend)); + } + + private final Flux contextWriteSkippingContextPropagation(Function contextModifier) { + return onAssembly(new FluxContextWrite<>(this, contextModifier)); + } + /** * Counts the number of values in this {@link Flux}. * The count will be emitted when onComplete is observed. @@ -4866,7 +4874,7 @@ public final Flux doOnComplete(Runnable onComplete) { * @return a {@link Flux} that cleans up matching elements that get discarded upstream of it. */ public final Flux doOnDiscard(final Class type, final Consumer discardHook) { - return contextWrite(Operators.discardLocalAdapter(type, discardHook)); + return contextWriteSkippingContextPropagation(Operators.discardLocalAdapter(type, discardHook)); } /** @@ -7147,7 +7155,7 @@ public final Flux onErrorComplete(Predicate predicate) { */ public final Flux onErrorContinue(BiConsumer errorConsumer) { BiConsumer genericConsumer = errorConsumer; - return contextWrite(Context.of( + return contextWriteSkippingContextPropagation(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.resume(genericConsumer) )); @@ -7231,7 +7239,7 @@ public final Flux onErrorContinue(Predicate errorPre @SuppressWarnings("unchecked") Predicate genericPredicate = (Predicate) errorPredicate; BiConsumer genericErrorConsumer = errorConsumer; - return contextWrite(Context.of( + return contextWriteSkippingContextPropagation(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.resumeIf(genericPredicate, genericErrorConsumer) )); @@ -7248,7 +7256,7 @@ public final Flux onErrorContinue(Predicate errorPre * was used downstream */ public final Flux onErrorStop() { - return contextWrite(Context.of( + return contextWriteSkippingContextPropagation(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.stop())); } diff --git a/reactor-core/src/main/java/reactor/core/publisher/Mono.java b/reactor-core/src/main/java/reactor/core/publisher/Mono.java index f8f5ae96ee..4c1df5d815 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/Mono.java +++ b/reactor-core/src/main/java/reactor/core/publisher/Mono.java @@ -2424,6 +2424,14 @@ public final Mono contextWrite(Function contextModifier) { return onAssembly(new MonoContextWrite<>(this, contextModifier)); } + private final Mono contextWriteSkippingContextPropagation(ContextView contextToAppend) { + return contextWriteSkippingContextPropagation(c -> c.putAll(contextToAppend)); + } + + private final Mono contextWriteSkippingContextPropagation(Function contextModifier) { + return onAssembly(new MonoContextWrite<>(this, contextModifier)); + } + /** * Provide a default single value if this mono is completed without any data * @@ -2713,7 +2721,7 @@ public final Mono doOnCancel(Runnable onCancel) { * @return a {@link Mono} that cleans up matching elements that get discarded upstream of it. */ public final Mono doOnDiscard(final Class type, final Consumer discardHook) { - return contextWrite(Operators.discardLocalAdapter(type, discardHook)); + return contextWriteSkippingContextPropagation(Operators.discardLocalAdapter(type, discardHook)); } /** @@ -3712,7 +3720,7 @@ public final Mono onErrorComplete(Predicate predicate) { */ public final Mono onErrorContinue(BiConsumer errorConsumer) { BiConsumer genericConsumer = errorConsumer; - return contextWrite(Context.of( + return contextWriteSkippingContextPropagation(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.resume(genericConsumer) )); @@ -3802,7 +3810,7 @@ public final Mono onErrorContinue(Predicate errorPre @SuppressWarnings("unchecked") Predicate genericPredicate = (Predicate) errorPredicate; BiConsumer genericErrorConsumer = errorConsumer; - return contextWrite(Context.of( + return contextWriteSkippingContextPropagation(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.resumeIf(genericPredicate, genericErrorConsumer) )); @@ -3819,7 +3827,7 @@ public final Mono onErrorContinue(Predicate errorPre * was used downstream */ public final Mono onErrorStop() { - return contextWrite(Context.of( + return contextWriteSkippingContextPropagation(Context.of( OnNextFailureStrategy.KEY_ON_NEXT_ERROR_STRATEGY, OnNextFailureStrategy.stop())); } diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java index 27c5927d91..a23774a906 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java @@ -33,6 +33,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; @@ -40,6 +41,7 @@ import java.util.stream.Stream; import io.micrometer.context.ContextRegistry; +import io.micrometer.context.ThreadLocalAccessor; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -2453,4 +2455,124 @@ void fluxToIterable() { assertThat(value.get()).isEqualTo("present"); } } + + @Nested + class SpecialContextAlteringOperators { + + // The cases here consider operators like doOnDiscard(), which underneath + // utilize contextWrite() for its purpose. They are special in that we use them + // internally and do not anticipate the registered keys to be corresponding to + // any ThreadLocal values. That expectation is reasonable in user facing code + // as we don't know what keys are used and whether a ThreadLocalAccessor is + // registered for these keys. Therefore, in specific cases that are internal to + // reactor-core, we can skip ThreadLocal restoration in fragments of the chain. + + // Explanation of GET/SET operations on TL in the scenarios here: + // When going UP, we use the value "present". + // When going DOWN, we clear the value or restore a captured empty value. + // + // 1 x GET in block() with implicit context capture + // + // 1 x GET going UP from contextWrite (read current to restore later) + // + 2 x SET going UP from contextWrite + SET restoring current later + // + // 1 x GET going DOWN from contextWrite with subscription (read current) + // + 2 x SET going DOWN from contextWrite + SET restoring current later + // + // 1 x GET going UP to request (read current) + // + 2 x SET going UP from contextWrite + SET restoring current later + // + // 1 x GET going DOWN to deliver onComplete (read current) + // + 2 x SET going DOWN from contextWrite + SET restoring current later + + @Test + void discardFlux() { + CountingThreadLocalAccessor accessor = new CountingThreadLocalAccessor(); + ContextRegistry.getInstance().registerThreadLocalAccessor(accessor); + + AtomicInteger tlPresent = new AtomicInteger(); + AtomicInteger discards = new AtomicInteger(); + + Flux.just("a") + .doOnEach(signal -> { + if (CountingThreadLocalAccessor.TL.get().equals("present")) { + tlPresent.incrementAndGet(); + } + }) + .filter(s -> false) + .doOnDiscard(String.class, s -> discards.incrementAndGet()) + .count() + .contextWrite(ctx -> ctx.put(CountingThreadLocalAccessor.KEY, "present")) + .block(); + + assertThat(tlPresent.get()).isEqualTo(2); // 1 x onNext + 1 x onComplete + assertThat(discards.get()).isEqualTo(1); + // 5 with doOnDiscard skipping TL restoration, 9 with restoring + assertThat(accessor.reads.get()).isEqualTo(5); + // 8 with doOnDiscard skipping TL restoration, 16 with restoring + assertThat(accessor.writes.get()).isEqualTo(8); + + ContextRegistry.getInstance().removeThreadLocalAccessor(CountingThreadLocalAccessor.KEY); + } + + @Test + void discardMono() { + CountingThreadLocalAccessor accessor = new CountingThreadLocalAccessor(); + ContextRegistry.getInstance().registerThreadLocalAccessor(accessor); + + AtomicInteger tlPresent = new AtomicInteger(); + AtomicInteger discards = new AtomicInteger(); + + Mono.just("a") + .doOnEach(signal -> { + if (CountingThreadLocalAccessor.TL.get().equals("present")) { + tlPresent.incrementAndGet(); + } + }) + .filter(s -> false) + .doOnDiscard(String.class, s -> discards.incrementAndGet()) + .contextWrite(ctx -> ctx.put(CountingThreadLocalAccessor.KEY, "present")) + .block(); + + assertThat(tlPresent.get()).isEqualTo(2); // 1 x onNext + 1 x onComplete + assertThat(discards.get()).isEqualTo(1); + // 5 with doOnDiscard skipping TL restoration, 9 with restoring + assertThat(accessor.reads.get()).isEqualTo(5); + // 8 with doOnDiscard skipping TL restoration, 16 with restoring + assertThat(accessor.writes.get()).isEqualTo(8); + + ContextRegistry.getInstance().removeThreadLocalAccessor(CountingThreadLocalAccessor.KEY); + } + } + + private static class CountingThreadLocalAccessor implements ThreadLocalAccessor { + static final String KEY = "CTLA"; + static final ThreadLocal TL = new ThreadLocal<>(); + + AtomicInteger reads = new AtomicInteger(); + AtomicInteger writes = new AtomicInteger(); + + @Override + public Object key() { + return KEY; + } + + @Override + public String getValue() { + reads.incrementAndGet(); + return TL.get(); + } + + @Override + public void setValue(String s) { + writes.incrementAndGet(); + TL.set(s); + } + + @Override + public void setValue() { + writes.incrementAndGet(); + TL.remove(); + } + } } From eaa889e983a9a058b57c807f92c8c763ef1a44ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Wed, 17 Jul 2024 09:52:32 +0200 Subject: [PATCH 302/312] Skip automatic context-propagation in Flux.generate (#3848) As Flux.generate uses SynchronousSink, which should not be used asynchronously, we can eliminate the unnecessary ThreadLocal restoration from this operator. Related to #3840. --- .../main/java/reactor/core/publisher/FluxGenerate.java | 9 +++------ .../publisher/AutomaticContextPropagationTest.java | 10 ---------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/publisher/FluxGenerate.java b/reactor-core/src/main/java/reactor/core/publisher/FluxGenerate.java index fbc2a0f4bb..98b76f47af 100644 --- a/reactor-core/src/main/java/reactor/core/publisher/FluxGenerate.java +++ b/reactor-core/src/main/java/reactor/core/publisher/FluxGenerate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,18 +74,15 @@ final class FluxGenerate @Override public void subscribe(CoreSubscriber actual) { - CoreSubscriber wrapped = - Operators.restoreContextOnSubscriberIfAutoCPEnabled(this, actual); - S state; try { state = stateSupplier.call(); } catch (Throwable e) { - Operators.error(wrapped, Operators.onOperatorError(e, wrapped.currentContext())); + Operators.error(actual, Operators.onOperatorError(e, actual.currentContext())); return; } - wrapped.onSubscribe(new GenerateSubscription<>(wrapped, state, generator, stateConsumer)); + actual.onSubscribe(new GenerateSubscription<>(actual, state, generator, stateConsumer)); } @Override diff --git a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java index a23774a906..16f83b6985 100644 --- a/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java +++ b/reactor-core/src/withMicrometerTest/java/reactor/core/publisher/AutomaticContextPropagationTest.java @@ -763,16 +763,6 @@ void fluxConcatIterable() { // Direct subscription } - @Test - void fluxGenerate() { - assertThreadLocalsPresentInFlux(() -> Flux.generate(sink -> { - sink.next("Hello"); - // the generator is checked if any signal was delivered by the consumer - // so we perform asynchronous completion only - executorService.submit(sink::complete); - })); - } - @Test void fluxCombineLatest() { assertThreadLocalsPresentInFlux(() -> From 941e324999ea0d0133f18fb2d1aa605ba405e0d4 Mon Sep 17 00:00:00 2001 From: Trustin Lee Date: Tue, 23 Jul 2024 16:15:18 +0900 Subject: [PATCH 303/312] Allow registering a custom `Predicate` for determining non-blocking threads (#3854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change allows registering non-blocking threads to be identified as such by reactor-core instead of only relying on the `reactor.core.scheduler.NonBlocking` interface. Some third-party libraries and frameworks don't directly depend on `reactor-core`, yet they want to mark the threads they manage as non-blocking and can't directly use the `NonBlocking` type. Modifications: - Added a new method `Schedulers.registerNonBlockingThreadPredicate()` so that a user can register their own `Predicate` that determines whether a given thread is non-blocking or not - Also added `Schedulers.resetNonBlockingThreadPredicate()` so that a user can unregister all previous `Predicate`s - Fixed an incorrectly implemented test that doesn't really test anything: `SchedulersTest.isInNonBlockingThreadTrue()` --------- Co-authored-by: Dariusz Jędrzejczyk --- .../reactor/core/scheduler/Schedulers.java | 45 +++++++++++--- .../core/scheduler/SchedulersTest.java | 60 +++++++++++++++++-- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java index 9dbd985b60..7617cb1808 100644 --- a/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java +++ b/reactor-core/src/main/java/reactor/core/scheduler/Schedulers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import io.micrometer.core.instrument.MeterRegistry; @@ -121,6 +122,10 @@ public abstract class Schedulers { .map(Boolean::parseBoolean) .orElse(false); + static final Predicate DEFAULT_NON_BLOCKING_THREAD_PREDICATE = thread -> false; + + static Predicate nonBlockingThreadPredicate = DEFAULT_NON_BLOCKING_THREAD_PREDICATE; + /** * Create a {@link Scheduler} which uses a backing {@link Executor} to schedule * Runnables for async operators. @@ -659,24 +664,50 @@ public static void onHandleError(String key, BiConsumer + *
  • the thread implements {@link NonBlocking}; or
  • + *
  • any of the {@link Predicate}s registered via {@link #registerNonBlockingThreadPredicate(Predicate)} + * returns {@code true}.
  • + * * * @return {@code true} if blocking is forbidden in this thread, {@code false} otherwise */ public static boolean isInNonBlockingThread() { - return Thread.currentThread() instanceof NonBlocking; + return isNonBlockingThread(Thread.currentThread()); } /** * Check if calling a Reactor blocking API in the given {@link Thread} is forbidden - * or not, by checking if the thread implements {@link NonBlocking} (in which case it is - * forbidden and this method returns {@code true}). + * or not. This method returns {@code true} and will forbid the Reactor blocking API if + * any of the following conditions meet: + *
      + *
    • the thread implements {@link NonBlocking}; or
    • + *
    • any of the {@link Predicate}s registered via {@link #registerNonBlockingThreadPredicate(Predicate)} + * returns {@code true}.
    • + *
    * * @return {@code true} if blocking is forbidden in that thread, {@code false} otherwise */ public static boolean isNonBlockingThread(Thread t) { - return t instanceof NonBlocking; + return t instanceof NonBlocking || nonBlockingThreadPredicate.test(t); + } + + /** + * Registers the specified {@link Predicate} that determines whether it is forbidden to call + * a Reactor blocking API in a given {@link Thread} or not. + */ + public static void registerNonBlockingThreadPredicate(Predicate predicate) { + nonBlockingThreadPredicate = nonBlockingThreadPredicate.or(predicate); + } + + /** + * Unregisters all the {@link Predicate}s registered so far via + * {@link #registerNonBlockingThreadPredicate(Predicate)}. + */ + public static void resetNonBlockingThreadPredicate() { + nonBlockingThreadPredicate = DEFAULT_NON_BLOCKING_THREAD_PREDICATE; } /** diff --git a/reactor-core/src/test/java/reactor/core/scheduler/SchedulersTest.java b/reactor-core/src/test/java/reactor/core/scheduler/SchedulersTest.java index b0e41798b0..7232a0248b 100644 --- a/reactor-core/src/test/java/reactor/core/scheduler/SchedulersTest.java +++ b/reactor-core/src/test/java/reactor/core/scheduler/SchedulersTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -359,10 +360,53 @@ public void isNonBlockingThreadInstanceOf() { @Test public void isInNonBlockingThreadTrue() { - new ReactorThreadFactory.NonBlockingThread(() -> assertThat(Schedulers.isInNonBlockingThread()) - .as("isInNonBlockingThread") - .isFalse(), - "isInNonBlockingThreadTrue"); + assertNonBlockingThread(ReactorThreadFactory.NonBlockingThread::new, true); + } + + @Test + public void customNonBlockingThreadPredicate() { + assertThat(Schedulers.nonBlockingThreadPredicate) + .as("nonBlockingThreadPredicate") + .isSameAs(Schedulers.DEFAULT_NON_BLOCKING_THREAD_PREDICATE); + + // The custom `Predicate` is not registered yet, + // so `CustomNonBlockingThread` will be considered blocking. + assertNonBlockingThread(CustomNonBlockingThread::new, false); + + // Now register the `Predicate` and ensure `CustomNonBlockingThread` is non-blocking. + Schedulers.registerNonBlockingThreadPredicate(t -> t instanceof CustomNonBlockingThread); + try { + assertNonBlockingThread(CustomNonBlockingThread::new, true); + } finally { + // Restore the global predicate. + Schedulers.resetNonBlockingThreadPredicate(); + } + + assertThat(Schedulers.nonBlockingThreadPredicate) + .as("nonBlockingThreadPredicate (after reset)") + .isSameAs(Schedulers.DEFAULT_NON_BLOCKING_THREAD_PREDICATE); + } + + private static void assertNonBlockingThread(BiFunction threadFactory, + boolean expectedNonBlocking) { + CompletableFuture future = new CompletableFuture<>(); + Thread thread = threadFactory.apply(() -> { + try { + assertThat(Schedulers.isInNonBlockingThread()) + .as("isInNonBlockingThread") + .isEqualTo(expectedNonBlocking); + future.complete(null); + } catch (Throwable cause) { + future.completeExceptionally(cause); + } + }, "assertNonBlockingThread"); + + assertThat(Schedulers.isNonBlockingThread(thread)) + .as("isNonBlockingThread") + .isEqualTo(expectedNonBlocking); + + thread.start(); + future.join(); } @Test @@ -1457,4 +1501,10 @@ public void dispose() { } } } + + final static class CustomNonBlockingThread extends Thread { + CustomNonBlockingThread(Runnable target, String name) { + super(target, name); + } + } } From d85b90953f871818db2ad34b51c1b1e5737dcb5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 25 Jul 2024 09:33:42 +0200 Subject: [PATCH 304/312] Dispose TimedRunnable upon TimedWorker shutdown (#3856) The `TimedRunnable` that is created using a `TimedWorker` was not disposed upon `Worker` shutdown. That led the pending tasks timers to run forever, causing leaks. This change keeps track of created `TimedRunnable` instances by the `TimedWorker`, allowing to dispose the resources responsibly. Resolves #3844 --- .../micrometer/TimedScheduler.java | 143 +++++++++++++++--- .../micrometer/TimedSchedulerTest.java | 79 ++++++++++ 2 files changed, 198 insertions(+), 24 deletions(-) diff --git a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java index c6bdc59400..8762c6efc8 100644 --- a/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java +++ b/reactor-core-micrometer/src/main/java/reactor/core/observability/micrometer/TimedScheduler.java @@ -16,8 +16,10 @@ package reactor.core.observability.micrometer; +import java.util.Collection; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.LongTaskTimer; @@ -27,8 +29,10 @@ import io.micrometer.core.instrument.Timer; import reactor.core.Disposable; +import reactor.core.Disposables; import reactor.core.observability.micrometer.TimedSchedulerMeterDocumentation.SubmittedTags; import reactor.core.scheduler.Scheduler; +import reactor.util.annotation.Nullable; import static reactor.core.observability.micrometer.TimedSchedulerMeterDocumentation.*; @@ -136,21 +140,32 @@ static final class TimedWorker implements Worker { final TimedScheduler parent; final Worker delegate; + /** + * As this Worker creates {@link TimedRunnable} instances which are {@link Disposable} + * it needs to keep track of them to be able to dispose them when this instance + * is {@link #dispose() disposed}. + */ + final Composite disposables; + TimedWorker(TimedScheduler parent, Worker delegate) { this.parent = parent; this.delegate = delegate; + this.disposables = Disposables.composite(); } TimedRunnable wrap(Runnable task) { - return new WorkerBackedTimedRunnable(parent.registry, parent, delegate, task); + return new WorkerBackedTimedRunnable(parent.registry, parent, delegate, + task, disposables); } TimedRunnable wrapPeriodic(Runnable task) { - return new WorkerBackedTimedRunnable(parent.registry, parent, delegate, task, true); + return new WorkerBackedTimedRunnable(parent.registry, parent, delegate, + task, disposables, true); } @Override public void dispose() { + disposables.dispose(); delegate.dispose(); } @@ -162,6 +177,7 @@ public boolean isDisposed() { @Override public Disposable schedule(Runnable task) { TimedRunnable timedTask = wrap(task); + disposables.add(timedTask); return timedTask.schedule(); } @@ -169,6 +185,7 @@ public Disposable schedule(Runnable task) { @Override public Disposable schedule(Runnable task, long delay, TimeUnit unit) { TimedRunnable timedTask = wrap(task); + disposables.add(timedTask); return timedTask.schedule(delay, unit); } @@ -176,14 +193,21 @@ public Disposable schedule(Runnable task, long delay, TimeUnit unit) { @Override public Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) { TimedRunnable timedTask = wrapPeriodic(task); + disposables.add(timedTask); + return timedTask.schedulePeriodically(initialDelay, period, unit); } } private static abstract class TimedRunnable implements Runnable, Disposable { - final MeterRegistry registry; - final TimedScheduler parent; - final Runnable task; + /** marker that the Worker was disposed and the parent got notified */ + static final Composite DISPOSED = new EmptyCompositeDisposable(); + /** marker that the Worker has completed, for the PARENT field */ + static final Composite DONE = new EmptyCompositeDisposable(); + + final MeterRegistry registry; + final TimedScheduler timedScheduler; + final Runnable task; final LongTaskTimer.Sample pendingSample; @@ -191,22 +215,29 @@ private static abstract class TimedRunnable implements Runnable, Disposable { Disposable disposable; - TimedRunnable(MeterRegistry registry, TimedScheduler parent, Runnable task) { - this(registry, parent, task, false); + volatile Composite parent; + static final AtomicReferenceFieldUpdater PARENT = + AtomicReferenceFieldUpdater.newUpdater(TimedRunnable.class, Composite.class, "parent"); + + TimedRunnable(MeterRegistry registry, TimedScheduler timedScheduler, Runnable task, + @Nullable Composite parent) { + this(registry, timedScheduler, task, parent, false); } - TimedRunnable(MeterRegistry registry, TimedScheduler parent, Runnable task, boolean periodic) { + TimedRunnable(MeterRegistry registry, TimedScheduler timedScheduler, Runnable task, + @Nullable Composite parent, boolean periodic) { this.registry = registry; - this.parent = parent; + this.timedScheduler = timedScheduler; this.task = task; if (periodic) { this.pendingSample = null; } else { - this.pendingSample = parent.pendingTasks.start(); + this.pendingSample = timedScheduler.pendingTasks.start(); } this.isRerun = false; //will be ignored if not periodic + PARENT.lazySet(this, parent); } @Override @@ -220,16 +251,23 @@ public void run() { this.isRerun = true; } else { - parent.submittedPeriodicIteration.increment(); + timedScheduler.submittedPeriodicIteration.increment(); } } - Runnable completionTrackingTask = parent.completedTasks.wrap(this.task); - this.parent.activeTasks.record(completionTrackingTask); + try { + Runnable completionTrackingTask = timedScheduler.completedTasks.wrap(this.task); + this.timedScheduler.activeTasks.record(completionTrackingTask); + } finally { + Composite o = parent; + if (o != DISPOSED && PARENT.compareAndSet(this, o, DONE) && o != null) { + o.remove(this); + } + } } public Disposable schedule() { - parent.submittedDirect.increment(); + timedScheduler.submittedDirect.increment(); try { disposable = this.internalSchedule(); @@ -241,7 +279,7 @@ public Disposable schedule() { } public Disposable schedule(long delay, TimeUnit unit) { - parent.submittedDelayed.increment(); + timedScheduler.submittedDelayed.increment(); try { disposable = this.internalSchedule(delay, unit); @@ -253,7 +291,7 @@ public Disposable schedule(long delay, TimeUnit unit) { } public Disposable schedulePeriodically(long initialDelay, long period, TimeUnit unit) { - parent.submittedPeriodicInitial.increment(); + timedScheduler.submittedPeriodicInitial.increment(); return this.internalSchedulePeriodically(initialDelay, period, unit); } @@ -266,6 +304,23 @@ public void dispose() { if (pendingSample != null) { pendingSample.stop(); } + + for (;;) { + Composite o = parent; + if (o == DONE || o == DISPOSED || o == null) { + return; + } + if (PARENT.compareAndSet(this, o, DISPOSED)) { + o.remove(this); + return; + } + } + } + + @Override + public boolean isDisposed() { + Composite o = PARENT.get(this); + return o == DISPOSED || o == DONE; } abstract Disposable internalSchedule(); @@ -279,13 +334,15 @@ static final class WorkerBackedTimedRunnable extends TimedRunnable { final Worker worker; - WorkerBackedTimedRunnable(MeterRegistry registry, TimedScheduler parent, Worker worker, Runnable task) { - super(registry, parent, task); + WorkerBackedTimedRunnable(MeterRegistry registry, TimedScheduler timedScheduler, + Worker worker, Runnable task, Composite parent) { + super(registry, timedScheduler, task, parent); this.worker = worker; } - WorkerBackedTimedRunnable(MeterRegistry registry, TimedScheduler parent, Worker worker, Runnable task, boolean periodic) { - super(registry, parent, task, periodic); + WorkerBackedTimedRunnable(MeterRegistry registry, TimedScheduler timedScheduler, + Worker worker, Runnable task, Composite parent, boolean periodic) { + super(registry, timedScheduler, task, parent, periodic); this.worker = worker; } @@ -309,13 +366,15 @@ static final class SchedulerBackedTimedRunnable extends TimedRunnable { final Scheduler scheduler; - SchedulerBackedTimedRunnable(MeterRegistry registry, TimedScheduler parent, Scheduler scheduler, Runnable task) { - super(registry, parent, task); + SchedulerBackedTimedRunnable(MeterRegistry registry, TimedScheduler timedScheduler, + Scheduler scheduler, Runnable task) { + super(registry, timedScheduler, task, null); this.scheduler = scheduler; } - SchedulerBackedTimedRunnable(MeterRegistry registry, TimedScheduler parent, Scheduler scheduler, Runnable task, boolean periodic) { - super(registry, parent, task, periodic); + SchedulerBackedTimedRunnable(MeterRegistry registry, TimedScheduler timedScheduler, + Scheduler scheduler, Runnable task, boolean periodic) { + super(registry, timedScheduler, task, null, periodic); this.scheduler = scheduler; } @@ -334,4 +393,40 @@ Disposable internalSchedulePeriodically(long initialDelay, long period, TimeUnit return scheduler.schedulePeriodically(this, initialDelay, period, unit); } } + + /** + * Copy of reactor.core.scheduler.EmptyCompositeDisposable for internal use. + */ + static final class EmptyCompositeDisposable implements Disposable.Composite { + + @Override + public boolean add(Disposable d) { + return false; + } + + @Override + public boolean addAll(Collection ds) { + return false; + } + + @Override + public boolean remove(Disposable d) { + return false; + } + + @Override + public int size() { + return 0; + } + + @Override + public void dispose() { + } + + @Override + public boolean isDisposed() { + return false; + } + + } } diff --git a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java index b0c22467b4..91d3f79f89 100644 --- a/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java +++ b/reactor-core-micrometer/src/test/java/reactor/core/observability/micrometer/TimedSchedulerTest.java @@ -16,6 +16,7 @@ package reactor.core.observability.micrometer; +import java.lang.reflect.Field; import java.time.Duration; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; @@ -24,10 +25,12 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import io.micrometer.core.instrument.LongTaskTimer; import io.micrometer.core.instrument.MockClock; import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.internal.DefaultLongTaskTimer; import io.micrometer.core.instrument.search.RequiredSearch; import io.micrometer.core.instrument.simple.SimpleConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -35,12 +38,14 @@ import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mockito; import reactor.core.Disposable; import reactor.core.Disposables; +import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; import reactor.test.AutoDisposingExtension; @@ -77,6 +82,80 @@ void aDotIsAddedToPrefix() { .allSatisfy(name -> assertThat(name).startsWith("noDot.")); } + @Test + @Tag("slow") + // see https://github.com/reactor/reactor-core/issues/3844 + void cancellationClearsPendingTasks() throws Exception { + TimedScheduler scheduler = (TimedScheduler) Micrometer.timedScheduler( + Schedulers.newSingle("single-test-scheduler"), + new SimpleMeterRegistry(), + "test"); + + AtomicLong totalCalls = new AtomicLong(); + AtomicLong errorCalls = new AtomicLong(); + + int iterations = 500_000; + + new Thread(() -> { + for (int i = 0; i < iterations; i++) { + Mono.delay(Duration.ofMillis(5)) + .subscribeOn(scheduler) + .timeout(Duration.ofMillis(20), scheduler) + .subscribe( + __ -> totalCalls.incrementAndGet(), + __ -> { + totalCalls.incrementAndGet(); + errorCalls.incrementAndGet(); + }); + } + }).start(); + + while (totalCalls.get() < iterations) { + Thread.sleep(100); + System.out.printf("Progress: %.1f\n", 100d / iterations * totalCalls.get()); + } + + scheduler.dispose(); + + System.out.println("Pending tasks: " + scheduler.pendingTasks.activeTasks()); + System.out.println("Error calls: " + errorCalls.get()); + + assertThat(scheduler.pendingTasks.activeTasks()).isEqualTo(0); + } + + @Test + // see https://github.com/reactor/reactor-core/issues/3844 + void disposeClearsPendingTasksInWorker() throws Exception { + TimedScheduler scheduler = (TimedScheduler) Micrometer.timedScheduler( + Schedulers.newSingle("ttt"), + new SimpleMeterRegistry(), + "test"); + + TimedScheduler.TimedWorker worker = (TimedScheduler.TimedWorker) scheduler.createWorker(); + + worker.schedule(() -> { + try { + System.out.println("First task run"); + Thread.sleep(1000); + } catch (InterruptedException e) { + System.out.println("First task interrupted"); + } + }); + + worker.schedule(() -> { + try { + System.out.println("Second task run"); + Thread.sleep(500); + } catch (InterruptedException e) { + System.out.println("Second task interrupted"); + } + }); + + worker.dispose(); + + assertThat(scheduler.pendingTasks.activeTasks()).isEqualTo(0); + } + @Test void constructorIgnoresDotAtEndOfMetricPrefix() { TimedScheduler test = new TimedScheduler(Schedulers.immediate(), registry, "dot.", Tags.empty()); From bcab229b08b265353192fcc25a086ac5b5f51c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 1 Aug 2024 16:22:55 +0200 Subject: [PATCH 305/312] Fix ThreadPerTaskScheduler busyness accounting (#3859) This change prevents the same `BoundedElasticThreadPerTaskScheduler` being picked up when the maximum number of Virtual Threads are already being executed in parallel. The consequence of improper busyness accounting was that tasks were executed sequentially instead of being run in parallel because the same `Worker` was being picked by operators. Resolves #3857 --- .../BoundedElasticThreadPerTaskScheduler.java | 4 +- ...ndedElasticThreadPerTaskSchedulerTest.java | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java index def20a1e0b..8cd0dd4533 100644 --- a/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java +++ b/reactor-core/src/main/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2023-2024 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -882,7 +882,7 @@ static long retain(SequentialThreadPerTaskExecutor instance) { static long incrementRefCnt(long state) { long rawRefCnt = state & REF_CNT_MASK; - return (rawRefCnt) == REF_CNT_MASK ? state : (rawRefCnt >> 31 + 1) << 31 | (state &~ REF_CNT_MASK); + return (rawRefCnt) == REF_CNT_MASK ? state : ((rawRefCnt >> 31) + 1) << 31 | (state &~ REF_CNT_MASK); } static long release(SequentialThreadPerTaskExecutor instance) { diff --git a/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java b/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java index d19dd4c7e6..38bcad2a56 100644 --- a/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java +++ b/reactor-core/src/test/java21/reactor/core/scheduler/BoundedElasticThreadPerTaskSchedulerTest.java @@ -32,6 +32,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -775,4 +777,82 @@ void ensuresTotalTasksMathIsDoneCorrectlyInEdgeCase() { // in the capacity counting since they don't occupy a queue Assertions.assertThat(scheduler.estimateRemainingTaskCapacity()).isEqualTo(10L * (Integer.MAX_VALUE / 10 + 1) - 1000 + 10); } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void allTasksExecuteInParallelWhenMaxProvided(boolean useWorker) throws Exception { + int total = 2 * Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE; + + BoundedElasticThreadPerTaskScheduler scheduler = newScheduler(total, total); + scheduler.init(); + + CountDownLatch latch = new CountDownLatch(total); + + try { + for (int i = 0; i < total; i++) { + Runnable task = () -> { + try { + latch.countDown(); + latch.await(); + } + catch (InterruptedException e) { + // ignore + } + }; + if (useWorker) { + scheduler.createWorker().schedule(task); + } else { + scheduler.schedule(task); + } + } + + Assertions.assertThat(latch.await(1, TimeUnit.SECONDS)).isTrue(); + } finally { + scheduler.dispose(); + } + } + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void allTasksExecuteInParallelWhenUsingDefaultMax(boolean useWorker) throws Exception { + int parallelTasks = Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE; + int total = 2 * parallelTasks; + + Scheduler scheduler = Schedulers.boundedElastic(); + scheduler.init(); + + CountDownLatch allScheduled = new CountDownLatch(1); + + CountDownLatch secondBatch = new CountDownLatch(parallelTasks); + + try { + for (int i = 0; i < total; i++) { + final int taskIndex = i; + Runnable task = () -> { + try { + System.out.println("Task #" + taskIndex); + allScheduled.await(); + if (taskIndex >= parallelTasks) { + secondBatch.countDown(); + secondBatch.await(); + } + } + catch (InterruptedException e) { + // ignore + } + }; + if (useWorker) { + scheduler.createWorker().schedule(task); + } else { + scheduler.schedule(task); + } + } + + allScheduled.countDown(); + + Assertions.assertThat(secondBatch.await(1, TimeUnit.SECONDS)).isTrue(); + } finally { + scheduler.dispose(); + } + } } \ No newline at end of file From bd75614c34c6e2b4d7fa51686e3f8f8f1e47cfaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Tue, 6 Aug 2024 11:21:23 +0200 Subject: [PATCH 306/312] [test] Disable asynchrony in OnDiscard bufferTimeout test (#3861) Recent test failures were observed where `bufferTimeout` case in `OnDiscardShouldNotLeakTest` failed occasionally due to flaky behaviour caused by the discard happening shortly after the assertion failed. The reason was a tiny delay which was not coordinated with the assertion. This change disables the asynchronous behaviour of the case. --- .../reactor/core/publisher/OnDiscardShouldNotLeakTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java index 245c4ddef6..20cc043ca6 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/OnDiscardShouldNotLeakTest.java @@ -157,7 +157,9 @@ public class OnDiscardShouldNotLeakTest { DiscardScenario.fluxSource("monoFilterWhenFalse", main -> main.last().filterWhen(__ -> Mono.just(false).hide())), DiscardScenario.fluxSource("last", main -> main.last(new Tracked("default")).flatMap(f -> Mono.just(f).hide())), DiscardScenario.fluxSource("flatMapIterable", f -> f.flatMapIterable(Arrays::asList)), - DiscardScenario.fluxSource("bufferTimeout", f -> f.bufferTimeout(2, Duration.ofMillis(1), true).flatMapIterable(Function.identity())), + // no asynchronicity is supported in these tests so long timeout is used to be + // effectively disabled in bufferTimeout test: + DiscardScenario.fluxSource("bufferTimeout", f -> f.bufferTimeout(2, Duration.ofDays(1), true).flatMapIterable(Function.identity())), DiscardScenario.fluxSource("publishOnDelayErrors", f -> f.publishOn(Schedulers.immediate())), DiscardScenario.fluxSource("publishOnImmediateErrors", f -> f.publishOn(Schedulers.immediate(), false, Queues.SMALL_BUFFER_SIZE)), DiscardScenario.fluxSource("publishOnAndPublishOn", main -> main From 0b9c683abf2b13d13da3422d4e95b0158c6cd53e Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 13 Aug 2024 09:45:33 +0300 Subject: [PATCH 307/312] [release] Prepare and release 3.5.20 --- README.md | 10 +++++----- gradle.properties | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9d763d23cf..c1f22196ee 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.5.19" - testCompile "io.projectreactor:reactor-test:3.5.19" + compile "io.projectreactor:reactor-core:3.5.20" + testCompile "io.projectreactor:reactor-test:3.5.20" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.5.20-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.5.20-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.5.21-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.5.21-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.5.19" + // implementation "io.projectreactor:reactor-tools:3.5.20" } ``` diff --git a/gradle.properties b/gradle.properties index 6521c802b3..e68870d156 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.20-SNAPSHOT -bomVersion=2022.0.21 -metricsMicrometerVersion=1.0.20-SNAPSHOT +version=3.5.20 +bomVersion=2022.0.22 +metricsMicrometerVersion=1.0.20 org.gradle.parallel=true From b164b339da62b6697434020ea28b0342c16754dc Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 13 Aug 2024 11:37:33 +0300 Subject: [PATCH 308/312] [release] Next development version 3.5.21-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index e68870d156..31906387ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.5.20 +version=3.5.21-SNAPSHOT bomVersion=2022.0.22 -metricsMicrometerVersion=1.0.20 +metricsMicrometerVersion=1.0.21-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5dee6ee24..1a11533bff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,9 +7,9 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.5.19" -baselinePerfCore = "3.5.19" -baselinePerfExtra = "3.5.1" +baseline-core-api = "3.5.20" +baselinePerfCore = "3.5.20" +baselinePerfExtra = "3.5.2" # Other shared versions asciidoctor = "4.0.3" From f33604e97ae807c85a9b1b6615aea0ff58f976c3 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 13 Aug 2024 11:48:26 +0300 Subject: [PATCH 309/312] [release] Prepare and release 3.6.9 --- README.md | 10 +++++----- gradle.properties | 6 +++--- gradle/libs.versions.toml | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1decace91f..3f643b45ba 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ repositories { } dependencies { - compile "io.projectreactor:reactor-core:3.6.8" - testCompile "io.projectreactor:reactor-test:3.6.8" + compile "io.projectreactor:reactor-core:3.6.9" + testCompile "io.projectreactor:reactor-test:3.6.9" // Alternatively, use the following for latest snapshot artifacts in this line - // compile "io.projectreactor:reactor-core:3.6.9-SNAPSHOT" - // testCompile "io.projectreactor:reactor-test:3.6.9-SNAPSHOT" + // compile "io.projectreactor:reactor-core:3.6.10-SNAPSHOT" + // testCompile "io.projectreactor:reactor-test:3.6.10-SNAPSHOT" // Optionally, use `reactor-tools` to help debugging reactor code - // implementation "io.projectreactor:reactor-tools:3.6.8" + // implementation "io.projectreactor:reactor-tools:3.6.9" } ``` diff --git a/gradle.properties b/gradle.properties index ea435ab52d..c7d2f9ec0f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.9-SNAPSHOT -bomVersion=2023.0.8 -metricsMicrometerVersion=1.1.9-SNAPSHOT +version=3.6.9 +bomVersion=2023.0.9 +metricsMicrometerVersion=1.1.9 org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 280cc98062..97e14c89b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,9 +14,9 @@ baselinePerfExtra = "3.5.1" # Other shared versions asciidoctor = "4.0.3" #note that some micrometer artifacts like context-propagation has a different version directly set in libraries below -micrometer = "1.12.8" +micrometer = "1.12.9" micrometerDocsGenerator = "1.0.3" -micrometerTracingTest="1.2.8" +micrometerTracingTest="1.2.9" contextPropagation="1.1.1" kotlin = "1.8.22" reactiveStreams = "1.0.4" From dcb33534a3a52e1e42955246dec269c60407ad2a Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 13 Aug 2024 12:27:40 +0300 Subject: [PATCH 310/312] [release] Next development version 3.6.10-SNAPSHOT --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 6 +++--- settings.gradle | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index c7d2f9ec0f..4d1dc4877d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=3.6.9 +version=3.6.10-SNAPSHOT bomVersion=2023.0.9 -metricsMicrometerVersion=1.1.9 +metricsMicrometerVersion=1.1.10-SNAPSHOT org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97e14c89b2..2372c2981b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,9 +7,9 @@ [versions] # Baselines, should be updated on every release -baseline-core-api = "3.6.8" -baselinePerfCore = "3.6.8" -baselinePerfExtra = "3.5.1" +baseline-core-api = "3.6.9" +baselinePerfCore = "3.6.9" +baselinePerfExtra = "3.5.2" # Other shared versions asciidoctor = "4.0.3" diff --git a/settings.gradle b/settings.gradle index f018a6376e..0204913f93 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,9 +25,9 @@ dependencyResolutionManagement { versionCatalogs { libs { if (System.getProperty("useSnapshotMicrometerVersion")) { - version('micrometer', '1.12.9-SNAPSHOT') + version('micrometer', '1.12.10-SNAPSHOT') version('micrometerDocsGenerator', "1.0.4-SNAPSHOT") - version('micrometerTracingTest', "1.2.9-SNAPSHOT") + version('micrometerTracingTest', "1.2.10-SNAPSHOT") version('contextPropagation', "1.1.2-SNAPSHOT") } } From 5a462d6da786f67ec028cca93c5839487d6cc504 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Tue, 13 Aug 2024 20:06:44 +0300 Subject: [PATCH 311/312] Update dependabot's target branch to 3.6.x --- .github/dependabot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e123311bcb..e3b6eacc2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: reviewers: - "reactor/core-team" # updates in the oldest maintenance branch, we'll forward-merge up to main - target-branch: "3.4.x" + target-branch: "3.6.x" ignore: # JSR166 backport is fixed - dependency-name: "io.projectreactor:jsr166" @@ -59,4 +59,4 @@ updates: reviewers: - "reactor/core-team" # updates in oldest maintenance branch, we'll forward-merge up to main - target-branch: "3.4.x" \ No newline at end of file + target-branch: "3.6.x" From 4f46a1e1efb0c855880eadc121780c59a7c7b373 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:10:30 +0000 Subject: [PATCH 312/312] Bump gradle/actions from 3.5.0 to 4.0.0 in /.github/workflows Bumps [gradle/actions](https://github.com/gradle/actions) from 3.5.0 to 4.0.0. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/d9c87d481d55275bb5441eef3fe0e46805f9ef70...af1da67850ed9a4cedd57bfd976089dd991e2582) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 6 +++--- .github/workflows/full.yml | 2 +- .github/workflows/gradle-wrapper-validation.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/snapshots.yml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bbaddba82..875ace8588 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,12 +32,12 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # tag=v3 + - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # tag=v3 name: spotless (license header) if: always() with: arguments: spotlessCheck -PspotlessFrom=origin/${{ github.base_ref }} - - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # tag=v3 + - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # tag=v3 name: api compatibility if: always() with: @@ -97,7 +97,7 @@ jobs: with: distribution: 'temurin' java-version: 8 - - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # tag=v3 + - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # tag=v3 name: Run Gradle Tests with: arguments: ${{ matrix.test-type.arguments }} diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index d6c4e8dc66..179b6690e0 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -48,6 +48,6 @@ jobs: distribution: 'temurin' java-version: 8 - name: Run Gradle Tests - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # tag=v3 + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # tag=v3 with: arguments: ${{ matrix.test-type.arguments }} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 049e52964b..6ff7554578 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -7,4 +7,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag=v4 - - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # tag=v3 + - uses: gradle/actions/wrapper-validation@af1da67850ed9a4cedd57bfd976089dd991e2582 # tag=v3 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 57766a9132..f39314722b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -60,7 +60,7 @@ jobs: run: ./gradlew qualifyVersionGha - name: run checks id: checks - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # tag=v3 + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # tag=v3 with: arguments: ${{ matrix.test-type.arguments }} #deploy the snapshot artifacts to Artifactory diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml index 16c1745f94..18f16e8cc1 100644 --- a/.github/workflows/snapshots.yml +++ b/.github/workflows/snapshots.yml @@ -46,6 +46,6 @@ jobs: distribution: 'temurin' java-version: 8 - name: Run Gradle Tests - uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # tag=v3 + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # tag=v3 with: arguments: ${{ matrix.test-type.arguments }} \ No newline at end of file