Skip to content

Compliance with Reactive Streams specs#7191

Open
davidh44 wants to merge 3 commits into
masterfrom
hdavidh/fix-tck-tests
Open

Compliance with Reactive Streams specs#7191
davidh44 wants to merge 3 commits into
masterfrom
hdavidh/fix-tck-tests

Conversation

@davidh44

@davidh44 davidh44 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

Reactive Streams TCK tests are TestNG-based, so Surefire only runs them when the surefire-testng provider is configured. Only core/sdk-core and http-clients/netty-nio-client had it. As a result, ~20 TCK tests across four other modules were silently skipped — they compiled and looked like they ran, but Surefire never executed them (no report, no "Tests run" line).

Modifications

1. Moved TCK/TestNG surefire configuration to the parent pom.xml

Previously, each module with TCK tests needed to independently declare:

  • reactive-streams-tck as a test dependency
  • surefire-testng as a surefire provider dependency

This was error-prone — four modules had the TCK dependency but not the provider, silently skipping their tests.

Now the parent POM declares reactive-streams-tck as a global test dependency (which brings TestNG transitively) and configures both surefire-junit-platform and surefire-testng providers. Any module with a *TckTest.java file will have it automatically discovered and executed. The junit=false TestNG property prevents double-execution of JUnit tests.

Removed the per-module reactive-streams-tck dependency and surefire plugin overrides from: utils, core/sdk-core, core/http-auth-aws, http-clients/netty-nio-client, services/s3, services-custom/s3-transfer-manager. Also removed the unused reactive-streams-tck dependency from http-client-spi and http-clients/aws-crt-client (these had the dependency declared but no actual TCK tests or surefire-testng provider).

2. utilsSimplePublisher (Reactive Streams rules 3.13 and 1.03)

Rule 3.13: SimplePublisher never released its reference to the subscriber after a terminal event, so the TCK's required_spec313 (publisher must drop subscriber references after cancellation) failed. Now the subscriber field is set to null after onComplete, onError, and cancel. A separate subscribed AtomicBoolean was introduced to keep enforcing the single-subscription guard (previously done by null-checking subscriber), and panicAndDie guards against the now-nullable field.

This one fix resolved 4 TCK failures, because OutputStreamPublisher and InputStreamConsumingPublisher (utils) and SigV4DataFramePublisher (http-auth-aws) all delegate to SimplePublisher.

Rule 1.03: When request(n) was called from within the subscriber's onSubscribe, processEventQueue() would immediately deliver onNext before onSubscribe returned — violating the rule that signals must not be invoked concurrently. Added a volatile boolean onSubscribeInProgress flag that suppresses signal delivery until onSubscribe completes. The processEventQueue() call after the flag is cleared picks up any pending work.

3. core/http-auth-awsChunkedEncodedPublisherTckTest

26 failures were a test defect, not a production bug: the test never called .contentLength(...), which the builder requires. Production always sets it via the x-amz-decoded-content-length header (AwsChunkedV4PayloadSigner / AwsChunkedV4aPayloadSigner), so the Validate.notNull guard is correct. Fixed the test to pass a contentLength matching the data it produces.

4. services-custom/s3-transfer-managerAsyncBufferingSubscriber

Two TCK Subscriber-rule violations:

  • Rule 2.13 (spec213): onNext(null) must throw NullPointerException. Added a null check on the incoming element.
  • Rule 2.3 (spec203): a Subscriber must not call Subscription.cancel() from within onError. The returnFuture.whenComplete handler cancelled the subscription whenever the future failed — including when onError itself failed it (synchronously, from within onError). Added an onErrorInvoked flag so the handler skips the subscription cancel when the failure originated from onError (upstream has already terminated, so the cancel was redundant anyway). External-abort cancellation is unchanged.

The existing unit test consumerFunctionThrows_shouldCancelSubscriptionAndCompleteFutureExceptionally asserted cancel() was called twice — the second call being the rule-2.3 violation. Updated it to expect a single cancel (from the onNext catch block); the subscription is still cancelled and the future still completes exceptionally.

Testing

Newly-enabled TCK tests pass across all affected modules.

@davidh44
davidh44 requested a review from a team as a code owner July 24, 2026 16:59
@davidh44 davidh44 added changelog-not-required Indicate changelog entry is not required for a specific PR no-api-surface-area-change Indicate there is no API surface area change and thus API surface area review is not required labels Jul 24, 2026
@davidh44
davidh44 requested a review from zoewangg July 24, 2026 18:37
// would call Subscription::cancel from within onError (Reactive Streams rule 2.3). Still cancel on an
// external abort.
if (!onErrorInvoked) {
synchronized (this) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could there be a race condition where onErrorInvoked turns to true (onError invoked) right after the check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, Rule 2.3 only prohibits calling cancel() from within the onError call stack (same thread). That's the case this flag guards: onError → sets flag → calls completeExceptionally() → handler runs inline on the same thread → sees flag → skips cancel. In the scenario you're describing (different thread sets the flag after the check), the handler is running because of an external abort, not because of onError — so calling cancel() there is correct and not a rule 2.3 violation.

@@ -317,6 +340,11 @@ private boolean shouldProcessQueueEntry(QueueEntry<T> entry) {
return false;
}

if (onSubscribeInProgress) {
// Do not deliver signals until onSubscribe has returned (Reactive Streams rule 1.03).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to understand this: how could processEventQueue be invoked if onSubscribeInProgress?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscriber calls request(n) from within onSubscribe (rule 2.7 allows this, and most subscribers do it to signal initial demand). request(n)processEventQueue(). Without this flag, if data was already queued, it would deliver onNext before onSubscribe returns — violating rule 1.03. A concurrent send() from another thread can also trigger processEventQueue() during onSubscribe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog-not-required Indicate changelog entry is not required for a specific PR no-api-surface-area-change Indicate there is no API surface area change and thus API surface area review is not required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants