fix: harden txn client cancellation and cleanup#25643
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
0df6d30 to
9317934
Compare
|
Blocking The latest head still leaves one admission-lifecycle hole in Concrete failure path:
Result: I verified this from the code path and with a deterministic scratch repro in a throwaway worktree by pausing exactly between Suggested fix: snapshot-acquisition failure for a queued txn needs to participate in the same admission ownership state machine as wait-active cancellation. Either make the failure path mark the waiter non-promotable / non-owning before |
|
Addressed the new admission blocker in The finding was valid. The previous state machine was scoped too narrowly to cancellation inside The fix is structural rather than a snapshot-specific branch:
The deterministic regression holds B in snapshot acquisition, lets A's close claim/dequeue B, blocks B's active-map publication at its shard, then cancels B. It proves B cannot return or emit its sole ClosedEvent before publication and finally verifies Reflection: the core state-machine direction was right, but its abstraction boundary and the review method were wrong. I modeled a Fresh validation after the final edit:
|
|
Reviewed latest head The previous blocker around queued snapshot-acquisition failure racing with claimed promotion now looks fixed. What changed in the right direction:
I re-checked the ownership closure in |
|
Double-checked the latest head One reproducible blocker remains in the unrelated skill/tooling commit included in this PR: [P2] The new
|
LeftHandCold
left a comment
There was a problem hiding this comment.
Re-reviewed the latest head. The previous txn lifecycle blockers are fixed: the terminal gate remains sealed after close, queued admission promotion now has a linearized owner, every post-open failure crosses the same admission barrier, and legacy token 0 behavior is preserved. The full txn/client suite, the full race suite, and the focused concurrency regressions pass locally.
One P2 remains in the tooling included in this PR: the new CGo wrapper's Linux external link contract omits CRoaring. Since that wrapper is documented as the supported way to test CGo-transitive packages, the included tooling should be fixed (or split out) before merge.
|
|
||
| # go test executables run from temporary build directories. Use repository | ||
| # absolute rpaths; package-relative rpaths belong to distributable binaries. | ||
| external_flags="$platform_link_flags -L$cgo_dir -lmo -L$lib_dir -Wl,-rpath,$cgo_dir -Wl,-rpath,$lib_dir" |
There was a problem hiding this comment.
[P2] Please include libmo's CRoaring dependency in the Linux external link graph. This wrapper unconditionally injects -lmo, but a CGo-transitive target such as ./pkg/txn/client does not import cgo/lib.go and therefore does not inherit its -lroaring directive. On Linux the documented wrapper command consequently reaches the final link with unresolved roaring64_bitmap_* symbols from libmo.so. Adding -lroaring after -L$lib_dir makes that link succeed; alternatively, avoid forcing -lmo for packages that do not need it. Please also cover this with a Linux smoke test of a CGo-transitive package, since the current green CI does not execute the wrapper.
There was a problem hiding this comment.
Fixed in 6ba1a22f28. The wrapper now appends the authoritative cgo package CgoLDFLAGS after -lmo, closing both current and future ordered CPU native dependencies without maintaining a second hard-coded list.
Coverage is now part of the existing CPU Linux UT path through a minimal CGo-transitive fixture that references libmo without importing cgo/lib.go. A Docker Linux/arm64 rebuild reproduced the old roaring64_bitmap_* failure and passed with the new wrapper; macOS ordinary/race smoke and full pkg/txn/client ordinary/race tests also passed.
|
Re-reviewed latest head The previous blockers are closed: queued admission promotion is linearized, all post- I also verified the |
aptend
left a comment
There was a problem hiding this comment.
Approved after re-reviewing latest head 37797246e5. The previously requested lifecycle and admission fixes are complete, and I found no remaining merge-blocking issue.
gouhongshen
left a comment
There was a problem hiding this comment.
Request changes — independent re-review of head 3779724.
发现两个仍需合并前处理的 P2:
-
两个导出 API 的签名被直接改写:pkg/cdc.EnterRunSql 从 func(...) func() 变为 func(...) (func(), error),compile.MarkQueryRunning 从无返回值变为返回 error。PR 的 API-change 仍未勾选;仓外调用方会直接 source break。请保留旧 API,并通过新增 Try/WithError API 传递 admission error,或明确声明并按 API 变更流程处理。
-
新增 CGo wrapper 的链接图不完整:它在第 99 行无条件注入 -lmo,但没有注入 -lroaring;而 cgo/Makefile 和 cgo/lib.go 都明确把 libmo 与 CRoaring 链接。Linux 上按该 wrapper 运行 CGo-transitive target 时,最终链接可能出现 roaring64_bitmap_* 未解析。请补齐 -lroaring 并检查对应前置库,同时同步 reference 命令,或避免对不需要 libmo 的 target 强制链接。
本地验证当前 head:pkg/txn/client 普通测试、race、go vet、git diff --check 均通过;cdc/compile/frontend 的 CGo 编译受本机缺失 roaring/usearch/xxhash headers 阻塞。
| } | ||
|
|
||
| var EnterRunSql = func(ctx context.Context, txnOp client.TxnOperator, sql string) func() { | ||
| var EnterRunSql = func(ctx context.Context, txnOp client.TxnOperator, sql string) (func(), error) { |
There was a problem hiding this comment.
[P2] Preserve the exported EnterRunSql API. This PR changes pkg/cdc.EnterRunSql from func(...) func() to func(...) (func(), error), which is a source-breaking change for external callers while API-change is not declared. Keep the old symbol and add an additive error-returning helper, or explicitly version/document the API break.
There was a problem hiding this comment.
Fixed in 6ba1a22f28 with an additive API. cdc.EnterRunSql retains the original func(...) func() type and preserves opaque legacy tokens, including zero. The CDC production path now uses the new TryEnterRunSql(...) (func(), error) helper for rejection-aware admission.
Compile-time signature assertions and a legacy zero-token cleanup regression lock the old contract; the sealed-transaction regression covers the new path. Focused race -count=50, full ordinary/race pkg/cdc, full pkg/frontend, vet, and make build passed.
| } | ||
|
|
||
| func MarkQueryRunning(c *Compile, txn txnClient.TxnOperator) { | ||
| func MarkQueryRunning(c *Compile, txn txnClient.TxnOperator) error { |
There was a problem hiding this comment.
[P2] Preserve the exported MarkQueryRunning API. Its return type changed from no result to error, so external callers no longer compile. Please keep the existing signature and expose the rejection-aware path additively, or explicitly declare this API break.
There was a problem hiding this comment.
Fixed in 6ba1a22f28 with an additive API. MarkQueryRunning(c, txn) keeps its original no-result signature and legacy non-rejecting behavior. New internal callers that must observe admission rejection use TryMarkQueryRunning(c, txn) error. Both share one validate-then-publish implementation without per-call closures.
Compile-time signature assertions plus runtime tests cover the legacy token-0 path and the rejection path's rollback of process-running state. Focused race -count=50, full ordinary/race pkg/sql/compile, vet, and make build passed.
|
|
||
| # go test executables run from temporary build directories. Use repository | ||
| # absolute rpaths; package-relative rpaths belong to distributable binaries. | ||
| external_flags="$platform_link_flags -L$cgo_dir -lmo -L$lib_dir -Wl,-rpath,$cgo_dir -Wl,-rpath,$lib_dir" |
There was a problem hiding this comment.
[P2] Complete the libmo link graph. cgo/Makefile and cgo/lib.go link libmo with -lroaring, but this wrapper injects only -lmo. On Linux, the documented wrapper path for a CGo-transitive target can fail the final link with unresolved roaring64_bitmap_* symbols. Add -lroaring (and validate the library prerequisite), and keep the reference commands consistent.
There was a problem hiding this comment.
Fixed in 6ba1a22f28. The wrapper now closes the complete ordered libmo CPU dependency graph after -lmo. Rather than hard-coding only -lroaring, it queries the authoritative cgo package CgoLDFLAGS via go list, so future native dependency changes do not drift from the wrapper.
I also added a minimal pure-Go → CGo transitive fixture and wired it fail-closed into the existing CPU Linux UT path; GPU remains on its explicit CUDA/cuVS contract. Verified with fresh macOS ordinary/race smoke and a Docker Linux/arm64 rebuild: the old link graph reproducibly failed on roaring64_bitmap_*, while the new wrapper passed. Reference commands and both skill mirrors were updated consistently.
|
Blocking I did another deeper pass on the latest head Problem
Concrete failure path
So a rollback attempt is currently treated as equivalent to rollback completion. Why this is a real bug, not just a suspicious patternI verified it with a deterministic scratch repro in a throwaway worktree:
That means the current head really does allow operator reuse after a failed deferred rollback transport/send path. Why this mattersAt that point the old generation's remote rollback has not been confirmed, but the operator is already reusable for a new txn generation. Even if local cleanup ran, the old TN-side txn lifetime has not been durably resolved, so restart is running ahead of remote cleanup. Suggested fixDo not mark A restart should become admissible only after one of these is true:
At minimum, please add a regression that forces the deferred rollback send path to fail and proves |
|
Addressed the blocker in 4205dcd. The finding was valid. The root invariant is not “the deferred callback returned”; it is “the previous operator generation has no unresolved terminal responsibility.” The previous terminalCleanupDone bit represented execution completion, so it could not distinguish a successful rollback from a failed workspace, TN, or unlock cleanup. The fix is generation-wide rather than specific to the reported deferred-send case:
Regression matrix added:
Fresh validation on the final commit:
Reflection: the earlier review boundary was still too local. It modeled the detached callback lifecycle, then used “done” as a proxy for generation safety. The correct review product is terminal owner × completion outcome × cleanup layer × reuse generation. Once reuse is the authority, success must be proven by the full old-generation closure, not inferred from local control-flow completion. |
XuPeng-SH
left a comment
There was a problem hiding this comment.
Self-review PASS on 4205dcd051. I found no remaining merge-blocking correctness, concurrency, resource-lifecycle, compatibility, or performance issue.
The latest terminal-outcome gate closes the remaining restart gap: cleanup stays Pending while deferred rollback owns it, successful workspace/TN/lock cleanup publishes Succeeded, and any unresolved cleanup keeps the generation permanently non-restartable. I also rechecked the full ownership graph for max-active promotion/cancellation, client Close, timestamp waiter pool reuse, RunSQL generation boundaries, public terminal-call serialization, and downstream frontend/compile/CDC admission.
Validation completed:
- deterministic CGo-transitive Linux smoke passed with the wrapper resolving the authoritative
-lusearch_c -lroaringdependency order; - full
pkg/txn/clientordinary and race suites passed; - latest terminal-outcome regressions passed under
-race -count=20; - frontend, compile, and CDC rejection/legacy-compatibility regressions passed under
-race -count=10; go vet ./pkg/txn/clientandgit diff --checkpassed;- RunSQL enter/exit and timestamp no-wait hot paths remain zero-allocation.
Non-blocking metadata note: the PR body still says issue #25206, but that issue is an unrelated lock-allocator retry bug already closed as fixed by #25651. Please update the issue linkage before merge.
GitHub does not allow the PR author (XuPeng-SH, the currently authenticated account) to submit an APPROVED review on their own PR, so this comment records the self-review verdict; an external reviewer still needs to approve the latest head.
What type of PR is this?
Which issue(s) this PR fixes:
issue #25206
What this PR does / why we need it:
This PR hardens the txn client lifecycle for cancellation, client shutdown, and CN-failure recovery, fixing multiple issues that could cause goroutine leaks, hangs, or process crashes in distributed scenarios.
Changes
1. Replace
sync.Condwith channel-based signaling (client.go)sync.Cond.Wait()cannot respond to context cancellation or client close, causing goroutines to hang indefinitely.pausedCandclosedCchannels; all wait paths now useselectto listen onctx.Done()andclosedC, ensuring every exit condition wakes blocked callers.closedflag with proper cleanup onClose()to unblock pausedNew()calls.2. Decouple abort signal from timestamp (
client.go)abortCcarriedtime.Timewith buffer=1; new abort events were dropped when the channel was full, causing the worker to operate on stale timestamps and miss transactions that should be aborted.abortCto a wakeup-onlychan struct{}; the latest abort observation is stored inatomic.Int64with CAS to ensure monotonic advancement. The worker readslatestAbortAton each wakeup, never missing an observation.3. Convert panics to error returns (
operator.go)checkResponseTxnStatusForReadWrite,checkResponseTxnStatusForCommit,checkResponseTxnStatusForRollback, andcheckTxnErrorpreviously usedpanicon malformed TN responses, which crashes the entire CN process.paniccalls replaced withreturn moerr.NewInternalError(...), allowing callers to handle errors gracefully.4. Add waiter cleanup on context cancellation (
timestamp_waiter.go)GetTimestampcaller was canceled, the waiter remained in the linked list until an unrelated future timestamp notification happened to clean it up — a permanent leak if no further notifications arrived.removeWaiterto drop the waiter from the list immediately on cancellation and unref it.indexfield for O(1) swap-delete removal.5. Context-aware
openTxnandwaitMarkAllActiveAbortedLockedopenTxnnow acceptscontext.Contextand checksctx.Err()at all wait points, preventing unbounded blocking.waitMarkAllActiveAbortedLockednow propagates context cancellation and client close signals.6. Safe type assertion for
ResumeLockServiceResumeLockServicetype assertion changed from bare cast (panics on mismatch) took-pattern assertion.Tests Added
TestOpenTxnReturnsWhenPausedContextCanceled— verifies pausedNew()returns on context timeout.TestCloseUnblocksPausedNew— verifiesClose()unblocks a pausedNew()call.TestOpenTxnReturnsWhenAbortMarkingContextCanceled— verifiesopenTxnreturns on context cancel during abort marking.TestMarkAllActiveTxnAbortedRetainsLatestObservation— verifies CAS monotonic advancement and wakeup signal coalescing.TestGetTimestampCanceledWaitIsRemoved— verifies canceled waiters are removed from the list.TestCommitAndRollbackDoNotProceedAfterRunningSQLTimeout— verifies commit/rollback abort when running SQL times out.TestMalformedTNResponsesReturnErrors— verifies malformed responses return errors instead of panicking.