Skip to content

txnkv: support shared lock upgrades - #2014

Open
wfxr wants to merge 13 commits into
tikv:masterfrom
wfxr:feat/shared-lock-upgrade-next-gen
Open

txnkv: support shared lock upgrades#2014
wfxr wants to merge 13 commits into
tikv:masterfrom
wfxr:feat/shared-lock-upgrade-next-gen

Conversation

@wfxr

@wfxr wfxr commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

Support upgrading a pessimistic shared lock to an exclusive lock when the caller explicitly opts in through LockCtx.AllowSharedLockUpgrade.

This is needed for TiDB foreign-key checking with tidb_foreign_key_check_in_shared_lock enabled. In that flow, a transaction may first acquire a shared lock while checking the parent row and then update the same parent row later in the same transaction.

Related to pingcap/tidb#68815.

Changes

This PR adds the client-side upgrade path and preserves transaction correctness across deterministic, fatal, and undetermined upgrade failures.

  • Add LockCtx.AllowSharedLockUpgrade to gate shared-to-exclusive lock upgrades explicitly.
  • Detect keys already locked in shared mode and send upgrade requests separately from normal exclusive-lock requests.
  • Promote local lock flags only after a successful upgrade, and preserve the existing shared-lock state after deterministic conflicts or validation errors.
  • Add typed ErrLockUpgradeConflict and ErrSharedLockLost handling, including API v2 key decoding and error-key redaction.
  • Poison the transaction with a fatal error after TiKV reports SharedLockLost, and mark the result undetermined when an upgrade failure leaves the remote lock state uncertain.
  • Reject shared-lock upgrades while aggressive/fair locking mode is active.
  • Add focused coverage for key selection, request grouping, flag promotion, error classification, transaction poisoning, API v2 decoding, and redaction.

Dependency and TODO

This draft depends on the protocol changes in pingcap/kvproto#1495. Until that PR merges, go.mod temporarily replaces github.com/pingcap/kvproto with github.com/wfxr/kvproto at commit d04fa040275306661d8123933445bd72f7feaf7f. Go does not propagate dependency-module replace directives, so downstream projects that test this client-go branch must add the same temporary replacement in their main module.

  • Push the supporting kvproto changes to a public fork.
  • Replace the host-specific go.work / go.work.sum setup with the pinned remote fork.
  • After feat(kvrpcpb): add lock upgrade conflict error pingcap/kvproto#1495 merges, update github.com/pingcap/kvproto to the merged upstream commit, remove the temporary github.com/wfxr/kvproto replacement, run go mod tidy, and rerun the full test suite.

Testing

The branch now resolves kvproto without a local workspace, and the following checks pass:

GOWORK=off go mod tidy
GOWORK=off go mod verify
GOWORK=off go test --tags=intest ./...
git diff --check

Summary by CodeRabbit

  • New Features

    • Added support for upgrading eligible pessimistic shared locks to exclusive locks.
    • Added an option to enable shared-lock upgrades.
    • Added clearer handling and reporting for lock-upgrade conflicts and lost shared locks.
  • Bug Fixes

    • Improved transaction safety by preserving fatal errors and preventing operations after transaction failure.
    • Added key decoding and redaction support for new lock-related errors.
  • Tests

    • Expanded coverage for shared-lock upgrades, transaction error handling, rollback behavior, and key redaction.

@ti-chi-bot

ti-chi-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@ti-chi-bot ti-chi-bot Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 1, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign ekexium for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added the dco-signoff: yes Indicates the PR's author has signed the dco. label Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds shared-lock upgrade support for eligible pessimistic transactions. It introduces typed lock errors, decodes and redacts their keys, tracks fatal and undetermined transaction states, and adds transaction and codec tests.

Changes

Shared-lock upgrade support

