Current scope
Implement a logical read/write transaction deadline distinct from STATEMENT_TIMEOUT and user-idle timeout (#357). The old SET LOCAL prerequisite #400 is complete. The existing owner/attempt separation from SAVEPOINT must retain one budget across replacement.
Contract to implement
-
TRANSACTION_TIMEOUT accepts a duration or NULL; zero/NULL means no additional transaction deadline. Snapshot the configured duration for a logical transaction.
-
Define the clock explicitly: start at its first actual database operation, not a client-only BEGIN/SHOW, while preserving the originally selected duration. Do not reset it on a later statement, ABORTED retry or ROLLBACK TO.
-
A pending SET LOCAL may select the duration before first database use; reject attempts to extend/change the budget after activation. Session SET after BEGIN applies to a later logical owner, not the current one.
-
Every SQL/Batch DML/commit/replay RPC receives the minimum of caller, statement and remaining transaction deadline. A client-only SHOW must not activate or renew it.
-
On expiry cancel/retire the matching logical owner, stop its heartbeat and restore SET LOCAL exactly once. No stale timer may cancel a replacement owner.
Design clarification (2026-09-14)
- Verify against the current commit and pinned SDK, not a stale coordinator checkout. At the audited baseline, mycli uses Go Spanner v1.95.0 ReadWriteStmtBasedTransaction. Its default constructor normally sends an explicit BeginTransaction RPC; generic pending BEGIN is client-only. The statement-based wrapper overrides the embedded callback-based transaction's begin decision. Start the budget before the actual transaction RPC, including constructor BeginTransaction when applicable. Merely taking a multiplexed session or buffering DML without a transaction RPC does not start it. A buffered-DML route that activates the owner first can nevertheless already have issued BeginTransaction.
- A deadline must cancel an in-flight RPC without first waiting for TransactionManager.mu: some existing RPC paths hold that mutex. Retire only the matching logical owner under the mutex after cancellation. A captured owner pointer and unchanged total budget must survive physical attempt replacement.
- Timer goroutines must not mutate the live registry through Registry.Set. Detach the owner's LOCAL undo for restoration at a serialized session/CLI safe point. Drain expired-owner undo before a later statement reads execution defaults or creates a new owner, and on Close; the ordinary end-of-statement defer also handles an in-flight operation returning after cancellation. Document this safe-point restoration boundary. A naked inExec boolean, even atomic, is not an atomic check-and-restore protocol.
- Test cancellation of an RPC while the transaction mutex is held, idle expiry followed immediately by BEGIN for a new owner, explicit-constructor failure, delayed old-owner callbacks, and first activation through automatic DML. Prove errors/ownership, not merely elapsed time. Preserve the existing failed-construction and SAVEPOINT recovery invariants.
Acceptance
References: Go transaction timeout, Java property, mycli session_transaction_context.go and transaction_manager.go.
Verification and delivery
Plan refreshed on 2026-09-14 against a8fa2418b65af9b889cf92e02190366b3e6e02ea. This body supersedes obsolete implementation suggestions in earlier comments; history is retained. Tracked with #47.
For each implementation PR, capture focused test output to a file and check the exit code, then run make check before pushing; run make check-race for transaction concurrency/lifecycle changes. Keep the coverage gate at 80%. Update user docs and the compatibility matrix with the resulting behavior. No database/sql migration or unrelated dependency upgrade.
Current scope
Implement a logical read/write transaction deadline distinct from STATEMENT_TIMEOUT and user-idle timeout (#357). The old SET LOCAL prerequisite #400 is complete. The existing owner/attempt separation from SAVEPOINT must retain one budget across replacement.
Contract to implement
TRANSACTION_TIMEOUT accepts a duration or NULL; zero/NULL means no additional transaction deadline. Snapshot the configured duration for a logical transaction.
Define the clock explicitly: start at its first actual database operation, not a client-only BEGIN/SHOW, while preserving the originally selected duration. Do not reset it on a later statement, ABORTED retry or ROLLBACK TO.
A pending SET LOCAL may select the duration before first database use; reject attempts to extend/change the budget after activation. Session SET after BEGIN applies to a later logical owner, not the current one.
Every SQL/Batch DML/commit/replay RPC receives the minimum of caller, statement and remaining transaction deadline. A client-only SHOW must not activate or renew it.
On expiry cancel/retire the matching logical owner, stop its heartbeat and restore SET LOCAL exactly once. No stale timer may cancel a replacement owner.
Design clarification (2026-09-14)
Acceptance
Tests cover pending idle time, cumulative statements, long/streamed reads, automatic DML, commit, replay, shorter caller context and cancellation.
Expiry ends the correct owner and retained attempts cannot restart its budget; no fixed sleeps are used as synchronization.
Document distinction from Expire user-idle transactions independently of keepalive traffic #357 and interaction with Implement bounded ABORTED retries using the SAVEPOINT replay machinery #293.
References: Go transaction timeout, Java property, mycli session_transaction_context.go and transaction_manager.go.
Verification and delivery
Plan refreshed on 2026-09-14 against
a8fa2418b65af9b889cf92e02190366b3e6e02ea. This body supersedes obsolete implementation suggestions in earlier comments; history is retained. Tracked with #47.For each implementation PR, capture focused test output to a file and check the exit code, then run
make checkbefore pushing; runmake check-racefor transaction concurrency/lifecycle changes. Keep the coverage gate at 80%. Update user docs and the compatibility matrix with the resulting behavior. No database/sql migration or unrelated dependency upgrade.