Skip to content

transaction: refuse to resolve shared locks instead of mis-handling them#556

Open
eduralph wants to merge 3 commits into
tikv:masterfrom
getwyrd:shared-locks
Open

transaction: refuse to resolve shared locks instead of mis-handling them#556
eduralph wants to merge 3 commits into
tikv:masterfrom
getwyrd:shared-locks

Conversation

@eduralph

@eduralph eduralph commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Stacked on #550 — please review the second commit only. Until #550 merges, the diff below also
contains its re-vendor.

Why: the re-vendored kvproto exposes shared locks (Op::SharedLock /
Op::SharedPessimisticLock). Their contract is unusual: a shared lock's real holders live only in
kvrpcpb.LockInfo.shared_lock_infos, and the proto comment is explicit that you must "DO NOT read
from the wrapper LockInfo" — whose own lock_version and primary_lock are left unset (its key
is not; see below).

What: this client doesn't implement shared-lock resolution, and every partial handling of one
is worse than none — resolving the wrapper checks transaction 0; filtering on the wrapper's fields
(use_async_commit, lock_type) silently drops the real members; and the pessimistic-lock special
cases in the resolver don't know SharedPessimisticLock. So resolution refuses them with an
explicit error: in resolve_locks, in LockResolver's crate-public entry point, and in
CleanupLocks::execute before any filter runs. Until real support lands, an explicit error is the
only answer that cannot roll back a live transaction or skip a dead one. Servers predating shared
locks never produce them, so this is a no-op there.

The API-v2 keyspace codecs are made shared-lock-aware separately, because they run on scan results
that never reach the resolver. They now take the shape of client-go's codecV2.decodeLockInfo
(internal/apicodec/codec_v2.go), which decodes a LockInfo's own Key / PrimaryLock /
Secondaries and then recurses into SharedLockInfos, with no wrapper special case. This client
now does the same: convert the lock's own key fields, then recurse into the members.

What the wrapper actually contains. An earlier revision skipped a wrapper's own fields entirely,
reading the proto's "DO NOT read from the wrapper LockInfo" as covering them. Checking the writer —
SharedLocks::into_lock_info in TiKV's components/txn_types/src/lock.rs, identical at v8.5.7
and on master — that's only half right, and the half it gets wrong matters:

info.set_shared_lock_infos(shared_locks.into());
info.set_key(raw_key);

A wrapper sets lock_type, shared_lock_infos and key — the key it locks — and leaves
primary_lock/lock_version at their defaults. Each member is built from that same raw key plus
its own primary and version. So the caveat scopes the per-transaction fields, not the locked key.
Skipping the wrapper wholesale would have handed scan_locks a physical key beside decoded member
keys; converting it wholesale would have hit the length assertion on the unset primary. Hence no
wrapper special case, just a per-field guard — both fields are exercised by tests.

I'd still welcome a correction if that reading of the writer is off. It can't be settled by test at
today's pin: CI runs TIKV_VERSION: v8.5.5, which predates shared locks, so no integration test in
this repo can produce one — the coverage below is unit-level by necessity, and the refusal path is
unreachable against that server. Worth noting for a separate change: v8.5.6 is the first release
carrying shared locks, so bumping the CI pin within the same release family would make these paths
reachable. Not proposing it here, given this PR is already a split-out.

The two directions guard differently, on purpose:

  • Truncating, empty bytes can only mean unset (an encoded key always carries its 4-byte
    prefix), so unset fields are skipped rather than run into pretruncate_bytes' length assertion.
  • Encoding, the input is a logical key and the empty logical key is valid in API v2, so
    nothing is skipped. scan_locksresolve_locks round-trips locks through truncate-then-encode,
    and skipping empties there would strand a lock on the empty key with no prefix, sending resolution
    after an empty physical key. Wrappers need no encode-side guard because resolution refuses them
    first.

The panic hazard predates the re-vendor: a wrapper arrives the same way whichever proto vintage
parses it.

Full shared-lock support is deliberately follow-up work.