Layer / File(s) Summary
Typed lock errors and key handling
go.mod, integration_tests/go.mod, error/error.go, error/error_test.go, internal/apicodec/*, util/redact/*
Updated kvproto definitions support typed shared-lock errors. ExtractKeyErr, API decoding, and redaction now handle both lock error types.
Transaction upgrade execution
kv/kv.go, txnkv/transaction/2pc.go, txnkv/transaction/txn.go
LockCtx enables upgrades. Transaction locking separates upgrade candidates, classifies failures, tracks fatal and undetermined states, rolls back held locks, and blocks later operations after poisoned states.
Upgrade behavior validation
txnkv/transaction/txn_test.go
Tests cover feature gating, successful promotion, conflicts, rollback, undetermined outcomes, transaction poisoning, and error precedence.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Transaction
  participant lockKeys
  participant twoPhaseCommitter
  participant TiKV
  Transaction->>lockKeys: submit lock requests
  lockKeys->>twoPhaseCommitter: initialize transaction state
  twoPhaseCommitter->>TiKV: acquire ordinary locks
  twoPhaseCommitter->>TiKV: upgrade shared locks
  TiKV-->>twoPhaseCommitter: return lock result or typed error
  twoPhaseCommitter-->>Transaction: update state or return error
Loading

Possibly related PRs

Suggested reviewers: zyguan, cfzjywxk

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's primary change: support for shared lock upgrades in txnkv.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ti-chi-bot ti-chi-bot Bot added the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Jul 1, 2026
@wfxr wfxr changed the title txnkv: support shared lock upgrades for foreign key checks txnkv: support shared lock upgrades Jul 1, 2026
@wfxr
wfxr force-pushed the feat/shared-lock-upgrade-next-gen branch from f501e4b to 2741a6a Compare July 30, 2026 02:21
@wfxr
wfxr force-pushed the feat/shared-lock-upgrade-next-gen branch from 837a720 to afb7539 Compare August 7, 2026 03:40
@wfxr
wfxr marked this pull request as ready for review August 7, 2026 06:47
@ti-chi-bot ti-chi-bot Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 7, 2026
@wfxr

wfxr commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

/retest

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go.mod (1)

18-38: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not merge the fork replace directive.

replace directives apply only to the main module. Downstream modules that import github.com/tikv/client-go/v2 resolve github.com/pingcap/kvproto at the version in Line 18, which does not contain LockUpgradeConflict or SharedLockLost. Those builds fail to compile error/error.go, internal/apicodec/codec_v2.go, and util/redact/redact.go.

Block this PR until pingcap/kvproto#1495 merges, then pin the upstream pseudo-version and remove the replace line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go.mod` around lines 18 - 38, Remove the github.com/pingcap/kvproto replace
directive and do not merge the fork. After pingcap/kvproto#1495 is merged,
update the github.com/pingcap/kvproto dependency to the corresponding upstream
pseudo-version containing LockUpgradeConflict and SharedLockLost, ensuring
downstream modules compile without relying on the local replacement.
🧹 Nitpick comments (6)
error/error.go (1)

360-371: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the asymmetric ordering of the two new checks.

SharedLockLost is checked before Conflict, but LockUpgradeConflict is checked after it. The precedence is deliberate: error/error_test.go Line 44 sets Conflict together with SharedLockLost and requires the shared-lock-lost type. The reason LockUpgradeConflict sits below Conflict is not stated. If TiKV can set both Conflict and LockUpgradeConflict in one KeyError, the caller receives ErrWriteConflict, and txn.go isLockUpgradeResultUndetermined then treats the outcome as deterministic through the write-conflict branch instead of the lock-upgrade branch. Add a short comment that records the intended precedence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@error/error.go` around lines 360 - 371, Add a concise comment in the
error-conversion logic around SharedLockLost, Conflict, and LockUpgradeConflict
documenting that their asymmetric ordering is intentional: SharedLockLost takes
precedence over Conflict, while Conflict takes precedence over
LockUpgradeConflict so combined errors follow the write-conflict handling path.
Preserve the existing check order and behavior.
txnkv/transaction/txn.go (3)

1595-1604: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Document the per-key RPC cost and the partial-failure state.

The loop sends one PessimisticLock RPC per upgrade key, sequentially, on the calling thread. For N upgrade keys the statement pays N round trips. txnkv/transaction/txn_test.go Line 411 asserts one key per upgrade request, so the batching is intentional.

The loop also leaves a partial state on failure. If the first upgrade key succeeds and the second fails, the first key is exclusive, the second stays shared, and the error returns. No test covers two upgrade keys with a failure on the second.

Add a comment that states both properties, and add a test for the multi-upgrade-key partial failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/txn.go` around lines 1595 - 1604, Document the upgrade loop
around txn.lockPessimisticKeyGroup to state that it performs one sequential
PessimisticLock RPC per upgrade key and that failures can leave earlier keys
upgraded while later keys remain shared. Add a transaction test covering
multiple upgrade keys where the second upgrade fails, asserting the first is
exclusive, the second remains shared, and the error is returned.

1481-1510: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

The non-upgrade error path duplicates the legacy path.

Lines 1481-1510 repeat the deadlock detection, the rollbackForUpdateTS selection, the async rollback, and the retry sleep from Lines 1876-1917. The two copies already differ: the legacy copy also adjusts txn.lockedCnt and clears aggressiveLockingContext.currentLockedKeys. Two copies of the same failure protocol will drift further.

Extract the shared failure handling into one helper, then let both call sites pass the parts that differ (allKeys versus keys, and the aggressive-locking cleanup).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/txn.go` around lines 1481 - 1510, Extract the duplicated
pessimistic-lock failure handling from the current block and the legacy path
into a shared helper, preserving deadlock detection, callback invocation,
rollbackForUpdateTS selection, async rollback, and retry sleeps. Have both
callers provide their respective key collection (`allKeys` or `keys`) and an
optional aggressive-locking cleanup so the legacy path still updates
`txn.lockedCnt` and clears `aggressiveLockingContext.currentLockedKeys` without
duplicating the protocol.

802-807: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Explain why this gate runs before defer txn.close().

This block intentionally differs from getTxnStateErr at Line 1396. It returns only the fatal error, and it runs before defer txn.close() on Line 808, so the transaction stays valid and the caller can still call Rollback. txnkv/transaction/txn_test.go Line 626 asserts that behavior. The undetermined case falls through to Line 859, which runs after defer txn.close() and therefore invalidates the transaction, as asserted at txnkv/transaction/txn_test.go Line 750.

Nothing in the code records this distinction. A later refactor that replaces this block with getTxnStateErr() would remove rollback availability after a fatal error. Add a comment.

♻️ Proposed comment
+	// Check the fatal error before `defer txn.close()` below, so a poisoned
+	// transaction stays valid and the caller can still roll back the locks it
+	// holds. The undetermined case is handled after `defer txn.close()`, because
+	// an undetermined result must invalidate the transaction.
 	if txn.committer != nil {
 		undeterminedErr, fatalTxnErr := txn.committer.getTxnStateErrs()
 		if undeterminedErr == nil && fatalTxnErr != nil {
 			return fatalTxnErr
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/txn.go` around lines 802 - 807, Add a concise explanatory
comment immediately before the txn.committer fatal-error gate, documenting that
it must run before defer txn.close() so fatal errors preserve transaction
validity for caller Rollback, while undetermined errors continue to the later
post-close path; keep the existing control flow unchanged.
txnkv/transaction/txn_test.go (1)

323-336: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The recorder aliases one backing array.

Line 323 allocates keys with len(lockReq.Mutations). Line 329 stores keys[:0] in the summary. Line 335 then appends into that same backing array while the loop still writes keys[i] at Line 325. Each append overwrites the slot that the same iteration just filled with the identical value, so the result is correct today. The aliasing is not obvious, and it breaks if the write order or the append order changes.

Build the summary after the loop instead.

♻️ Proposed simplification
 			case tikvrpc.CmdPessimisticLock:
 				lockReq := req.PessimisticLock()
 				keys := make([][]byte, len(lockReq.Mutations))
 				for i, mutation := range lockReq.Mutations {
 					keys[i] = append([]byte(nil), mutation.Key...)
-					if i == 0 {
-						requests = append(requests, requestSummary{
-							cmd:  req.Type,
-							keys: keys[:0],
-							op:   mutation.Op,
-						})
-					} else {
-						require.Equal(t, requests[len(requests)-1].op, mutation.Op)
-					}
-					requests[len(requests)-1].keys = append(requests[len(requests)-1].keys, keys[i])
+					if i > 0 {
+						require.Equal(t, lockReq.Mutations[0].Op, mutation.Op)
+					}
 				}
+				requests = append(requests, requestSummary{
+					cmd:  req.Type,
+					keys: keys,
+					op:   lockReq.Mutations[0].Op,
+				})
 				return onLock(len(requests)-1, lockReq)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/txn_test.go` around lines 323 - 336, Refactor the request
recording logic around lockReq.Mutations so it first collects each mutation key
and validates operation consistency during the loop, then creates and appends a
single requestSummary after the loop. Do not store keys[:0] or append to the
summary while keys is still being populated; ensure the summary owns the
completed key collection.
txnkv/transaction/2pc.go (1)

2439-2443: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the two results of getTxnStateErrs.

The function returns two values of the same error type. Callers at txnkv/transaction/txn.go Line 803 and Line 1400 depend on positional order. Named results document the order at the declaration and prevent a silent swap during a future edit.

♻️ Proposed signature
-func (c *twoPhaseCommitter) getTxnStateErrs() (error, error) {
+func (c *twoPhaseCommitter) getTxnStateErrs() (undeterminedErr error, fatalTxnErr error) {
 	c.mu.RLock()
 	defer c.mu.RUnlock()
 	return c.mu.undeterminedErr, c.mu.fatalTxnErr
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/2pc.go` around lines 2439 - 2443, Update the
twoPhaseCommitter.getTxnStateErrs signature to use descriptive named error
results that identify the undetermined and fatal transaction errors in
positional order. Keep the existing locking and return expressions unchanged,
ensuring callers such as txn.go continue receiving undeterminedErr first and
fatalTxnErr second.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@kv/kv.go`:
- Line 70: Add a clear Go doc comment immediately above the exported LockCtx
field AllowSharedLockUpgrade, documenting that it permits shared-lock upgrades,
the upgrade is rejected in aggressive/fair locking mode, and failure may leave
the transaction undetermined or fatally poisoned.

In `@txnkv/transaction/txn_test.go`:
- Around line 511-559: Add a subtest alongside
LockUpgradeConflictReturnsTypedErrorWithoutRetrySemantics that makes the upgrade
request return a Deadlock key error, configures lockCtx.OnDeadlock, and verifies
the callback is invoked with dl.IsRetryable set. Use the existing recorder
transaction and upgrade-lock setup, and assert the deadlock-specific branch
behavior through lockKeys.

In `@txnkv/transaction/txn.go`:
- Around line 1468-1479: The upgrade error handling in
txnkv/transaction/txn.go:1468-1479 must handle tikverr.ErrDeadlock before the
existing undetermined-result checks: set dl.IsRetryable via hashInKeys and
invoke lockCtx.OnDeadlock(dl), without adding pessimistic rollback so the
retained shared lock remains intact. Add a subtest in
txnkv/transaction/txn_test.go:511-559 that produces a Deadlock key error during
upgrade and verifies OnDeadlock is called and IsRetryable is set.
- Around line 1516-1519: In the loop using lockCtx.Values, remove the single-use
keyStr variable and inline the string conversion directly in the map lookup,
changing the access to use lockCtx.Values[string(key)] while preserving the
existing valExists and value-handling behavior.
- Around line 1778-1784: Guard the shared-lock upgrade path in the surrounding
transaction lock flow with txn.committer != nil && lockCtx.ForUpdateTS > 0
before calling lockKeysWithSharedLockUpgrade. Preserve the existing normal
pessimistic-lock branch, and when ForUpdateTS is zero update only the local
memory-buffer upgrade flag instead of sending upgrade RPCs.
- Around line 1565-1575: Update the primary-selection block around
selectPrimaryForPessimisticLock so it never uses upgradeKeys as a fallback
candidate when normalExclusiveKeys is empty. Require at least one normal
exclusive key before selecting the pessimistic primary, or explicitly return the
established error for a shared-only candidate before issuing the upgrade RPC.

---

Outside diff comments:
In `@go.mod`:
- Around line 18-38: Remove the github.com/pingcap/kvproto replace directive and
do not merge the fork. After pingcap/kvproto#1495 is merged, update the
github.com/pingcap/kvproto dependency to the corresponding upstream
pseudo-version containing LockUpgradeConflict and SharedLockLost, ensuring
downstream modules compile without relying on the local replacement.

---

Nitpick comments:
In `@error/error.go`:
- Around line 360-371: Add a concise comment in the error-conversion logic
around SharedLockLost, Conflict, and LockUpgradeConflict documenting that their
asymmetric ordering is intentional: SharedLockLost takes precedence over
Conflict, while Conflict takes precedence over LockUpgradeConflict so combined
errors follow the write-conflict handling path. Preserve the existing check
order and behavior.

In `@txnkv/transaction/2pc.go`:
- Around line 2439-2443: Update the twoPhaseCommitter.getTxnStateErrs signature
to use descriptive named error results that identify the undetermined and fatal
transaction errors in positional order. Keep the existing locking and return
expressions unchanged, ensuring callers such as txn.go continue receiving
undeterminedErr first and fatalTxnErr second.

In `@txnkv/transaction/txn_test.go`:
- Around line 323-336: Refactor the request recording logic around
lockReq.Mutations so it first collects each mutation key and validates operation
consistency during the loop, then creates and appends a single requestSummary
after the loop. Do not store keys[:0] or append to the summary while keys is
still being populated; ensure the summary owns the completed key collection.

In `@txnkv/transaction/txn.go`:
- Around line 1595-1604: Document the upgrade loop around
txn.lockPessimisticKeyGroup to state that it performs one sequential
PessimisticLock RPC per upgrade key and that failures can leave earlier keys
upgraded while later keys remain shared. Add a transaction test covering
multiple upgrade keys where the second upgrade fails, asserting the first is
exclusive, the second remains shared, and the error is returned.
- Around line 1481-1510: Extract the duplicated pessimistic-lock failure
handling from the current block and the legacy path into a shared helper,
preserving deadlock detection, callback invocation, rollbackForUpdateTS
selection, async rollback, and retry sleeps. Have both callers provide their
respective key collection (`allKeys` or `keys`) and an optional
aggressive-locking cleanup so the legacy path still updates `txn.lockedCnt` and
clears `aggressiveLockingContext.currentLockedKeys` without duplicating the
protocol.
- Around line 802-807: Add a concise explanatory comment immediately before the
txn.committer fatal-error gate, documenting that it must run before defer
txn.close() so fatal errors preserve transaction validity for caller Rollback,
while undetermined errors continue to the later post-close path; keep the
existing control flow unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 715b2c7b-1d02-4fd4-b301-2ff1cc9c350c

📥 Commits

Reviewing files that changed from the base of the PR and between aff3e66 and afb7539.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • error/error.go
  • error/error_test.go
  • go.mod
  • internal/apicodec/codec_v2.go
  • internal/apicodec/codec_v2_test.go
  • kv/kv.go
  • txnkv/transaction/2pc.go
  • txnkv/transaction/txn.go
  • txnkv/transaction/txn_test.go
  • util/redact/redact.go
  • util/redact/redact_test.go

Comment thread kv/kv.go
CheckExistence bool
LockOnlyIfExists bool
InShareMode bool
AllowSharedLockUpgrade bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a doc comment to the new public field.

LockCtx is part of the public client API. AllowSharedLockUpgrade gates behavior that callers cannot infer from the name: an upgrade failure can leave the transaction undetermined or fatally poisoned, and the upgrade is rejected in aggressive/fair locking mode.

As per coding guidelines: "Exported identifiers must have clear Go doc comments when they are part of the public client API."

📝 Proposed doc comment
 	InShareMode             bool
+	// AllowSharedLockUpgrade allows a key that this transaction already holds in
+	// shared mode to be upgraded to an exclusive lock. Upgrade requests are sent
+	// separately from ordinary exclusive locks. A deterministic upgrade failure
+	// keeps the existing shared lock. Any other upgrade failure marks the
+	// transaction result undetermined or fatal. Upgrades are rejected in
+	// aggressive/fair locking mode.
 	AllowSharedLockUpgrade  bool
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AllowSharedLockUpgrade bool
InShareMode bool
// AllowSharedLockUpgrade allows a key that this transaction already holds in
// shared mode to be upgraded to an exclusive lock. Upgrade requests are sent
// separately from ordinary exclusive locks. A deterministic upgrade failure
// keeps the existing shared lock. Any other upgrade failure marks the
// transaction result undetermined or fatal. Upgrades are rejected in
// aggressive/fair locking mode.
AllowSharedLockUpgrade bool
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kv/kv.go` at line 70, Add a clear Go doc comment immediately above the
exported LockCtx field AllowSharedLockUpgrade, documenting that it permits
shared-lock upgrades, the upgrade is rejected in aggressive/fair locking mode,
and failure may leave the transaction undetermined or fatally poisoned.

Source: Coding guidelines

Comment thread txnkv/transaction/txn_test.go
Comment thread txnkv/transaction/txn.go
Comment on lines +1468 to +1479
if isUpgrade {
var sharedLockLost *tikverr.ErrSharedLockLost
if errors.As(err, &sharedLockLost) {
txn.committer.setFatalTxnErr(err)
return 0, err
}
if isLockUpgradeResultUndetermined(err) {
txn.committer.setUndeterminedErr(err)
return 0, errors.WithStack(tikverr.ErrResultUndetermined)
}
return 0, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The upgrade error path omits the deadlock protocol, and no test detects it. isLockUpgradeResultUndetermined classifies *tikverr.ErrDeadlock as deterministic, so an upgrade that deadlocks returns the error directly without marking dl.IsRetryable and without calling lockCtx.OnDeadlock. The ordinary lock path performs both steps. No subtest covers a deadlock on an upgrade, so the gap is invisible.

  • txnkv/transaction/txn.go#L1468-L1479: in the isUpgrade branch, detect *tikverr.ErrDeadlock, set dl.IsRetryable through hashInKeys, and call lockCtx.OnDeadlock(dl). Do not add the pessimistic rollback, because the rollback would drop the retained shared lock.
  • txnkv/transaction/txn_test.go#L511-L559: add a subtest that returns a Deadlock key error for the upgrade request, then assert that lockCtx.OnDeadlock runs and that IsRetryable is set.
📍 Affects 2 files
  • txnkv/transaction/txn.go#L1468-L1479 (this comment)
  • txnkv/transaction/txn_test.go#L511-L559
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/txn.go` around lines 1468 - 1479, The upgrade error
handling in txnkv/transaction/txn.go:1468-1479 must handle tikverr.ErrDeadlock
before the existing undetermined-result checks: set dl.IsRetryable via
hashInKeys and invoke lockCtx.OnDeadlock(dl), without adding pessimistic
rollback so the retained shared lock remains intact. Add a subtest in
txnkv/transaction/txn_test.go:511-559 that produces a Deadlock key error during
upgrade and verifies OnDeadlock is called and IsRetryable is set.

Comment thread txnkv/transaction/txn.go Outdated
Comment thread txnkv/transaction/txn.go
Comment on lines +1565 to +1575
if txn.committer.primaryKey == nil {
// Prefer selecting the primary from freshly requested exclusive locks
// when possible, so the primary is not just a shared-locked key that
// still depends on upgrade success.
assignedPrimaryKey = true
keysForPrimary := normalExclusiveKeys
if len(keysForPrimary) == 0 {
keysForPrimary = upgradeKeys
}
txn.selectPrimaryForPessimisticLock(keysForPrimary)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect primary selection, resetPrimary, and ttlManager interaction.
set -euo pipefail

ast-grep run --pattern $'func (txn *KVTxn) selectPrimaryForPessimisticLock($$$) {
  $$$
}' --lang go txnkv/transaction/txn.go

ast-grep run --pattern $'func (txn *KVTxn) resetPrimary($$$) {
  $$$
}' --lang go txnkv/transaction/txn.go

rg -n -C 3 'setPrimary|run\(|ttlManager' txnkv/transaction/txn.go

Repository: tikv/client-go

Length of output: 8280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "txnkv/transaction/txn.go relevant sections:"
sed -n '1530,1615p' txnkv/transaction/txn.go
sed -n '1780,1935p' txnkv/transaction/txn.go
sed -n '2038,2064p' txnkv/transaction/txn.go

echo
echo "LockCtx constructors/callers around pessimistic lock APIs:"
rg -n -C 4 '\bLockKeys\b|\bForUpdateTS\b|\bLockOnlyIfExists\b' . --glob '*.go' | head -n 220

echo
echo "lockPessimisticKeyGroup implementation:"
ast-grep run --pattern $'func (txn *KVTxn) lockPessimisticKeyGroup($$$) {
  $$$
}' --lang go txnkv/transaction/txn.go

Repository: tikv/client-go

Length of output: 27148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find references to lockKeys/lockKeysWithSharedLockUpgrade and InShareMode:"
rg -n -C 5 '\blogInShareMode|InShareMode|lockKeysWithSharedLockUpgrade|lock.*Upgrade|PessimisticLockWakeUpMode_WakeUp'.*InShareMode|LockType|LockType\.' txnkv/transaction *.go integration_tests/shared_lock_test.go 2>/dev/null | head -n 360

echo
echo "Locate two-phase committer methods:"
rg -n -C 4 'func .*pessimisticLockMutations|func .*run\(|startTTLManager|stopTTLManager|reset\(' txnkv/transaction -g '*.go' | head -n 260

Repository: tikv/client-go

Length of output: 287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find references to lockKeys/lockKeysWithSharedLockUpgrade and InShareMode:"
rg -n -C 5 'lockKeysWithSharedLockUpgrade|InShareMode|PessimisticLockWakeUpMode|LockType|LockType\.' txnkv/transaction integration_tests 2>/dev/null | head -n 360

echo
echo "Locate two-phase committer methods:"
rg -n -C 4 'func .*pessimisticLockMutations|func .*run\(|startTTLManager|stopTTLManager|reset\(' txnkv/transaction -g '*.go' | head -n 260

echo
echo "Find upgrade-related tests or examples:"
rg -n -C 4 'SharedLock|shared lock|upgrade|ForUpdateTS|sharedlocked|locked.*shared|InShareMode|StartAggressiveLocking' integration_tests -g '*.go' | head -n 420

Repository: tikv/client-go

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "pessimistic.go around send/request handling:"
sed -n '120,205p' txnkv/transaction/pessimistic.go
sed -n '630,720p' txnkv/transaction/pessimistic.go

echo
echo "txn.go around lockKeysWithSharedLockUpgrade callers/input validation:"
sed -n '1750,1805p' txnkv/transaction/txn.go
sed -n '1620,1655p' txnkv/transaction/txn.go
sed -n '1935,1960p' txnkv/transaction/txn.go

echo
echo "2pc.go around shared lock precommit/shared lock validation:"
sed -n '690,735p' txnkv/transaction/2pc.go

Repository: tikv/client-go

Length of output: 11166


Reject selecting an upgrade/key-only shared lock as the pessimistic primary.

selectPrimaryForPessimisticLock picks sortedKeys[0]; when normalExclusiveKeys is empty, upgradeKeys[0] is still a shared Op_SharedPessimisticLock request. The ordinary pessimistic fast path also rejects shared-mode pessimistic locking without an existing exclusive primary. Guard this fallback by requiring len(normalExclusiveKeys) > 0, or return an explicit error when the only candidate is shared-locked before sending the upgrade RPC.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/txn.go` around lines 1565 - 1575, Update the
primary-selection block around selectPrimaryForPessimisticLock so it never uses
upgradeKeys as a fallback candidate when normalExclusiveKeys is empty. Require
at least one normal exclusive key before selecting the pessimistic primary, or
explicitly return the established error for a shared-only candidate before
issuing the upgrade RPC.

Comment thread txnkv/transaction/txn.go
Comment on lines +1778 to +1784
if len(upgradeKeys) > 0 {
if len(keys) > 0 {
keys = deduplicateKeys(keys)
}
upgradeKeys = deduplicateKeys(upgradeKeys)
return txn.lockKeysWithSharedLockUpgrade(ctx, lockCtx, keys, upgradeKeys)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find ForUpdateTS handling and zero-value paths around pessimistic locking.
set -euo pipefail

rg -n -C 4 'ForUpdateTS\s*[><=]=?\s*0' --type=go
rg -n -C 3 'NewLockCtx\(' --type=go -g '!**/*_test.go'

Repository: tikv/client-go

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching txn.go:"
fd -a 'txn\.go$' . | sed 's#^\./##'

echo
echo "Locate lockKeysWithSharedLockUpgrade and nearby lines:"
for f in $(fd 'txn\.go$' .); do
  if rg -q 'lockKeysWithSharedLockUpgrade|func .*lockKeys\(' "$f"; then
    echo "FILE: $f"
    wc -l "$f"
    ast-grep outline "$f" --match 'lockKeysWithSharedLockUpgrade' --view expanded || true
    ast-grep outline "$f" --match 'lockKeys' --view expanded || true
  fi
done

Repository: tikv/client-go

Length of output: 754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in $(fd 'txn\.go$' .); do
  echo "=== $f ==="
  sed -n '1660,1965p' "$f" | cat -n -v | sed 's/^/LINE /'
done

Repository: tikv/client-go

Length of output: 15121


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "txn.go outline around NewLockCtx and lockCtx fields:"
rg -n -C 3 'NewLockCtx|type LockCtx|ForUpdateTS|AllowSharedLockUpgrade|LockOnlyIfExists|InShareMode' --type=go -g '!**/*_test.go'

echo
echo "txn.go lines 1500-1760:"
sed -n '1500,1760p' txnkv/transaction/txn.go | cat -n -v

echo
echo "search lockKeys calls:"
rg -n -C 4 '\.lockKeys\(|lockKeys\(' --type=go -g '!**/*_test.go'

Repository: tikv/client-go

Length of output: 205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find LockCtx-related definitions/usages:"
rg -n -C 4 'NewLockCtx|type LockCtx|ForUpdateTS|AllowSharedLockUpgrade|LockOnlyIfExists|InShareMode|lockKeys\(' txnkv examples -- '*.go'

echo
echo "Inspect lockKeysWithSharedLockUpgrade implementation:"
sed -n '1530,1610p' txnkv/transaction/txn.go | cat -n -v

Repository: tikv/client-go

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect lockKeysWithSharedLockUpgrade implementation:"
sed -n '1544,1615p' txnkv/transaction/txn.go | cat -n -v

echo
echo "Find production-call-site paths with LockOnlyIfExists or shared locks around LockKeys:"
rg -n -C 5 'LockOnlyIfExists|AllowSharedLockUpgrade|LockKeys\(|LockKeysFunc\(' txnkv examples -- '*.go' ':!*_test.go'

echo
echo "Search public/client LockKeys helpers that set LockOnlyIfExists:"
rg -n -C 5 'LockOnlyIfExists|AllowSharedLockUpgrade|NewLockCtx' tikv/txnkv txnkv -- '*.go' ':!*_test.go'

Repository: tikv/client-go

Length of output: 24503


Guard shared-lock upgrades with ForUpdateTS > 0.

lockKeysWithSharedLockUpgrade always creates/uses the committer and sends pessimistic-lock RPCs. Keep the existing branch at Line 1790 for normal pessimistic locks, then branch on txn.committer != nil && lockCtx.ForUpdateTS > 0 before sending upgrade RPCs. If ForUpdateTS == 0, only update the local memory-buffer upgrade flag, matching the normal fallback path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/txn.go` around lines 1778 - 1784, Guard the shared-lock
upgrade path in the surrounding transaction lock flow with txn.committer != nil
&& lockCtx.ForUpdateTS > 0 before calling lockKeysWithSharedLockUpgrade.
Preserve the existing normal pessimistic-lock branch, and when ForUpdateTS is
zero update only the local memory-buffer upgrade flag instead of sending upgrade
RPCs.

wfxr added 13 commits August 7, 2026 15:00
Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Add a typed ErrLockUpgradeConflict, decode it from KeyError including API v2 key decoding, treat it as a known shared-lock upgrade failure, and keep the local go.work wiring committed so Task 5 remains buildable against the local kvproto update during rollout sequencing.

Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Rename the shared-lock-upgrade helper to describe the
undetermined-result case directly and invert its return value so the
upgrade error branch reads in positive form without changing behavior.

Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
For non-upgrade pessimistic lock groups, the rollback path always uses the same keys that were locked. The shared-lock-upgrade path returns before any pessimistic rollback, so the extra parameter was unused there and can be removed without changing behavior.

Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Move the temporary lock key into the LockOnlyIfExists validation branch and rename it to lockKey so the variable name matches its diagnostic-only role.

Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Replace the local go.work wiring with the pushed kvproto fork so this branch can build without host-specific workspace paths.

Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Look up the returned lock value with the converted key directly so staticcheck can avoid retaining an unnecessary string conversion variable.

Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
Mirror the root module's kvproto version and replacement in the independent integration module because dependency replace directives are not inherited. Regenerate its checksums with Go 1.25.12 so integration jobs can resolve the shared-lock RPC definitions.

Signed-off-by: Wenxuan Zhang <wenxuangm@gmail.com>
@wfxr
wfxr force-pushed the feat/shared-lock-upgrade-next-gen branch from afb7539 to 93081a8 Compare August 7, 2026 07:01
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@integration_tests/go.mod`:
- Line 10: Scope the kvproto replacement in the integration test module to the
specific newer version requiring github.com/wfxr/kvproto, rather than replacing
every kvproto version. Preserve the older transitive version selected by
tikb/pd/client unless compatibility with the replacement is verified.

In `@txnkv/transaction/txn.go`:
- Around line 802-807: Move the fatalTxnErr check in Commit below the defer
txn.close() registration so returning fatalTxnErr still closes the transaction
and releases committer resources. Preserve the existing undetermined-error
handling and fatal-error return behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a082e299-cebb-42f8-88bd-859e82ce6b24

📥 Commits

Reviewing files that changed from the base of the PR and between aff3e66 and 93081a8.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • integration_tests/go.sum is excluded by !**/*.sum
📒 Files selected for processing (12)
  • error/error.go
  • error/error_test.go
  • go.mod
  • integration_tests/go.mod
  • internal/apicodec/codec_v2.go
  • internal/apicodec/codec_v2_test.go
  • kv/kv.go
  • txnkv/transaction/2pc.go
  • txnkv/transaction/txn.go
  • txnkv/transaction/txn_test.go
  • util/redact/redact.go
  • util/redact/redact_test.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • kv/kv.go
  • go.mod
  • txnkv/transaction/2pc.go
  • error/error_test.go
  • internal/apicodec/codec_v2.go
  • internal/apicodec/codec_v2_test.go
  • util/redact/redact.go
  • util/redact/redact_test.go
  • txnkv/transaction/txn_test.go
  • error/error.go

Comment thread integration_tests/go.mod
github.com/pingcap/errors v0.11.5-0.20260508054701-306e305bcf41
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86
github.com/pingcap/kvproto v0.0.0-20260721064811-683dad8fa368
github.com/pingcap/kvproto v0.0.0-20260806092442-d04fa0402753

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

cd integration_tests

go list -m -f '{{.Path}}@{{.Version}}{{if .Replace}} => {{.Replace.Path}}@{{.Replace.Version}}{{end}}' \
  github.com/pingcap/kvproto

go mod graph |
  awk '$2 ~ /^github.com\/pingcap\/kvproto@/ { print $2 }' |
  sort -u

go mod tidy -diff
go mod verify

Repository: tikv/client-go

Length of output: 144


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,120p'
echo "== go.mod snippets =="
if [ -f integration_tests/go.mod ]; then
  nl -ba integration_tests/go.mod | sed -n '1,40p;160,190p'
fi
echo "== go.sum kvproto entries =="
if [ -f integration_tests/go.sum ]; then
  rg -n 'github.com/(pingcap|wfxr)/kvproto|github.com/tikv/client-go/v2' integration_tests/go.sum || true
fi
echo "== imports/usages =="
rg -n 'pingcap/kvproto|wfxr/kvproto|ErrShared|LockShared|shared.lock|shared lock' -g '*.go' -g 'go.mod' -g 'go.sum' integration_tests || true

Repository: tikv/client-go

Length of output: 3794


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== integration_tests/go.mod lines =="
sed -n '1,35p;150,190p' integration_tests/go.mod
echo "== integration_tests/go.sum kvproto references =="
awk '
  /github\.com\/(pingcap|wfxr)\/kvproto/ || /github\.com\/tikv\/client-go/v2/ { print }
' integration_tests/go.sum | sed -n '1,120p'
echo "== shared-lock error schema definitions =="
rg -n 'ErrShared|Shared|shared_lock|shared lock|lock_shared|LockShared' integration_tests shared_lock_test lock_test internal error --glob '*.go' --glob 'go.mod' --glob 'go.sum' || true
echo "== go command availability =="
command -v go || true
go version || true

Repository: tikv/client-go

Length of output: 3462


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== go.sum kvproto references =="
grep -n 'github.com/\(pingcap\|wfxr\)/kvproto\|github.com/tikv/client-go/v2' integration_tests/go.sum || true
echo "== shared_lock references =="
grep -RInE 'ErrShared|Shared|shared_lock|shared lock|lock_shared|LockShared' integration_tests --include='*.go' --include='go.mod' --include='go.sum' || true
echo "== go command availability =="
command -v go || true
go version || true

Repository: tikv/client-go

Length of output: 7333


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

cd integration_tests

echo "== required module line =="
grep -n 'require github.com/pingcap/kvproto\|github.com/pingcap/kvproto' go.mod
echo "== replace directive line =="
grep -n '^replace github.com/pingcap/kvproto' go.mod
echo "== direct module info =="
go list -m -json github.com/pingcap/kvproto | sed -n '1,80p'
echo "== module graph kvproto lines =="
go mod graph | grep 'github.com/pingcap/kvproto@' | sort -u || true
echo "== tidy diff status =="
if ! go mod tidy -diff; then
  true
fi
echo "== module verify status =="
if ! go mod verify; then
  true
fi

Repository: tikv/client-go

Length of output: 368


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

cd integration_tests

echo "== module info =="
go list -m -json github.com/pingcap/kvproto
echo "== module graph kvproto lines =="
go mod graph | grep 'github.com/pingcap/kvproto@' | sort -u || true
echo "== mod tidy diff exit status =="
go mod tidy; echo "exit=$?"
echo "== mod verify exit status =="
go mod verify; echo "exit=$?"

Repository: tikv/client-go

Length of output: 22249


Limit the kvproto replacement to the versions that need it.

The unversioned replace directive also substitutes the older github.com/pingcap/kvproto@v0.0.0-20260622063236-b41e86365ce0 selected by tikb/pd/client, while the direct module target is the newer commit. Scope the replacement to the single version that requires github.com/wfxr/kvproto unless the older transitive version is also compatible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration_tests/go.mod` at line 10, Scope the kvproto replacement in the
integration test module to the specific newer version requiring
github.com/wfxr/kvproto, rather than replacing every kvproto version. Preserve
the older transitive version selected by tikb/pd/client unless compatibility
with the replacement is verified.

Comment thread txnkv/transaction/txn.go
Comment on lines +802 to +807
if txn.committer != nil {
undeterminedErr, fatalTxnErr := txn.committer.getTxnStateErrs()
if undeterminedErr == nil && fatalTxnErr != nil {
return fatalTxnErr
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The fatal-error return skips txn.close().

defer txn.close() is registered at Line 808, after this block. When fatalTxnErr != nil, Commit returns before that defer is registered. The transaction stays valid, and the committer resources, including the TTL manager, are not released. The undetermined path at Line 859 returns after the defer, so it does close. Move the fatal check below Line 808 for consistent cleanup.

🐛 Proposed fix
-	if txn.committer != nil {
-		undeterminedErr, fatalTxnErr := txn.committer.getTxnStateErrs()
-		if undeterminedErr == nil && fatalTxnErr != nil {
-			return fatalTxnErr
-		}
-	}
 	defer txn.close()
+
+	if txn.committer != nil {
+		undeterminedErr, fatalTxnErr := txn.committer.getTxnStateErrs()
+		if undeterminedErr == nil && fatalTxnErr != nil {
+			return fatalTxnErr
+		}
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if txn.committer != nil {
undeterminedErr, fatalTxnErr := txn.committer.getTxnStateErrs()
if undeterminedErr == nil && fatalTxnErr != nil {
return fatalTxnErr
}
}
defer txn.close()
if txn.committer != nil {
undeterminedErr, fatalTxnErr := txn.committer.getTxnStateErrs()
if undeterminedErr == nil && fatalTxnErr != nil {
return fatalTxnErr
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@txnkv/transaction/txn.go` around lines 802 - 807, Move the fatalTxnErr check
in Commit below the defer txn.close() registration so returning fatalTxnErr
still closes the transaction and releases committer resources. Preserve the
existing undetermined-error handling and fatal-error return behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the dco. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant