Skip to content

Commit 02506d4

Browse files
fix(spanner): prevent statement cancellation race in AbstractBaseUnitOfWork
In `AbstractBaseUnitOfWork.executeStatementAsync`, statements are submitted asynchronously to `statementExecutor` before `currentlyRunningStatementFuture` is assigned. Under thread contention, the query RPC can reach the server and trigger `cancel()` before the future is stored, causing `cancel()` to silently drop the cancellation request and resulting in statement timeouts in CI tests such as `StatementTimeoutTest#testCancelReadWriteAutocommitMultipleStatements`. Fix this by tracking cancellation intent via an `isCancelled` flag guarded by `this`, setting it in `cancel()`, and immediately cancelling the future upon registration if cancellation was already requested.
1 parent 89a8268 commit 02506d4

1 file changed

Lines changed: 9 additions & 1 deletion

File tree

‎java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ public String getMessage() {
105105
@GuardedBy("this")
106106
private volatile Future<?> currentlyRunningStatementFuture = null;
107107

108+
@GuardedBy("this")
109+
private boolean isCancelled = false;
110+
108111
enum InterceptorsUsage {
109112
INVOKE_INTERCEPTORS,
110113
IGNORE_INTERCEPTORS
@@ -277,6 +280,7 @@ StatementTimeout getStatementTimeout() {
277280
@Override
278281
public void cancel() {
279282
synchronized (this) {
283+
this.isCancelled = true;
280284
if (currentlyRunningStatementFuture != null
281285
&& !currentlyRunningStatementFuture.isDone()
282286
&& !currentlyRunningStatementFuture.isCancelled()) {
@@ -419,14 +423,18 @@ public <ReqT, RespT> ApiCallContext configure(
419423
MoreExecutors.directExecutor());
420424
synchronized (this) {
421425
this.currentlyRunningStatementFuture = future;
426+
if (this.isCancelled) {
427+
future.cancel(true);
428+
}
422429
}
423430
future.addListener(
424431
new Runnable() {
425432
@Override
426433
public void run() {
427-
synchronized (this) {
434+
synchronized (AbstractBaseUnitOfWork.this) {
428435
if (currentlyRunningStatementFuture == future) {
429436
currentlyRunningStatementFuture = null;
437+
isCancelled = false;
430438
}
431439
}
432440
if (isSingleUse()) {

0 commit comments

Comments
 (0)