Verification: 4 new tests — the resolver refusal (a plain lock passes; a wrapper carrying members
and a lock marked shared only by its op are both refused); the codec converting a wrapper's own key
alongside its members; a wrapper's unset fields surviving truncation (the other reading, so both are
pinned); and a lock on the empty logical key round-tripping through truncate-then-encode. 71 lib
tests; make check green; txn/raw/failpoint integration suites green against a local api-v2 cluster;
and behaviour compared against client-go by driving both clients through identical scenarios — no
observable change.

Summary by CodeRabbit

  • New Features

    • Expanded backup, restore, encryption, and import capabilities, including snapshot preparation and partition-range management.
    • Added support for region scanning, keyspace-scoped GC safeguards, region buckets, resource usage tracking, and advanced coprocessor routing.
    • Enhanced transaction, batching, tracing, health feedback, and timestamp APIs.
    • Added support for shared-lock metadata, asynchronous commit details, and improved diagnostics.
  • Bug Fixes

    • Prevented unsafe shared-lock resolution and cleanup.
    • Improved keyspace lock handling for empty and nested lock keys.
    • Refined request metadata defaults and compatibility behavior.

…revision

The vendored protos were an unrecorded late-2023/early-2024 vintage (errorpb
and pdpb untouched since tikv#324, one hand-patched addition from tikv#519). Nothing
recorded which kvproto revision they came from, and the staleness now blocks
real features.

Re-vendor everything at pingcap/kvproto b41e86365ce0 — the revision client-go
currently pins — and add proto/VERSION so the vintage is never again guesswork.
Regenerated with the existing tonic-build pipeline (21 files). One mechanical
source fix: pdpb.RequestHeader gained caller_component/caller_id, so the TSO
stream's header construction now defaults the new fields.

This commit is the re-vendor and nothing else. Newly available surface includes
KvFlush / KvBufferBatchGet (pipelined DML), HealthFeedback, and
pdpb.BatchScanRegions.

Two new protocol semantics become VISIBLE with the refresh —
errorpb.UndeterminedResult (an unknown raft apply outcome) and shared locks
(SharedLock / SharedPessimisticLock, whose real holders live in
shared_lock_infos). Handling them is deliberately left to follow-up PRs so this
one stays a pure re-vendor. Neither is a regression introduced here:

- UndeterminedResult falls through to the generic region-error arm, which
  backs off and retries — the same default the follow-up keeps for every plan
  that is not a commit point.
- A shared-lock wrapper carries no key bytes on the wire regardless of which
  proto vintage parses it, so the keyspace truncation hazard predates this
  commit; the refresh only makes the wrapper identifiable.

Tests: make check green; 67 lib tests; txn/raw/failpoint integration suites
green against a local api-v2 cluster.

Signed-off-by: Eduard R. <eduard@ralphovi.net>
@ti-chi-bot ti-chi-bot Bot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. dco-signoff: yes Indicates the PR's author has signed the dco. labels Jul 25, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 25, 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 andremouche 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

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b9a1405d-b708-45da-95f8-48a7c47a842d

📥 Commits

Reviewing files that changed from the base of the PR and between 2c0f2f3 and 6e07351.

⛔ Files ignored due to path filters (21)
  • src/generated/backup.rs is excluded by !**/generated/**
  • src/generated/cdcpb.rs is excluded by !**/generated/**
  • src/generated/coprocessor.rs is excluded by !**/generated/**
  • src/generated/deadlock.rs is excluded by !**/generated/**
  • src/generated/encryptionpb.rs is excluded by !**/generated/**
  • src/generated/errorpb.rs is excluded by !**/generated/**
  • src/generated/import_sstpb.rs is excluded by !**/generated/**
  • src/generated/keyspacepb.rs is excluded by !**/generated/**
  • src/generated/kvrpcpb.rs is excluded by !**/generated/**
  • src/generated/logbackup.rs is excluded by !**/generated/**
  • src/generated/meta_storagepb.rs is excluded by !**/generated/**
  • src/generated/metapb.rs is excluded by !**/generated/**
  • src/generated/mpp.rs is excluded by !**/generated/**
  • src/generated/pdpb.rs is excluded by !**/generated/**
  • src/generated/raft_cmdpb.rs is excluded by !**/generated/**
  • src/generated/raft_serverpb.rs is excluded by !**/generated/**
  • src/generated/resource_manager.rs is excluded by !**/generated/**
  • src/generated/resource_usage_agent.rs is excluded by !**/generated/**
  • src/generated/schedulingpb.rs is excluded by !**/generated/**
  • src/generated/tikvpb.rs is excluded by !**/generated/**
  • src/generated/tsopb.rs is excluded by !**/generated/**
📒 Files selected for processing (42)
  • proto/VERSION
  • proto/autoid.proto
  • proto/brpb.proto
  • proto/cdcpb.proto
  • proto/configpb.proto
  • proto/coprocessor.proto
  • proto/deadlock.proto
  • proto/debugpb.proto
  • proto/diagnosticspb.proto
  • proto/disaggregated.proto
  • proto/disk_usage.proto
  • proto/encryptionpb.proto
  • proto/enginepb.proto
  • proto/errorpb.proto
  • proto/gcpb.proto
  • proto/import_kvpb.proto
  • proto/import_sstpb.proto
  • proto/include/eraftpb.proto
  • proto/include/gogoproto/gogo.proto
  • proto/include/rustproto.proto
  • proto/keyspacepb.proto
  • proto/kvrpcpb.proto
  • proto/logbackuppb.proto
  • proto/meta_storagepb.proto
  • proto/metapb.proto
  • proto/mpp.proto
  • proto/pdpb.proto
  • proto/raft_cmdpb.proto
  • proto/raft_serverpb.proto
  • proto/recoverdatapb.proto
  • proto/replication_modepb.proto
  • proto/resource_manager.proto
  • proto/resource_usage_agent.proto
  • proto/schedulingpb.proto
  • proto/tikvpb.proto
  • proto/tracepb.proto
  • proto/tsopb.proto
  • src/pd/timestamp.rs
  • src/request/keyspace.rs
  • src/request/plan.rs
  • src/transaction/lock.rs
  • src/transaction/mod.rs

📝 Walkthrough

Walkthrough

The PR re-vendors kvproto definitions, adds protobuf code-generation options, expands multiple service and message contracts, and updates Rust transaction handling for shared-lock metadata and unset keys.

Changes

Protocol foundation

Layer / File(s) Summary
Proto generation foundation and formatting
proto/VERSION, proto/include/*, proto/*.proto
Records the kvproto revision, adds shared gogoproto options, and applies formatting-only updates across existing schemas.

Backup, encryption, and imports

Layer / File(s) Summary
Backup, encryption, and import contracts
proto/brpb.proto, proto/encryptionpb.proto, proto/import_sstpb.proto, proto/logbackuppb.proto, proto/meta_storagepb.proto
Adds snapshot preparation, encryption metadata, import controls, flush-now, and metadata deletion APIs.

Compute and routing

Layer / File(s) Summary
Compute, CDC, deadlock, and routing schemas
proto/cdcpb.proto, proto/coprocessor.proto, proto/deadlock.proto, proto/errorpb.proto, proto/mpp.proto, proto/keyspacepb.proto, proto/resource_usage_agent.proto, proto/schedulingpb.proto
Adds congestion, shard routing, deadlock replacement, error, MPP, resource-usage, and region-bucket protocol structures.

PD, raft, and transaction APIs

Layer / File(s) Summary
PD, raft, and TiKV service extensions
proto/pdpb.proto, proto/raft_cmdpb.proto, proto/raft_serverpb.proto, proto/tikvpb.proto
Expands region, GC, raft, batching, snapshot, health, and transaction service contracts.
Transaction and resource contracts
proto/kvrpcpb.proto, proto/resource_manager.proto
Adds commit metadata, tracing, shared-lock, execution-detail, keyspace, resource, and TiFlash-related fields and messages.
Shared-lock key handling and rejection
src/request/keyspace.rs, src/request/plan.rs, src/transaction/lock.rs, src/transaction/mod.rs, src/pd/timestamp.rs
Preserves nested shared-lock keys during conversion and rejects shared locks before resolution or cleanup.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • tikv/client-rust#550: Re-vendors the same kvproto revision and overlaps on encryption and shared-lock protocol changes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: shared locks are now refused instead of being resolved or mishandled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 contribution This PR is from a community contributor. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Jul 25, 2026
The re-vendored kvproto exposes shared locks (Op::SharedLock /
Op::SharedPessimisticLock). Their contract is unusual: a shared lock's real
holders live ONLY in kvrpcpb.LockInfo.shared_lock_infos — the proto comment is
explicit that you must "DO NOT read from the wrapper LockInfo", whose own key
and lock_version are unset.

This client does not implement shared-lock resolution, and every partial
handling of one is worse than none: resolving the wrapper checks transaction 0;
filtering on the wrapper's fields (use_async_commit, lock_type) silently drops
the real members; and the pessimistic-lock special cases in the resolver do not
know SharedPessimisticLock. So resolution REFUSES them with an explicit error —
in resolve_locks, in LockResolver's crate-public entry point, and in
CleanupLocks::execute before any filter runs. Until real support lands, an
explicit error is the only answer that cannot roll back a live transaction or
skip a dead one. Servers that predate shared locks never produce them, so this
is a no-op there.

The API-v2 keyspace codecs are made shared-lock-aware separately, because they
run on scan results that never reach the resolver. They now take the shape of
client-go's codecV2.decodeLockInfo (internal/apicodec/codec_v2.go), which
decodes a LockInfo's own Key/PrimaryLock/Secondaries and then recurses into
SharedLockInfos — it has no wrapper special case. This client now does the
same: convert the lock's OWN key fields, then recurse into the members.

An earlier revision skipped a wrapper's own fields entirely, reading the proto's
"DO NOT read from the wrapper LockInfo" as covering them. Checking the writer
(TiKV's SharedLocks::into_lock_info in components/txn_types/src/lock.rs,
identical at v8.5.7 and on master) shows
that is only half right, and the half it gets wrong matters:

  info.set_shared_lock_infos(shared_locks.into());
  info.set_key(raw_key);

A wrapper sets lock_type, shared_lock_infos and key — the key it locks — and
leaves primary_lock and lock_version at their defaults; each member is built
from that SAME raw key plus its own primary and version. So the caveat scopes
the per-transaction fields, not the locked key. Skipping the wrapper wholesale
would have handed scan_locks a physical key beside decoded member keys, while
converting it wholesale would have hit the length assertion on the unset
primary. Both fields are exercised by tests.

The two directions guard differently, on purpose. Truncating,
empty bytes can only mean "unset" — an encoded key always carries its 4-byte
prefix — so unset fields are skipped rather than run into pretruncate_bytes'
length assertion. Encoding, the input is a LOGICAL key and the empty logical key
is valid in API v2, so nothing is skipped: scan_locks -> resolve_locks
round-trips locks through truncate-then-encode, and skipping empties there would
strand a lock on the empty key with no prefix and send resolution after an empty
physical key. Wrappers need no encode-side guard because resolution refuses them
first. That panic hazard predates the re-vendor: a wrapper arrives the same way
whichever proto vintage parses it.

Full shared-lock support is deliberately follow-up work.

Split out of the kvproto re-vendor at pingyu's suggestion (tikv#550).

Tests: 4 new — the resolver refusal (a plain lock passes; a wrapper carrying
members and a lock marked shared only by its op are both refused); the codec
converting a wrapper's own key alongside its members; a wrapper's unset fields
surviving truncation (the other reading, so both are pinned); and a lock on the
empty logical key round-tripping through truncate-then-encode. 71 lib tests
green.

The coverage is unit-level by necessity: CI pins TIKV_VERSION v8.5.5, which
predates shared locks, so no integration test in this repo can produce one, and
the refusal path is unreachable against that server. v8.5.6 is the first release
carrying shared locks, so a CI pin bump within the same release family would
make these paths reachable — left to a separate change.

Signed-off-by: Eduard R. <eduard@ralphovi.net>
@eduralph
eduralph marked this pull request as ready for review July 25, 2026 22:12
@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 Jul 25, 2026
@ti-chi-bot ti-chi-bot Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution This PR is from a community contributor. dco-signoff: yes Indicates the PR's author has signed the dco. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants