Skip to content

fix: rollback transactions that are waiting for tx-id to be returned#4342

Merged
olavloite merged 1 commit intomainfrom
fix-orphaned-tx-rollback
Feb 12, 2026
Merged

fix: rollback transactions that are waiting for tx-id to be returned#4342
olavloite merged 1 commit intomainfrom
fix-orphaned-tx-rollback

Conversation

@olavloite
Copy link
Collaborator

If a transaction has been started by an async query, and the transaction is closed before the async query has returned the first results and the transaction ID, the transaction would not be rolled back by the client. This would cause locks to be held for longer than they should.

This fix adds a check whether the transaction has already sent a request that will start the transaction, and if so, it will add a callback that will rollback the transaction when the transaction ID is returned.

If a transaction has been started by an async query, and the transaction is closed
before the async query has returned the first results and the transaction ID, the
transaction would not be rolled back by the client. This would cause locks to be held
for longer than they should.

This fix adds a check whether the transaction has already sent a request that will
start the transaction, and if so, it will add a callback that will rollback the
transaction when the transaction ID is returned.
@olavloite olavloite requested a review from a team as a code owner February 12, 2026 09:39
@olavloite olavloite requested a review from a team February 12, 2026 09:39
@product-auto-label product-auto-label bot added size: m Pull request size is medium. api: spanner Issues related to the googleapis/java-spanner API. labels Feb 12, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @olavloite, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical issue where asynchronous transactions might not be properly rolled back if they are closed before the transaction ID is returned by the server. The change introduces a mechanism to defer the rollback operation until the transaction ID is resolved, ensuring that all initiated transactions are eventually cleaned up. This prevents potential resource leaks and improves the robustness of the transaction management system.

Highlights

  • Transaction Rollback Logic: Modified the rollbackAsync method in TransactionRunnerImpl to proactively handle transactions that have initiated an async query but have not yet received a transaction ID. This ensures that such transactions are properly rolled back once their ID becomes available, preventing orphaned transactions and prolonged lock holding.
  • New Test Cases for Orphaned Transactions: Added a new test file, OrphanedTransactionTest.java, which includes two test cases. These tests specifically verify that transactions are correctly rolled back even when closed before a transaction ID is returned from an async query, and that no rollback occurs if the initial query fails.
Changelog
  • google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java
    • Updated the rollbackAsync method to include logic for handling pending transaction IDs, ensuring a rollback is attempted once the ID is available.
  • google-cloud-spanner/src/test/java/com/google/cloud/spanner/OrphanedTransactionTest.java
    • Added a new test file to validate the correct rollback behavior of asynchronous transactions that are closed before receiving a transaction ID.
    • Included a test case for orphaned transactions that successfully initiate a query.
    • Included a test case for orphaned transactions where the initial query fails.
Activity
  • No specific activity (comments, reviews, or progress updates) has been recorded for this pull request since its creation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

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

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 introduces a fix to ensure that transactions waiting for a transaction ID are properly rolled back when closed. The changes in TransactionRunnerImpl.java correctly add logic to handle this scenario by waiting for the transaction ID future to complete before issuing a rollback. The new test file, OrphanedTransactionTest.java, provides good coverage for both the success and failure paths of this new logic. I've identified one area for improvement in the implementation.

Comment on lines +626 to +634
transactionId ->
transactionId.isEmpty()
? ApiFutures.immediateFuture(Empty.getDefaultInstance())
: rpc.rollbackAsync(
RollbackRequest.newBuilder()
.setSession(session.getName())
.setTransactionId(transactionId)
.build(),
getTransactionChannelHint()),
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The session should be marked as used when a rollback is initiated. This is consistent with other RPC calls in this class and is important for the session pool to correctly manage session liveness, preventing premature eviction of active sessions.

transactionId -> {
  if (transactionId.isEmpty()) {
    return ApiFutures.immediateFuture(Empty.getDefaultInstance());
  }
  session.markUsed(clock.instant());
  return rpc.rollbackAsync(
      RollbackRequest.newBuilder()
          .setSession(session.getName())
          .setTransactionId(transactionId)
          .build(),
      getTransactionChannelHint());
},

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The session.markUsed call is redundant, now that all operations use multiplexed sessions. I will open a follow-up PR to remove all calls to this, as it does not mean anything in the current setup.

session.markUsed(clock.instant());
return apiFuture;
} else if (transactionIdFuture != null) {
ApiFuture<ByteString> transactionIdOrEmptyFuture =
Copy link
Collaborator

@sakthivelmanii sakthivelmanii Feb 12, 2026

Choose a reason for hiding this comment

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

1. Do we have to update the session last use time?
2. Can we move this entire into a new function? Previously we used to have 2 condition.
1. transaction id is present
2. transaction id is null
the fix which we are implementing, we are waiting for beginTransaction RPC, once we get it the response back, based on the success / failure, we are trying to do what we were doing before(if transaction id is there then rollback, else do nothing). Moving this a function and reusing the code might be better.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do we have to update the session last use time?

No, see also my response to Gemini above :-)

Regarding the second point: I don't think there is much code to be shared, as the implementations are relatively different. There are two possible scenarios:

  1. There is a transaction ID: In that case this.transactionId is not null. How it has been set (e.g. returned by an explicit BeginTransaction, returned by a statement with an inlined-begin, or set in the constructor) is not important. The only important thing is that it is there.
  2. There is no transaction ID, but this.transactionIdFuture is not null: This means that we are waiting for a transaction ID to be returned, either by an inlined-begin, or by a BeginTransaction RPC. We don't wait for it to be returned, but we add a callback to the future. If the future returns a result, then we use that to execute an async Rollback.

I think that the only way that we could share the logic for the two above scenarios, would be by wrapping this.transactionId in something like an ApiFutures.immediateFuture(..) and then pass that Future in to the method that handles scenario 2. But I'm not sure that simplifies the overall code (and it does add a bit of extra overhead).

Copy link
Collaborator

@sakthivelmanii sakthivelmanii Feb 12, 2026

Choose a reason for hiding this comment

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

I was thinking of the following approach.

public void waitForTransactionId() {
  if (transactionId != null || transactionIdFuture == null) {
    return;
  }
  try {
    transactionId = SpannerApiFutures.get(transactionIdFuture);
  } catch (SpannerException ignored) {
  }
}

and just call waitForTransactionId before close() in rollbackAsync().

I thought of this way. Maybe if you see any issues, we can proceed with the current approach itself. I am approving the PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That would make rollbackAsync() potentially blocking, which is not something that we want.

@olavloite olavloite merged commit 866a8c2 into main Feb 12, 2026
33 of 34 checks passed
@olavloite olavloite deleted the fix-orphaned-tx-rollback branch February 12, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the googleapis/java-spanner API. size: m Pull request size is medium.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants