Skip to content

test(spanner): unflake multiUseReadOnlyTransactionCanUseInlineBeginForReadAsync - #14329

Merged
sakthivelmanii merged 1 commit into
mainfrom
deflake-session-impl-test
Sep 10, 2026
Merged

test(spanner): unflake multiUseReadOnlyTransactionCanUseInlineBeginForReadAsync#14329
sakthivelmanii merged 1 commit into
mainfrom
deflake-session-impl-test

Conversation

@sakthivelmanii

@sakthivelmanii sakthivelmanii commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

In SessionImplTest#multiUseReadOnlyTransactionCanUseInlineBeginForReadAsync, mock rpc.read invoked stream callbacks synchronously before returning StreamingCall. This caused a background thread to process rows and invoke startStream while this.stream was still unassigned, triggering a duplicate request (expected:<1> but was:<2>).

Fix this deterministically by delaying consumer.onPartialResultSet and consumer.onCompleted until StreamingCall.request(int) is called. In gRPC streaming, this.stream is guaranteed to be assigned before request() is executed, eliminating the race condition without relying on background threads or executors.

@sakthivelmanii
sakthivelmanii requested review from a team as code owners September 9, 2026 11:46

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request updates a unit test in SessionImplTest.java to execute the mocked RPC result stream consumer asynchronously using CompletableFuture.runAsync. The review feedback recommends adding exception handling to the asynchronous block to prevent unhandled exceptions from being silently swallowed, which would make debugging test failures difficult.

Comment on lines +828 to +832
CompletableFuture.runAsync(
() -> {
c.onPartialResultSet(resultSet);
c.onCompleted();
});

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.

medium

Using CompletableFuture.runAsync without an explicit executor defaults to the system-wide ForkJoinPool.commonPool(). In resource-constrained CI environments (e.g., single-core containers), this can lead to execution delays or starvation, potentially causing test flakiness.

Additionally, any unhandled exceptions thrown inside the async block (such as assertion failures or unexpected null pointers) will be silently swallowed by the CompletableFuture unless explicitly handled, making debugging difficult.

Consider adding exception handling to ensure any failures during the async execution are logged or propagated.

              CompletableFuture.runAsync(
                      () -> {
                        c.onPartialResultSet(resultSet);
                        c.onCompleted();
                      })
                  .exceptionally(
                      t -> {
                        t.printStackTrace();
                        return null;
                      });

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.

Let the test fail if there's an exception

@sakthivelmanii
sakthivelmanii force-pushed the deflake-session-impl-test branch from fb03b74 to d4b9fff Compare September 9, 2026 14:15
return AsyncResultSet.CallbackResponse.CONTINUE;
})
.get(5, TimeUnit.SECONDS);
ExecutorService executor = Executors.newSingleThreadExecutor();

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.

I don't think this really fixes the possibility that this test could flake. It does reduce the probability, but it does not rule it out. I think that this would be a better way to implement this test:

@Test
public void multiUseReadOnlyTransactionCanUseInlineBeginForReadAsync() throws Exception {
  PartialResultSet resultSet = inlineBeginResultSet("async-inline-tx");
  final ArgumentCaptor<SpannerRpc.ResultStreamConsumer> consumerCaptor =
      ArgumentCaptor.forClass(SpannerRpc.ResultStreamConsumer.class);
  final ArgumentCaptor<ReadRequest> requestCaptor = ArgumentCaptor.forClass(ReadRequest.class);

  Mockito.when(rpc.read(requestCaptor.capture(), consumerCaptor.capture(), anyMap(), any(), eq(false)))
      .then(
          invocation -> {
            SpannerRpc.ResultStreamConsumer consumer = invocation.getArgument(1);
            return new SpannerRpc.StreamingCall() {
              private final AtomicBoolean requested = new AtomicBoolean();

              @Override
              public void request(int numMessages) {
                if (requested.compareAndSet(false, true)) {
                  consumer.onPartialResultSet(resultSet);
                  consumer.onCompleted();
                }
              }

              @Override
              public void cancel(@Nullable String message) {}
            };
          });

  try (ReadOnlyTransaction transaction = inlineReadOnlyTransaction()) {
    try (AsyncResultSet resultSetAsync =
        transaction.readAsync("Dummy", KeySet.all(), Collections.singletonList("C"))) {
      resultSetAsync
          .setCallback(
              Runnable::run,
              asyncResultSet -> {
                while (asyncResultSet.tryNext() == AsyncResultSet.CursorState.OK) {}
                return AsyncResultSet.CallbackResponse.CONTINUE;
              })
          .get(5, TimeUnit.SECONDS);
    }
    assertEquals(
        Timestamp.fromProto(Timestamps.parse("2015-10-01T10:54:20.021Z")),
        transaction.getReadTimestamp());
  }

  Mockito.verify(rpc, Mockito.never()).beginTransaction(Mockito.any(), anyMap(), eq(false));
  assertEquals(1, requestCaptor.getAllValues().size());
  assertTrue(requestCaptor.getValue().getTransaction().hasBegin());
}

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.

Done. Updated to trigger consumer callbacks inside StreamingCall.request(int).

@sakthivelmanii
sakthivelmanii force-pushed the deflake-session-impl-test branch from d4b9fff to 1058a7b Compare September 9, 2026 16:45
…rReadAsync

In `SessionImplTest#multiUseReadOnlyTransactionCanUseInlineBeginForReadAsync`,
mock `rpc.read` invoked stream callbacks synchronously before returning the
`StreamingCall`. This caused a background thread to process rows and invoke
`startStream` while `this.stream` was still unassigned, triggering a duplicate
request (`expected:<1> but was:<2>`).

Fix this deterministically by delaying `consumer.onPartialResultSet` and
`consumer.onCompleted` until `StreamingCall.request(int)` is invoked. In gRPC
streaming, `this.stream` is guaranteed to be assigned before `request()` is
called, eliminating the race condition without relying on background thread
executors.
@sakthivelmanii
sakthivelmanii force-pushed the deflake-session-impl-test branch from 1058a7b to 3a8aed3 Compare September 9, 2026 17:28
@sakthivelmanii
sakthivelmanii merged commit d76d159 into main Sep 10, 2026
206 checks passed
@sakthivelmanii
sakthivelmanii deleted the deflake-session-impl-test branch September 10, 2026 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants