From dc280eb6772a93ebd108894cf82f4d1700858c59 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Sun, 15 Feb 2026 20:41:56 +0900 Subject: [PATCH 01/20] Refactor transaction timestamp management --- adapter/internal.go | 112 ++++++++-- kv/coordinator.go | 89 +++++++- kv/fsm.go | 395 +++++++++++++++++++++++++++++++-- kv/fsm_occ_test.go | 13 +- kv/hlc_wall.go | 28 +++ kv/shard_key.go | 7 + kv/shard_store.go | 216 ++++++++++++++++-- kv/sharded_coordinator.go | 240 +++++++++++++++++++- kv/sharded_integration_test.go | 15 +- kv/transaction.go | 48 +++- kv/txn_codec.go | 242 ++++++++++++++++++++ kv/txn_consts.go | 13 ++ kv/txn_errors.go | 12 + kv/txn_keys.go | 83 +++++++ store/lsm_store.go | 3 - store/mvcc_store.go | 27 ++- store/mvcc_store_prop_test.go | 4 +- 17 files changed, 1453 insertions(+), 94 deletions(-) create mode 100644 kv/hlc_wall.go create mode 100644 kv/txn_codec.go create mode 100644 kv/txn_consts.go create mode 100644 kv/txn_errors.go create mode 100644 kv/txn_keys.go diff --git a/adapter/internal.go b/adapter/internal.go index db7027cb..48f19cfd 100644 --- a/adapter/internal.go +++ b/adapter/internal.go @@ -1,6 +1,7 @@ package adapter import ( + "bytes" "context" "github.com/bootjp/elastickv/kv" @@ -56,30 +57,109 @@ func (i *Internal) stampTimestamps(req *pb.ForwardRequest) { return } if req.IsTxn { - var startTs uint64 - // All requests in a transaction must have the same timestamp. - // Find a timestamp from the requests, or generate a new one if none exist. - for _, r := range req.Requests { - if r.Ts != 0 { - startTs = r.Ts - break - } + i.stampTxnTimestamps(req.Requests) + return + } + + i.stampRawTimestamps(req.Requests) +} + +func (i *Internal) stampRawTimestamps(reqs []*pb.Request) { + for _, r := range reqs { + if r == nil { + continue + } + if r.Ts != 0 { + continue + } + if i.clock == nil { + r.Ts = 1 + continue + } + r.Ts = i.clock.Next() + } +} + +func (i *Internal) stampTxnTimestamps(reqs []*pb.Request) { + startTS := forwardedTxnStartTS(reqs) + if startTS == 0 { + if i.clock == nil { + startTS = 1 + } else { + startTS = i.clock.Next() } + } - if startTs == 0 && len(req.Requests) > 0 { - startTs = i.clock.Next() + // Assign the unified timestamp to all requests in the transaction. + for _, r := range reqs { + if r != nil { + r.Ts = startTS } + } + + i.fillForwardedTxnCommitTS(reqs, startTS) +} - // Assign the unified timestamp to all requests in the transaction. - for _, r := range req.Requests { - r.Ts = startTs +func forwardedTxnStartTS(reqs []*pb.Request) uint64 { + for _, r := range reqs { + if r != nil && r.Ts != 0 { + return r.Ts } + } + return 0 +} + +func forwardedTxnMetaMutation(r *pb.Request, metaPrefix []byte) (*pb.Mutation, bool) { + if r == nil { + return nil, false + } + if r.Phase != pb.Phase_COMMIT && r.Phase != pb.Phase_ABORT { + return nil, false + } + if len(r.Mutations) == 0 || r.Mutations[0] == nil { + return nil, false + } + if !bytes.HasPrefix(r.Mutations[0].Key, metaPrefix) { + return nil, false + } + return r.Mutations[0], true +} + +func (i *Internal) fillForwardedTxnCommitTS(reqs []*pb.Request, startTS uint64) { + const metaPrefix = "!txn|meta|" + + metaMutations := make([]*pb.Mutation, 0, len(reqs)) + prefix := []byte(metaPrefix) + for _, r := range reqs { + m, ok := forwardedTxnMetaMutation(r, prefix) + if !ok { + continue + } + meta, err := kv.DecodeTxnMeta(m.Value) + if err != nil { + continue + } + if meta.CommitTS != 0 { + continue + } + metaMutations = append(metaMutations, m) + } + if len(metaMutations) == 0 { return } - for _, r := range req.Requests { - if r.Ts == 0 { - r.Ts = i.clock.Next() + commitTS := startTS + 1 + if i.clock != nil { + i.clock.Observe(startTS) + commitTS = i.clock.Next() + } + + for _, m := range metaMutations { + meta, err := kv.DecodeTxnMeta(m.Value) + if err != nil { + continue } + meta.CommitTS = commitTS + m.Value = kv.EncodeTxnMeta(meta) } } diff --git a/kv/coordinator.go b/kv/coordinator.go index 9bf5b1e7..d58e743f 100644 --- a/kv/coordinator.go +++ b/kv/coordinator.go @@ -2,6 +2,7 @@ package kv import ( "context" + "sort" pb "github.com/bootjp/elastickv/proto" "github.com/cockroachdb/errors" @@ -101,7 +102,18 @@ func (c *Coordinate) nextStartTS() uint64 { } func (c *Coordinate) dispatchTxn(reqs []*Elem[OP], startTS uint64) (*CoordinateResponse, error) { - logs := txnRequests(startTS, reqs) + primary := primaryKeyForElems(reqs) + if len(primary) == 0 { + return nil, errors.WithStack(ErrTxnPrimaryKeyRequired) + } + + commitTS := c.clock.Next() + if commitTS <= startTS { + c.clock.Observe(startTS) + commitTS = c.clock.Next() + } + + logs := txnRequests(startTS, commitTS, defaultTxnLockTTLms, primary, reqs) r, err := c.transactionManager.Commit(logs) if err != nil { @@ -185,7 +197,8 @@ func (c *Coordinate) redirect(ctx context.Context, reqs *OperationGroup[OP]) (*C var requests []*pb.Request if reqs.IsTxn { - requests = txnRequests(reqs.StartTS, reqs.Elems) + primary := primaryKeyForElems(reqs.Elems) + requests = txnRequests(reqs.StartTS, 0, defaultTxnLockTTLms, primary, reqs.Elems) } else { for _, req := range reqs.Elems { requests = append(requests, c.toRawRequest(req)) @@ -237,17 +250,77 @@ func elemToMutation(req *Elem[OP]) *pb.Mutation { panic("unreachable") } -func txnRequests(startTS uint64, reqs []*Elem[OP]) []*pb.Request { - muts := make([]*pb.Mutation, 0, len(reqs)) +func txnRequests(startTS, commitTS, lockTTLms uint64, primaryKey []byte, reqs []*Elem[OP]) []*pb.Request { + meta := &pb.Mutation{ + Op: pb.Op_PUT, + Key: []byte(txnMetaPrefix), + Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: lockTTLms, CommitTS: 0}), + } + + prepareMuts := make([]*pb.Mutation, 0, len(reqs)+1) + prepareMuts = append(prepareMuts, meta) for _, req := range reqs { - muts = append(muts, elemToMutation(req)) + prepareMuts = append(prepareMuts, elemToMutation(req)) + } + + commitMeta := &pb.Mutation{ + Op: pb.Op_PUT, + Key: []byte(txnMetaPrefix), + Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: 0, CommitTS: commitTS}), + } + commitMuts := make([]*pb.Mutation, 0, len(reqs)+1) + commitMuts = append(commitMuts, commitMeta) + for _, req := range reqs { + commitMuts = append(commitMuts, &pb.Mutation{Op: pb.Op_PUT, Key: req.Key}) } - // Use separate slices for PREPARE and COMMIT to avoid sharing slice header/state. - prepareMuts := append([]*pb.Mutation(nil), muts...) - commitMuts := append([]*pb.Mutation(nil), muts...) return []*pb.Request{ {IsTxn: true, Phase: pb.Phase_PREPARE, Ts: startTS, Mutations: prepareMuts}, {IsTxn: true, Phase: pb.Phase_COMMIT, Ts: startTS, Mutations: commitMuts}, } } + +func primaryKeyForElems(reqs []*Elem[OP]) []byte { + keys := make([][]byte, 0, len(reqs)) + seen := map[string]struct{}{} + for _, e := range reqs { + if e == nil || len(e.Key) == 0 { + continue + } + k := string(e.Key) + if _, ok := seen[k]; ok { + continue + } + seen[k] = struct{}{} + keys = append(keys, e.Key) + } + if len(keys) == 0 { + return nil + } + sort.Slice(keys, func(i, j int) bool { return bytesCompare(keys[i], keys[j]) < 0 }) + return keys[0] +} + +func bytesCompare(a, b []byte) int { + min := len(a) + if len(b) < min { + min = len(b) + } + for i := 0; i < min; i++ { + if a[i] == b[i] { + continue + } + if a[i] < b[i] { + return -1 + } + return 1 + } + switch { + case len(a) < len(b): + return -1 + case len(a) > len(b): + return 1 + default: + return 0 + } +} diff --git a/kv/fsm.go b/kv/fsm.go index 8aa1057e..3d5f290b 100644 --- a/kv/fsm.go +++ b/kv/fsm.go @@ -1,6 +1,7 @@ package kv import ( + "bytes" "context" "io" "log/slog" @@ -45,7 +46,19 @@ func (f *kvFSM) Apply(l *raft.Log) interface{} { return errors.WithStack(err) } - err = f.handleRequest(ctx, r, r.Ts) + commitTS := r.Ts + if r.IsTxn && (r.Phase == pb.Phase_COMMIT || r.Phase == pb.Phase_ABORT) { + meta, _, err := splitTxnMeta(r.Mutations) + if err != nil { + return errors.WithStack(err) + } + if meta.CommitTS == 0 { + return errors.WithStack(ErrTxnCommitTSRequired) + } + commitTS = meta.CommitTS + } + + err = f.handleRequest(ctx, r, commitTS) if err != nil { return errors.WithStack(err) } @@ -63,6 +76,19 @@ func (f *kvFSM) handleRequest(ctx context.Context, r *pb.Request, commitTS uint6 } func (f *kvFSM) handleRawRequest(ctx context.Context, r *pb.Request, commitTS uint64) error { + for _, mut := range r.Mutations { + if mut == nil || len(mut.Key) == 0 { + return errors.WithStack(ErrInvalidRequest) + } + // Raw requests should not mutate txn-internal keys. + if isTxnInternalKey(mut.Key) { + return errors.WithStack(ErrInvalidRequest) + } + if err := f.assertNoConflictingTxnLock(ctx, mut.Key, 0); err != nil { + return err + } + } + muts, err := toStoreMutations(r.Mutations) if err != nil { return errors.WithStack(err) @@ -95,9 +121,9 @@ func (f *kvFSM) handleTxnRequest(ctx context.Context, r *pb.Request, commitTS ui case pb.Phase_PREPARE: return f.handlePrepareRequest(ctx, r) case pb.Phase_COMMIT: - return f.handleCommitRequest(ctx, r, commitTS) + return f.handleCommitRequest(ctx, r) case pb.Phase_ABORT: - return f.handleAbortRequest(ctx, r) + return f.handleAbortRequest(ctx, r, commitTS) case pb.Phase_NONE: // not reached return errors.WithStack(ErrUnknownRequestType) @@ -126,29 +152,372 @@ func (f *kvFSM) validateConflicts(ctx context.Context, muts []*pb.Mutation, star return nil } -func (f *kvFSM) handlePrepareRequest(ctx context.Context, r *pb.Request) error { - err := f.validateConflicts(ctx, r.Mutations, r.Ts) - f.log.InfoContext(ctx, "handlePrepareRequest finish") - return errors.WithStack(err) +func uniqueMutations(muts []*pb.Mutation) ([]*pb.Mutation, error) { + if len(muts) == 0 { + return []*pb.Mutation{}, nil + } + seen := make(map[string]struct{}, len(muts)) + out := make([]*pb.Mutation, 0, len(muts)) + for _, mut := range muts { + if mut == nil || len(mut.Key) == 0 { + return nil, errors.WithStack(ErrInvalidRequest) + } + keyStr := string(mut.Key) + if _, ok := seen[keyStr]; ok { + continue + } + seen[keyStr] = struct{}{} + out = append(out, mut) + } + return out, nil } -func (f *kvFSM) handleCommitRequest(ctx context.Context, r *pb.Request, commitTS uint64) error { - muts, err := toStoreMutations(r.Mutations) +func (f *kvFSM) handlePrepareRequest(ctx context.Context, r *pb.Request) error { + meta, muts, err := splitTxnMeta(r.Mutations) + if err != nil { + return err + } + if len(meta.PrimaryKey) == 0 { + return errors.WithStack(ErrTxnPrimaryKeyRequired) + } + if len(muts) == 0 { + return errors.WithStack(ErrInvalidRequest) + } + + startTS := r.Ts + uniq, err := uniqueMutations(muts) if err != nil { + return err + } + if err := f.validateConflicts(ctx, uniq, startTS); err != nil { return errors.WithStack(err) } - if err := f.validateConflicts(ctx, r.Mutations, r.Ts); err != nil { + + ttlMs := meta.LockTTLms + if ttlMs == 0 { + // Default when callers don't specify TTL (for example, Redis MULTI/EXEC). + ttlMs = defaultTxnLockTTLms + } + expireAt := hlcWallFromNowMs(ttlMs) + + storeMuts, err := f.buildPrepareStoreMutations(ctx, uniq, meta.PrimaryKey, startTS, expireAt) + if err != nil { + return err + } + + if err := f.store.ApplyMutations(ctx, storeMuts, startTS, startTS); err != nil { return errors.WithStack(err) } + return nil +} + +func (f *kvFSM) handleCommitRequest(ctx context.Context, r *pb.Request) error { + meta, muts, err := splitTxnMeta(r.Mutations) + if err != nil { + return err + } + if len(muts) == 0 { + return errors.WithStack(ErrInvalidRequest) + } + commitTS := meta.CommitTS + startTS := r.Ts + if commitTS <= startTS { + return errors.WithStack(ErrTxnCommitTSRequired) + } + + uniq, err := uniqueMutations(muts) + if err != nil { + return err + } + storeMuts, err := f.buildCommitStoreMutations(ctx, uniq, meta, startTS, commitTS) + if err != nil { + return err + } + + if len(storeMuts) == 0 { + return nil + } + return errors.WithStack(f.store.ApplyMutations(ctx, storeMuts, startTS, commitTS)) +} + +func (f *kvFSM) handleAbortRequest(ctx context.Context, r *pb.Request, abortTS uint64) error { + meta, muts, err := splitTxnMeta(r.Mutations) + if err != nil { + return err + } + if len(meta.PrimaryKey) == 0 { + return errors.WithStack(ErrTxnPrimaryKeyRequired) + } + if len(muts) == 0 { + return errors.WithStack(ErrInvalidRequest) + } + startTS := r.Ts + if abortTS <= startTS { + return errors.WithStack(ErrTxnCommitTSRequired) + } + + uniq, err := uniqueMutations(muts) + if err != nil { + return err + } + storeMuts, abortingPrimary, err := f.buildAbortCleanupStoreMutations(ctx, uniq, meta.PrimaryKey, startTS) + if err != nil { + return err + } + if abortingPrimary { + if err := f.appendRollbackRecord(ctx, meta.PrimaryKey, startTS, &storeMuts); err != nil { + return err + } + } + + if len(storeMuts) == 0 { + return nil + } + return errors.WithStack(f.store.ApplyMutations(ctx, storeMuts, startTS, abortTS)) +} + +func (f *kvFSM) buildPrepareStoreMutations(ctx context.Context, muts []*pb.Mutation, primaryKey []byte, startTS, expireAt uint64) ([]*store.KVPairMutation, error) { + storeMuts := make([]*store.KVPairMutation, 0, len(muts)*txnPrepareStoreMutationFactor) + for _, mut := range muts { + preparedMuts, err := f.prepareTxnMutation(ctx, mut, primaryKey, startTS, expireAt) + if err != nil { + return nil, err + } + storeMuts = append(storeMuts, preparedMuts...) + } + return storeMuts, nil +} + +func (f *kvFSM) buildCommitStoreMutations(ctx context.Context, muts []*pb.Mutation, meta TxnMeta, startTS, commitTS uint64) ([]*store.KVPairMutation, error) { + storeMuts := make([]*store.KVPairMutation, 0, len(muts)*txnCommitStoreMutationFactor+txnCommitStoreMutationSlack) + + committingPrimary := false + for _, mut := range muts { + key := mut.Key + if bytes.Equal(key, meta.PrimaryKey) { + committingPrimary = true + } + + keyMuts, err := f.commitTxnKeyMutations(ctx, key, startTS) + if err != nil { + return nil, err + } + storeMuts = append(storeMuts, keyMuts...) + } - return errors.WithStack(f.store.ApplyMutations(ctx, muts, r.Ts, commitTS)) + if committingPrimary { + storeMuts = append(storeMuts, &store.KVPairMutation{ + Op: store.OpTypePut, + Key: txnCommitKey(meta.PrimaryKey, startTS), + Value: encodeTxnCommitRecord(commitTS), + }) + } + + return storeMuts, nil +} + +func (f *kvFSM) buildAbortCleanupStoreMutations(ctx context.Context, muts []*pb.Mutation, primaryKey []byte, startTS uint64) ([]*store.KVPairMutation, bool, error) { + storeMuts := make([]*store.KVPairMutation, 0, len(muts)*txnAbortStoreMutationFactor) + abortingPrimary := false + for _, mut := range muts { + key := mut.Key + if bytes.Equal(key, primaryKey) { + abortingPrimary = true + } + + shouldClear, err := f.shouldClearAbortKey(ctx, key, startTS) + if err != nil { + return nil, false, err + } + if shouldClear { + storeMuts = append(storeMuts, txnCleanupMutations(key)...) + } + } + return storeMuts, abortingPrimary, nil } -func (f *kvFSM) handleAbortRequest(_ context.Context, _ *pb.Request) error { - // OCC does not rely on locks; abort is a no-op. +func (f *kvFSM) appendRollbackRecord(ctx context.Context, primaryKey []byte, startTS uint64, storeMuts *[]*store.KVPairMutation) error { + // Don't allow rollback to win after commit record exists. + if _, err := f.store.GetAt(ctx, txnCommitKey(primaryKey, startTS), ^uint64(0)); err == nil { + return errors.WithStack(ErrTxnAlreadyCommitted) + } else if err != nil && !errors.Is(err, store.ErrKeyNotFound) { + return errors.WithStack(err) + } + + *storeMuts = append(*storeMuts, &store.KVPairMutation{ + Op: store.OpTypePut, + Key: txnRollbackKey(primaryKey, startTS), + Value: encodeTxnRollbackRecord(), + }) return nil } +func (f *kvFSM) prepareTxnMutation(ctx context.Context, mut *pb.Mutation, primaryKey []byte, startTS, expireAt uint64) ([]*store.KVPairMutation, error) { + if err := f.assertNoConflictingTxnLock(ctx, mut.Key, startTS); err != nil { + return nil, err + } + + lockVal := encodeTxnLock(txnLock{ + StartTS: startTS, + TTLExpireAt: expireAt, + PrimaryKey: primaryKey, + IsPrimaryKey: bytes.Equal(mut.Key, primaryKey), + }) + intent, err := txnIntentFromPBMutation(mut, startTS) + if err != nil { + return nil, err + } + + storeMuts := make([]*store.KVPairMutation, 0, txnPrepareStoreMutationFactor) + storeMuts = append(storeMuts, + &store.KVPairMutation{Op: store.OpTypePut, Key: txnLockKey(mut.Key), Value: lockVal}, + &store.KVPairMutation{Op: store.OpTypePut, Key: txnIntentKey(mut.Key), Value: encodeTxnIntent(intent)}, + ) + return storeMuts, nil +} + +func txnIntentFromPBMutation(mut *pb.Mutation, startTS uint64) (txnIntent, error) { + switch mut.Op { + case pb.Op_PUT: + return txnIntent{StartTS: startTS, Op: txnIntentOpPut, Value: mut.Value}, nil + case pb.Op_DEL: + return txnIntent{StartTS: startTS, Op: txnIntentOpDel, Value: nil}, nil + default: + return txnIntent{}, errors.WithStack(ErrUnknownRequestType) + } +} + +func txnCleanupMutations(key []byte) []*store.KVPairMutation { + return []*store.KVPairMutation{ + {Op: store.OpTypeDelete, Key: txnLockKey(key)}, + {Op: store.OpTypeDelete, Key: txnIntentKey(key)}, + } +} + +func (f *kvFSM) txnLockForCommit(ctx context.Context, key []byte) (txnLock, bool, error) { + lockBytes, err := f.store.GetAt(ctx, txnLockKey(key), ^uint64(0)) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return txnLock{}, false, nil + } + return txnLock{}, false, errors.WithStack(err) + } + lock, derr := decodeTxnLock(lockBytes) + if derr != nil { + return txnLock{}, false, errors.WithStack(derr) + } + return lock, true, nil +} + +func (f *kvFSM) txnIntentForCommit(ctx context.Context, key []byte) (txnIntent, bool, error) { + intentBytes, err := f.store.GetAt(ctx, txnIntentKey(key), ^uint64(0)) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return txnIntent{}, false, nil + } + return txnIntent{}, false, errors.WithStack(err) + } + intent, derr := decodeTxnIntent(intentBytes) + if derr != nil { + return txnIntent{}, false, errors.WithStack(derr) + } + return intent, true, nil +} + +func storeMutationForIntent(key []byte, intent txnIntent) (*store.KVPairMutation, error) { + switch intent.Op { + case txnIntentOpPut: + return &store.KVPairMutation{Op: store.OpTypePut, Key: key, Value: intent.Value}, nil + case txnIntentOpDel: + return &store.KVPairMutation{Op: store.OpTypeDelete, Key: key}, nil + default: + return nil, errors.WithStack(ErrUnknownRequestType) + } +} + +func (f *kvFSM) commitTxnKeyMutations(ctx context.Context, key []byte, startTS uint64) ([]*store.KVPairMutation, error) { + lock, ok, err := f.txnLockForCommit(ctx, key) + if err != nil { + return nil, err + } + if !ok { + // Already resolved (committed/rolled back). + return nil, nil + } + if lock.StartTS != startTS { + return nil, errors.Wrapf(ErrTxnLocked, "key: %s", string(key)) + } + + intent, ok, err := f.txnIntentForCommit(ctx, key) + if err != nil { + return nil, err + } + + out := make([]*store.KVPairMutation, 0, txnCommitStoreMutationFactor) + if ok { + if intent.StartTS != startTS { + return nil, errors.Wrapf(ErrTxnInvalidMeta, "intent start_ts mismatch for key %s", string(key)) + } + mut, err := storeMutationForIntent(key, intent) + if err != nil { + return nil, err + } + out = append(out, mut) + } + out = append(out, txnCleanupMutations(key)...) + return out, nil +} + +func (f *kvFSM) shouldClearAbortKey(ctx context.Context, key []byte, startTS uint64) (bool, error) { + lockBytes, err := f.store.GetAt(ctx, txnLockKey(key), ^uint64(0)) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return true, nil + } + return false, errors.WithStack(err) + } + lock, derr := decodeTxnLock(lockBytes) + if derr != nil { + return false, errors.WithStack(derr) + } + if lock.StartTS != startTS { + return false, nil + } + return true, nil +} + +func splitTxnMeta(muts []*pb.Mutation) (TxnMeta, []*pb.Mutation, error) { + if len(muts) == 0 || muts[0] == nil || len(muts[0].Key) == 0 { + return TxnMeta{}, nil, errors.WithStack(ErrTxnMetaMissing) + } + if !isTxnMetaKey(muts[0].Key) { + return TxnMeta{}, nil, errors.WithStack(ErrTxnMetaMissing) + } + meta, err := DecodeTxnMeta(muts[0].Value) + if err != nil { + return TxnMeta{}, nil, errors.WithStack(errors.Wrap(err, "decode txn meta")) + } + return meta, muts[1:], nil +} + +func (f *kvFSM) assertNoConflictingTxnLock(ctx context.Context, key []byte, startTS uint64) error { + lockBytes, err := f.store.GetAt(ctx, txnLockKey(key), ^uint64(0)) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return nil + } + return errors.WithStack(err) + } + lock, err := decodeTxnLock(lockBytes) + if err != nil { + return errors.WithStack(err) + } + if startTS != 0 && lock.StartTS == startTS { + return nil + } + return errors.Wrapf(ErrTxnLocked, "key: %s", string(key)) +} + func toStoreMutations(muts []*pb.Mutation) ([]*store.KVPairMutation, error) { out := make([]*store.KVPairMutation, 0, len(muts)) for _, mut := range muts { diff --git a/kv/fsm_occ_test.go b/kv/fsm_occ_test.go index e89cbb13..d58fc3c9 100644 --- a/kv/fsm_occ_test.go +++ b/kv/fsm_occ_test.go @@ -47,16 +47,15 @@ func TestApplyReturnsErrorOnConflict(t *testing.T) { resp := fsm.Apply(&raft.Log{Type: raft.LogCommand, Data: data}) require.Nil(t, resp) - // Stale transaction attempts to commit with startTS=90. + // Stale transaction attempts to prewrite with startTS=90. conflict := &pb.Request{ IsTxn: true, - Phase: pb.Phase_COMMIT, + Phase: pb.Phase_PREPARE, Ts: 90, - Mutations: []*pb.Mutation{{ - Op: pb.Op_PUT, - Key: []byte("k"), - Value: []byte("v2"), - }}, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte("!txn|meta|"), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: []byte("k"), LockTTLms: 1000})}, + {Op: pb.Op_PUT, Key: []byte("k"), Value: []byte("v2")}, + }, } data, err = proto.Marshal(conflict) require.NoError(t, err) diff --git a/kv/hlc_wall.go b/kv/hlc_wall.go new file mode 100644 index 00000000..a2dd05cb --- /dev/null +++ b/kv/hlc_wall.go @@ -0,0 +1,28 @@ +package kv + +import "time" + +func hlcWallNow() uint64 { + ms := time.Now().UnixMilli() + if ms < 0 { + return 0 + } + return uint64(ms) << hlcLogicalBits +} + +func hlcWallFromNowMs(deltaMs uint64) uint64 { + now := hlcWallNow() + if deltaMs == 0 { + return now + } + + delta := deltaMs << hlcLogicalBits + if delta>>hlcLogicalBits != deltaMs { + // Saturate if the shift overflowed. + return ^uint64(0) + } + if ^uint64(0)-now < delta { + return ^uint64(0) + } + return now + delta +} diff --git a/kv/shard_key.go b/kv/shard_key.go index f1a06444..e989b6bb 100644 --- a/kv/shard_key.go +++ b/kv/shard_key.go @@ -8,6 +8,13 @@ func routeKey(key []byte) []byte { if key == nil { return nil } + if embedded, ok := txnRouteKey(key); ok { + // Transaction internal keys embed the logical key after the prefix. + if user := store.ExtractListUserKey(embedded); user != nil { + return user + } + return embedded + } if user := store.ExtractListUserKey(key); user != nil { return user } diff --git a/kv/shard_store.go b/kv/shard_store.go index dd3b7d9d..3de44299 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -39,27 +39,41 @@ func (s *ShardStore) GetAt(ctx context.Context, key []byte, ts uint64) ([]byte, // Some tests use ShardStore without raft; in that case serve reads locally. if g.Raft == nil { - val, err := g.Store.GetAt(ctx, key, ts) - if err != nil { - return nil, errors.WithStack(err) - } - return val, nil + return s.localGetAt(ctx, g, key, ts) } // Verify leadership with a quorum before serving reads from local state to // avoid stale results from a deposed leader. - if g.Raft.State() == raft.Leader { - if err := g.Raft.VerifyLeader().Error(); err == nil { - val, err := g.Store.GetAt(ctx, key, ts) - if err != nil { - return nil, errors.WithStack(err) - } - return val, nil - } + if isVerifiedRaftLeader(g.Raft) { + return s.leaderGetAt(ctx, g, key, ts) } return s.proxyRawGet(ctx, g, key, ts) } +func isVerifiedRaftLeader(r *raft.Raft) bool { + if r == nil || r.State() != raft.Leader { + return false + } + return r.VerifyLeader().Error() == nil +} + +func (s *ShardStore) leaderGetAt(ctx context.Context, g *ShardGroup, key []byte, ts uint64) ([]byte, error) { + if !isTxnInternalKey(key) { + if err := s.maybeResolveTxnLock(ctx, g, key, ts); err != nil { + return nil, err + } + } + return s.localGetAt(ctx, g, key, ts) +} + +func (s *ShardStore) localGetAt(ctx context.Context, g *ShardGroup, key []byte, ts uint64) ([]byte, error) { + val, err := g.Store.GetAt(ctx, key, ts) + if err != nil { + return nil, errors.WithStack(err) + } + return val, nil +} + func (s *ShardStore) ExistsAt(ctx context.Context, key []byte, ts uint64) (bool, error) { v, err := s.GetAt(ctx, key, ts) if err != nil { @@ -156,7 +170,7 @@ func (s *ShardStore) scanRouteAt(ctx context.Context, route distribution.Route, if err != nil { return nil, errors.WithStack(err) } - return kvs, nil + return s.resolveScanLocks(ctx, kvs, ts) } } @@ -164,7 +178,7 @@ func (s *ShardStore) scanRouteAt(ctx context.Context, route distribution.Route, if err != nil { return nil, err } - return kvs, nil + return s.resolveScanLocks(ctx, kvs, ts) } func (s *ShardStore) groupForID(groupID uint64) (*ShardGroup, bool) { @@ -278,6 +292,178 @@ func (s *ShardStore) proxyLatestCommitTS(ctx context.Context, g *ShardGroup, key return resp.Ts, resp.Exists, nil } +func (s *ShardStore) maybeResolveTxnLock(ctx context.Context, g *ShardGroup, key []byte, readTS uint64) error { + // Only consider locks visible at the read timestamp. + lockBytes, err := g.Store.GetAt(ctx, txnLockKey(key), readTS) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return nil + } + return errors.WithStack(err) + } + lock, err := decodeTxnLock(lockBytes) + if err != nil { + return errors.WithStack(err) + } + // Check primary transaction status to decide commit/rollback. + status, commitTS, err := s.primaryTxnStatus(ctx, lock.PrimaryKey, lock.StartTS) + if err != nil { + return err + } + switch status { + case txnStatusCommitted: + return applyTxnResolution(g, pb.Phase_COMMIT, lock.StartTS, commitTS, lock.PrimaryKey, [][]byte{key}) + case txnStatusRolledBack: + return applyTxnResolution(g, pb.Phase_ABORT, lock.StartTS, cleanupTS(lock.StartTS), lock.PrimaryKey, [][]byte{key}) + case txnStatusPending: + return errors.Wrapf(ErrTxnLocked, "key: %s", string(key)) + default: + return errors.Wrapf(ErrTxnInvalidMeta, "unknown txn status for key %s", string(key)) + } +} + +func (s *ShardStore) resolveScanLocks(ctx context.Context, kvs []*store.KVPair, ts uint64) ([]*store.KVPair, error) { + if len(kvs) == 0 { + return kvs, nil + } + out := make([]*store.KVPair, 0, len(kvs)) + for _, kvp := range kvs { + if kvp == nil { + continue + } + // Filter txn-internal keys from user-facing scans. + if isTxnInternalKey(kvp.Key) { + continue + } + v, err := s.GetAt(ctx, kvp.Key, ts) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + continue + } + return nil, err + } + out = append(out, &store.KVPair{Key: kvp.Key, Value: v}) + } + return out, nil +} + +type txnStatus int + +const ( + txnStatusPending txnStatus = iota + txnStatusCommitted + txnStatusRolledBack +) + +func (s *ShardStore) primaryTxnStatus(ctx context.Context, primaryKey []byte, startTS uint64) (txnStatus, uint64, error) { + commitTS, committed, err := s.txnCommitTS(ctx, primaryKey, startTS) + if err != nil { + return txnStatusPending, 0, err + } + if committed { + return txnStatusCommitted, commitTS, nil + } + + rolledBack, err := s.hasTxnRollback(ctx, primaryKey, startTS) + if err != nil { + return txnStatusPending, 0, err + } + if rolledBack { + return txnStatusRolledBack, 0, nil + } + + lock, ok, err := s.loadTxnLock(ctx, primaryKey) + if err != nil { + return txnStatusPending, 0, err + } + if !ok || lock.StartTS != startTS { + return txnStatusRolledBack, 0, nil + } + + if lock.TTLExpireAt != 0 && hlcWallNow() > lock.TTLExpireAt { + s.bestEffortAbortPrimary(primaryKey, startTS) + return txnStatusRolledBack, 0, nil + } + + return txnStatusPending, 0, nil +} + +func (s *ShardStore) txnCommitTS(ctx context.Context, primaryKey []byte, startTS uint64) (uint64, bool, error) { + b, err := s.GetAt(ctx, txnCommitKey(primaryKey, startTS), ^uint64(0)) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return 0, false, nil + } + return 0, false, err + } + cts, derr := decodeTxnCommitRecord(b) + if derr != nil { + return 0, false, errors.WithStack(derr) + } + return cts, true, nil +} + +func (s *ShardStore) hasTxnRollback(ctx context.Context, primaryKey []byte, startTS uint64) (bool, error) { + _, err := s.GetAt(ctx, txnRollbackKey(primaryKey, startTS), ^uint64(0)) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return false, nil + } + return false, err + } + return true, nil +} + +func (s *ShardStore) loadTxnLock(ctx context.Context, primaryKey []byte) (txnLock, bool, error) { + lockBytes, err := s.GetAt(ctx, txnLockKey(primaryKey), ^uint64(0)) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return txnLock{}, false, nil + } + return txnLock{}, false, err + } + lock, derr := decodeTxnLock(lockBytes) + if derr != nil { + return txnLock{}, false, errors.WithStack(derr) + } + return lock, true, nil +} + +func (s *ShardStore) bestEffortAbortPrimary(primaryKey []byte, startTS uint64) { + pg, ok := s.groupForKey(primaryKey) + if !ok || pg == nil || pg.Txn == nil { + return + } + _ = applyTxnResolution(pg, pb.Phase_ABORT, startTS, cleanupTS(startTS), primaryKey, [][]byte{primaryKey}) +} + +func applyTxnResolution(g *ShardGroup, phase pb.Phase, startTS, commitTS uint64, primaryKey []byte, keys [][]byte) error { + if g == nil || g.Txn == nil { + return errors.WithStack(store.ErrNotSupported) + } + meta := &pb.Mutation{ + Op: pb.Op_PUT, + Key: []byte(txnMetaPrefix), + Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, CommitTS: commitTS}), + } + muts := make([]*pb.Mutation, 0, len(keys)+1) + muts = append(muts, meta) + for _, k := range keys { + muts = append(muts, &pb.Mutation{Op: pb.Op_PUT, Key: k}) + } + _, err := g.Txn.Commit([]*pb.Request{{IsTxn: true, Phase: phase, Ts: startTS, Mutations: muts}}) + return errors.WithStack(err) +} + +func cleanupTS(startTS uint64) uint64 { + now := hlcWallNow() + next := startTS + 1 + if now > next { + return now + } + return next +} + // ApplyMutations applies a batch of mutations to the correct shard store. // // All mutations must belong to the same shard. Cross-shard mutation batches are diff --git a/kv/sharded_coordinator.go b/kv/sharded_coordinator.go index 009d20f7..917ecdbd 100644 --- a/kv/sharded_coordinator.go +++ b/kv/sharded_coordinator.go @@ -1,6 +1,7 @@ package kv import ( + "bytes" "context" "sort" @@ -64,6 +65,10 @@ func (c *ShardedCoordinator) Dispatch(ctx context.Context, reqs *OperationGroup[ reqs.StartTS = startTS } + if reqs.IsTxn { + return c.dispatchTxn(reqs.StartTS, reqs.Elems) + } + logs, err := c.requestLogs(reqs) if err != nil { return nil, err @@ -76,6 +81,172 @@ func (c *ShardedCoordinator) Dispatch(ctx context.Context, reqs *OperationGroup[ return &CoordinateResponse{CommitIndex: r.CommitIndex}, nil } +func (c *ShardedCoordinator) dispatchTxn(startTS uint64, elems []*Elem[OP]) (*CoordinateResponse, error) { + grouped, gids, err := c.groupMutations(elems) + if err != nil { + return nil, err + } + primaryKey := primaryKeyForElems(elems) + if len(primaryKey) == 0 { + return nil, errors.WithStack(ErrTxnPrimaryKeyRequired) + } + + prepared, err := c.prewriteTxn(startTS, primaryKey, grouped, gids) + if err != nil { + return nil, err + } + + commitTS := c.nextTxnTSAfter(startTS) + primaryGid, maxIndex, err := c.commitPrimaryTxn(startTS, primaryKey, grouped, commitTS) + if err != nil { + c.abortPreparedTxn(startTS, primaryKey, prepared, abortTSFrom(commitTS)) + return nil, errors.WithStack(err) + } + + maxIndex = c.commitSecondaryTxns(startTS, primaryGid, primaryKey, grouped, gids, commitTS, maxIndex) + return &CoordinateResponse{CommitIndex: maxIndex}, nil +} + +type preparedGroup struct { + gid uint64 + keys []*pb.Mutation +} + +func (c *ShardedCoordinator) prewriteTxn(startTS uint64, primaryKey []byte, grouped map[uint64][]*pb.Mutation, gids []uint64) ([]preparedGroup, error) { + prepareMeta := txnMetaMutation(primaryKey, defaultTxnLockTTLms, 0) + prepared := make([]preparedGroup, 0, len(gids)) + + for _, gid := range gids { + g, err := c.txnGroupForID(gid) + if err != nil { + return nil, err + } + req := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: append([]*pb.Mutation{prepareMeta}, grouped[gid]...), + } + if _, err := g.Txn.Commit([]*pb.Request{req}); err != nil { + c.abortPreparedTxn(startTS, primaryKey, prepared, c.nextTxnTSAfter(startTS)) + return nil, errors.WithStack(err) + } + prepared = append(prepared, preparedGroup{gid: gid, keys: keyMutations(grouped[gid])}) + } + + return prepared, nil +} + +func (c *ShardedCoordinator) commitPrimaryTxn(startTS uint64, primaryKey []byte, grouped map[uint64][]*pb.Mutation, commitTS uint64) (uint64, uint64, error) { + primaryGid := c.engineGroupIDForKey(primaryKey) + if primaryGid == 0 { + return 0, 0, errors.WithStack(ErrInvalidRequest) + } + + g, err := c.txnGroupForID(primaryGid) + if err != nil { + return 0, 0, err + } + + meta := txnMetaMutation(primaryKey, 0, commitTS) + keys := keyMutations(grouped[primaryGid]) + req := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: startTS, + Mutations: append([]*pb.Mutation{meta}, keys...), + } + + r, err := g.Txn.Commit([]*pb.Request{req}) + if err != nil { + return primaryGid, 0, errors.WithStack(err) + } + if r == nil { + return primaryGid, 0, nil + } + return primaryGid, r.CommitIndex, nil +} + +func (c *ShardedCoordinator) commitSecondaryTxns(startTS uint64, primaryGid uint64, primaryKey []byte, grouped map[uint64][]*pb.Mutation, gids []uint64, commitTS uint64, maxIndex uint64) uint64 { + meta := txnMetaMutation(primaryKey, 0, commitTS) + for _, gid := range gids { + if gid == primaryGid { + continue + } + g, ok := c.groups[gid] + if !ok || g == nil || g.Txn == nil { + continue + } + req := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: startTS, + Mutations: append([]*pb.Mutation{meta}, keyMutations(grouped[gid])...), + } + if r, err := g.Txn.Commit([]*pb.Request{req}); err == nil && r != nil && r.CommitIndex > maxIndex { + maxIndex = r.CommitIndex + } + } + return maxIndex +} + +func (c *ShardedCoordinator) abortPreparedTxn(startTS uint64, primaryKey []byte, prepared []preparedGroup, abortTS uint64) { + if len(prepared) == 0 { + return + } + + meta := txnMetaMutation(primaryKey, 0, abortTS) + for _, pg := range prepared { + g, ok := c.groups[pg.gid] + if !ok || g == nil || g.Txn == nil { + continue + } + req := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_ABORT, + Ts: startTS, + Mutations: append([]*pb.Mutation{meta}, pg.keys...), + } + _, _ = g.Txn.Commit([]*pb.Request{req}) + } +} + +func (c *ShardedCoordinator) txnGroupForID(gid uint64) (*ShardGroup, error) { + g, ok := c.groups[gid] + if !ok || g == nil || g.Txn == nil { + return nil, errors.Wrapf(ErrInvalidRequest, "unknown group %d", gid) + } + return g, nil +} + +func (c *ShardedCoordinator) nextTxnTSAfter(startTS uint64) uint64 { + if c.clock == nil { + return startTS + 1 + } + ts := c.clock.Next() + if ts <= startTS { + c.clock.Observe(startTS) + ts = c.clock.Next() + } + return ts +} + +func abortTSFrom(commitTS uint64) uint64 { + abortTS := commitTS + 1 + if abortTS == 0 { + return commitTS + } + return abortTS +} + +func txnMetaMutation(primaryKey []byte, lockTTLms uint64, commitTS uint64) *pb.Mutation { + return &pb.Mutation{ + Op: pb.Op_PUT, + Key: []byte(txnMetaPrefix), + Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: lockTTLms, CommitTS: commitTS}), + } +} + func (c *ShardedCoordinator) nextStartTS(ctx context.Context, elems []*Elem[OP]) (uint64, error) { maxTS, err := c.maxLatestCommitTS(ctx, elems) if err != nil { @@ -169,6 +340,14 @@ func (c *ShardedCoordinator) groupForKey(key []byte) (*ShardGroup, bool) { return g, ok } +func (c *ShardedCoordinator) engineGroupIDForKey(key []byte) uint64 { + route, ok := c.engine.GetRoute(routeKey(key)) + if !ok { + return 0 + } + return route.GroupID +} + func (c *ShardedCoordinator) toRawRequest(req *Elem[OP]) *pb.Request { switch req.Op { case Put: @@ -230,16 +409,14 @@ func (c *ShardedCoordinator) rawLogs(reqs *OperationGroup[OP]) []*pb.Request { } func (c *ShardedCoordinator) txnLogs(reqs *OperationGroup[OP]) ([]*pb.Request, error) { + // NOTE: ShardedCoordinator implements distributed transactions directly in + // Dispatch. txnLogs is retained for compatibility and single-shard helpers. grouped, gids, err := c.groupMutations(reqs.Elems) if err != nil { return nil, err } - if len(gids) > 1 { - return nil, errors.Wrapf( - ErrCrossShardTransactionNotSupported, - "involved_shards=%v", - gids, - ) + if len(gids) != 1 { + return nil, errors.WithStack(ErrInvalidRequest) } return buildTxnLogs(reqs.StartTS, grouped, gids), nil } @@ -270,9 +447,56 @@ func buildTxnLogs(startTS uint64, grouped map[uint64][]*pb.Mutation, gids []uint for _, gid := range gids { muts := grouped[gid] logs = append(logs, - &pb.Request{IsTxn: true, Phase: pb.Phase_PREPARE, Ts: startTS, Mutations: muts}, - &pb.Request{IsTxn: true, Phase: pb.Phase_COMMIT, Ts: startTS, Mutations: muts}, + &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: append([]*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKeyFromMutations(muts), LockTTLms: defaultTxnLockTTLms})}, + }, muts...), + }, + &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: startTS, + Mutations: append([]*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKeyFromMutations(muts)})}, + }, keyMutations(muts)...), + }, ) } return logs } + +func primaryKeyFromMutations(muts []*pb.Mutation) []byte { + if len(muts) == 0 { + return nil + } + primary := muts[0].Key + for _, m := range muts[1:] { + if m == nil || len(m.Key) == 0 { + continue + } + if bytes.Compare(m.Key, primary) < 0 { + primary = m.Key + } + } + return primary +} + +func keyMutations(muts []*pb.Mutation) []*pb.Mutation { + out := make([]*pb.Mutation, 0, len(muts)) + seen := map[string]struct{}{} + for _, m := range muts { + if m == nil || len(m.Key) == 0 { + continue + } + k := string(m.Key) + if _, ok := seen[k]; ok { + continue + } + seen[k] = struct{}{} + out = append(out, &pb.Mutation{Op: pb.Op_PUT, Key: m.Key}) + } + return out +} diff --git a/kv/sharded_integration_test.go b/kv/sharded_integration_test.go index 22989b8f..a8caf302 100644 --- a/kv/sharded_integration_test.go +++ b/kv/sharded_integration_test.go @@ -136,16 +136,15 @@ func TestShardedCoordinatorDispatch_RejectsCrossShardTxn(t *testing.T) { {Op: Put, Key: []byte("x"), Value: []byte("v2")}, }, } - if _, err := coord.Dispatch(ctx, ops); err == nil || !errors.Is(err, ErrCrossShardTransactionNotSupported) { - t.Fatalf("expected ErrCrossShardTransactionNotSupported, got %v", err) + if _, err := coord.Dispatch(ctx, ops); err != nil { + t.Fatalf("dispatch: %v", err) } - // Ensure the rejected transaction didn't write anything. - readTS := ^uint64(0) - if _, err := shardStore.GetAt(ctx, []byte("b"), readTS); !errors.Is(err, store.ErrKeyNotFound) { - t.Fatalf("expected key b missing, got %v", err) + readTS := shardStore.LastCommitTS() + if v, err := shardStore.GetAt(ctx, []byte("b"), readTS); err != nil || string(v) != "v1" { + t.Fatalf("get b: %v %v", v, err) } - if _, err := shardStore.GetAt(ctx, []byte("x"), readTS); !errors.Is(err, store.ErrKeyNotFound) { - t.Fatalf("expected key x missing, got %v", err) + if v, err := shardStore.GetAt(ctx, []byte("x"), readTS); err != nil || string(v) != "v2" { + t.Fatalf("get x: %v %v", v, err) } } diff --git a/kv/transaction.go b/kv/transaction.go index fd0e89a4..6984deca 100644 --- a/kv/transaction.go +++ b/kv/transaction.go @@ -72,11 +72,14 @@ func (t *TransactionManager) Commit(reqs []*pb.Request) (*TransactionResponse, e }() if err != nil { - _, _err := t.Abort(reqs) - if _err != nil { - return nil, errors.WithStack(errors.CombineErrors(err, _err)) + // Only attempt transactional cleanup for transactional batches. Raw request + // batches may partially succeed across shards by design. + if len(reqs) > 0 && reqs[0] != nil && reqs[0].IsTxn { + _, _err := t.Abort(reqs) + if _err != nil { + return nil, errors.WithStack(errors.CombineErrors(err, _err)) + } } - return nil, errors.WithStack(err) } @@ -88,11 +91,29 @@ func (t *TransactionManager) Commit(reqs []*pb.Request) (*TransactionResponse, e func (t *TransactionManager) Abort(reqs []*pb.Request) (*TransactionResponse, error) { var abortReqs []*pb.Request for _, req := range reqs { + if req == nil || !req.IsTxn { + continue + } + meta, muts, err := extractTxnMeta(req.Mutations) + if err != nil { + // Best-effort cleanup; skip requests we can't interpret. + continue + } + startTS := req.Ts + abortTS := startTS + 1 + meta.CommitTS = abortTS + abortReqs = append(abortReqs, &pb.Request{ - IsTxn: true, - Phase: pb.Phase_ABORT, - Ts: req.Ts, - Mutations: req.Mutations, + IsTxn: true, + Phase: pb.Phase_ABORT, + Ts: startTS, + Mutations: append([]*pb.Mutation{ + { + Op: pb.Op_PUT, + Key: []byte(txnMetaPrefix), + Value: EncodeTxnMeta(meta), + }, + }, muts...), }) } @@ -114,3 +135,14 @@ func (t *TransactionManager) Abort(reqs []*pb.Request) (*TransactionResponse, er CommitIndex: commitIndex, }, nil } + +func extractTxnMeta(muts []*pb.Mutation) (TxnMeta, []*pb.Mutation, error) { + if len(muts) == 0 || muts[0] == nil || !isTxnMetaKey(muts[0].Key) { + return TxnMeta{}, nil, errors.WithStack(ErrTxnMetaMissing) + } + meta, err := DecodeTxnMeta(muts[0].Value) + if err != nil { + return TxnMeta{}, nil, errors.WithStack(err) + } + return meta, muts[1:], nil +} diff --git a/kv/txn_codec.go b/kv/txn_codec.go new file mode 100644 index 00000000..6cf2ebd9 --- /dev/null +++ b/kv/txn_codec.go @@ -0,0 +1,242 @@ +package kv + +import ( + "bytes" + "encoding/binary" + "strconv" + + "github.com/cockroachdb/errors" +) + +const ( + txnMetaVersion byte = 1 + txnLockVersion byte = 1 + txnIntentVersion byte = 1 + txnCommitVersion byte = 1 + txnRollbackVersion byte = 1 +) + +const txnLockFlagPrimary byte = 0x01 + +// TxnMeta is embedded into transactional raft log requests via a synthetic +// mutation (key prefix "!txn|meta|"). It is not persisted in the MVCC store. +type TxnMeta struct { + PrimaryKey []byte + LockTTLms uint64 + CommitTS uint64 +} + +func EncodeTxnMeta(m TxnMeta) []byte { + var buf bytes.Buffer + buf.WriteByte(txnMetaVersion) + _ = binary.Write(&buf, binary.BigEndian, m.LockTTLms) + _ = binary.Write(&buf, binary.BigEndian, m.CommitTS) + primaryLen := uint64(len(m.PrimaryKey)) + _ = binary.Write(&buf, binary.BigEndian, primaryLen) + if primaryLen > 0 { + buf.Write(m.PrimaryKey) + } + return buf.Bytes() +} + +func DecodeTxnMeta(b []byte) (TxnMeta, error) { + if len(b) < 1 { + return TxnMeta{}, errors.New("txn meta: empty") + } + if b[0] != txnMetaVersion { + return TxnMeta{}, errors.WithStack(errors.Newf("txn meta: unsupported version %d", b[0])) + } + r := bytes.NewReader(b[1:]) + var ttl uint64 + var commitTS uint64 + var primaryLen uint64 + if err := binary.Read(r, binary.BigEndian, &ttl); err != nil { + return TxnMeta{}, errors.WithStack(err) + } + if err := binary.Read(r, binary.BigEndian, &commitTS); err != nil { + return TxnMeta{}, errors.WithStack(err) + } + if err := binary.Read(r, binary.BigEndian, &primaryLen); err != nil { + return TxnMeta{}, errors.WithStack(err) + } + if primaryLen == 0 { + return TxnMeta{PrimaryKey: nil, LockTTLms: ttl, CommitTS: commitTS}, nil + } + plen, err := u64ToInt(primaryLen) + if err != nil { + return TxnMeta{}, errors.WithStack(err) + } + if plen > r.Len() { + return TxnMeta{}, errors.New("txn meta: primary key truncated") + } + pk := make([]byte, plen) + if _, err := r.Read(pk); err != nil { + return TxnMeta{}, errors.WithStack(err) + } + return TxnMeta{PrimaryKey: pk, LockTTLms: ttl, CommitTS: commitTS}, nil +} + +type txnLock struct { + StartTS uint64 + TTLExpireAt uint64 + PrimaryKey []byte + IsPrimaryKey bool +} + +func encodeTxnLock(l txnLock) []byte { + var buf bytes.Buffer + buf.WriteByte(txnLockVersion) + _ = binary.Write(&buf, binary.BigEndian, l.StartTS) + _ = binary.Write(&buf, binary.BigEndian, l.TTLExpireAt) + var flags byte + if l.IsPrimaryKey { + flags |= txnLockFlagPrimary + } + buf.WriteByte(flags) + primaryLen := uint64(len(l.PrimaryKey)) + _ = binary.Write(&buf, binary.BigEndian, primaryLen) + if primaryLen > 0 { + buf.Write(l.PrimaryKey) + } + return buf.Bytes() +} + +func decodeTxnLock(b []byte) (txnLock, error) { + if len(b) < 1 { + return txnLock{}, errors.New("txn lock: empty") + } + if b[0] != txnLockVersion { + return txnLock{}, errors.WithStack(errors.Newf("txn lock: unsupported version %d", b[0])) + } + r := bytes.NewReader(b[1:]) + var startTS uint64 + var ttlExpireAt uint64 + if err := binary.Read(r, binary.BigEndian, &startTS); err != nil { + return txnLock{}, errors.WithStack(err) + } + if err := binary.Read(r, binary.BigEndian, &ttlExpireAt); err != nil { + return txnLock{}, errors.WithStack(err) + } + flags, err := r.ReadByte() + if err != nil { + return txnLock{}, errors.WithStack(err) + } + var primaryLen uint64 + if err := binary.Read(r, binary.BigEndian, &primaryLen); err != nil { + return txnLock{}, errors.WithStack(err) + } + plen, err := u64ToInt(primaryLen) + if err != nil { + return txnLock{}, errors.WithStack(err) + } + if plen > r.Len() { + return txnLock{}, errors.New("txn lock: primary key truncated") + } + primaryKey := make([]byte, plen) + if _, err := r.Read(primaryKey); err != nil { + return txnLock{}, errors.WithStack(err) + } + return txnLock{ + StartTS: startTS, + TTLExpireAt: ttlExpireAt, + PrimaryKey: primaryKey, + IsPrimaryKey: (flags & txnLockFlagPrimary) != 0, + }, nil +} + +type txnIntent struct { + StartTS uint64 + Op byte // 0=put, 1=del + Value []byte +} + +const ( + txnIntentOpPut byte = 0 + txnIntentOpDel byte = 1 +) + +func encodeTxnIntent(i txnIntent) []byte { + var buf bytes.Buffer + buf.WriteByte(txnIntentVersion) + _ = binary.Write(&buf, binary.BigEndian, i.StartTS) + buf.WriteByte(i.Op) + valLen := uint64(len(i.Value)) + _ = binary.Write(&buf, binary.BigEndian, valLen) + if valLen > 0 { + buf.Write(i.Value) + } + return buf.Bytes() +} + +func decodeTxnIntent(b []byte) (txnIntent, error) { + if len(b) < 1 { + return txnIntent{}, errors.New("txn intent: empty") + } + if b[0] != txnIntentVersion { + return txnIntent{}, errors.WithStack(errors.Newf("txn intent: unsupported version %d", b[0])) + } + r := bytes.NewReader(b[1:]) + var startTS uint64 + if err := binary.Read(r, binary.BigEndian, &startTS); err != nil { + return txnIntent{}, errors.WithStack(err) + } + op, err := r.ReadByte() + if err != nil { + return txnIntent{}, errors.WithStack(err) + } + var valLen uint64 + if err := binary.Read(r, binary.BigEndian, &valLen); err != nil { + return txnIntent{}, errors.WithStack(err) + } + vlen, err := u64ToInt(valLen) + if err != nil { + return txnIntent{}, errors.WithStack(err) + } + if vlen > r.Len() { + return txnIntent{}, errors.New("txn intent: value truncated") + } + var val []byte + if vlen > 0 { + val = make([]byte, vlen) + if _, err := r.Read(val); err != nil { + return txnIntent{}, errors.WithStack(err) + } + } + return txnIntent{StartTS: startTS, Op: op, Value: val}, nil +} + +func encodeTxnCommitRecord(commitTS uint64) []byte { + var buf bytes.Buffer + buf.WriteByte(txnCommitVersion) + _ = binary.Write(&buf, binary.BigEndian, commitTS) + return buf.Bytes() +} + +func decodeTxnCommitRecord(b []byte) (uint64, error) { + if len(b) < 1 { + return 0, errors.New("txn commit record: empty") + } + if b[0] != txnCommitVersion { + return 0, errors.WithStack(errors.Newf("txn commit record: unsupported version %d", b[0])) + } + r := bytes.NewReader(b[1:]) + var commitTS uint64 + if err := binary.Read(r, binary.BigEndian, &commitTS); err != nil { + return 0, errors.WithStack(err) + } + return commitTS, nil +} + +func encodeTxnRollbackRecord() []byte { + return []byte{txnRollbackVersion} +} + +func u64ToInt(v uint64) (int, error) { + if strconv.IntSize == 32 && v > uint64(^uint32(0)>>1) { + return 0, errors.New("txn codec: length overflows int32") + } + if strconv.IntSize == 64 && v > (^uint64(0)>>1) { + return 0, errors.New("txn codec: length overflows int64") + } + return int(v), nil +} diff --git a/kv/txn_consts.go b/kv/txn_consts.go new file mode 100644 index 00000000..c16866e5 --- /dev/null +++ b/kv/txn_consts.go @@ -0,0 +1,13 @@ +package kv + +const ( + defaultTxnLockTTLms uint64 = 30_000 + + txnPrepareStoreMutationFactor = 2 + + txnCommitStoreMutationFactor = 3 + txnCommitStoreMutationSlack = 2 + + txnAbortStoreMutationFactor = 2 + txnAbortStoreMutationSlack = 1 +) diff --git a/kv/txn_errors.go b/kv/txn_errors.go new file mode 100644 index 00000000..768c6a35 --- /dev/null +++ b/kv/txn_errors.go @@ -0,0 +1,12 @@ +package kv + +import "github.com/cockroachdb/errors" + +var ( + ErrTxnMetaMissing = errors.New("txn meta missing") + ErrTxnInvalidMeta = errors.New("txn meta invalid") + ErrTxnLocked = errors.New("txn locked") + ErrTxnCommitTSRequired = errors.New("txn commit ts required") + ErrTxnAlreadyCommitted = errors.New("txn already committed") + ErrTxnPrimaryKeyRequired = errors.New("txn primary key required") +) diff --git a/kv/txn_keys.go b/kv/txn_keys.go new file mode 100644 index 00000000..0bcecb33 --- /dev/null +++ b/kv/txn_keys.go @@ -0,0 +1,83 @@ +package kv + +import ( + "bytes" + "encoding/binary" +) + +const ( + txnLockPrefix = "!txn|lock|" + txnIntentPrefix = "!txn|int|" + txnCommitPrefix = "!txn|cmt|" + txnRollbackPrefix = "!txn|rb|" + txnMetaPrefix = "!txn|meta|" +) + +const txnStartTSSuffixLen = 8 + +func txnLockKey(userKey []byte) []byte { + return append([]byte(txnLockPrefix), userKey...) +} + +func txnIntentKey(userKey []byte) []byte { + return append([]byte(txnIntentPrefix), userKey...) +} + +func txnCommitKey(primaryKey []byte, startTS uint64) []byte { + k := make([]byte, 0, len(txnCommitPrefix)+len(primaryKey)+txnStartTSSuffixLen) + k = append(k, txnCommitPrefix...) + k = append(k, primaryKey...) + var raw [txnStartTSSuffixLen]byte + binary.BigEndian.PutUint64(raw[:], startTS) + k = append(k, raw[:]...) + return k +} + +func txnRollbackKey(primaryKey []byte, startTS uint64) []byte { + k := make([]byte, 0, len(txnRollbackPrefix)+len(primaryKey)+txnStartTSSuffixLen) + k = append(k, txnRollbackPrefix...) + k = append(k, primaryKey...) + var raw [txnStartTSSuffixLen]byte + binary.BigEndian.PutUint64(raw[:], startTS) + k = append(k, raw[:]...) + return k +} + +func isTxnInternalKey(key []byte) bool { + return bytes.HasPrefix(key, []byte(txnLockPrefix)) || + bytes.HasPrefix(key, []byte(txnIntentPrefix)) || + bytes.HasPrefix(key, []byte(txnCommitPrefix)) || + bytes.HasPrefix(key, []byte(txnRollbackPrefix)) || + bytes.HasPrefix(key, []byte(txnMetaPrefix)) +} + +func isTxnMetaKey(key []byte) bool { + return bytes.HasPrefix(key, []byte(txnMetaPrefix)) +} + +// txnRouteKey strips transaction-internal key prefixes to recover the embedded +// logical user key for shard routing. +func txnRouteKey(key []byte) ([]byte, bool) { + switch { + case bytes.HasPrefix(key, []byte(txnLockPrefix)): + return key[len(txnLockPrefix):], true + case bytes.HasPrefix(key, []byte(txnIntentPrefix)): + return key[len(txnIntentPrefix):], true + case bytes.HasPrefix(key, []byte(txnMetaPrefix)): + return key[len(txnMetaPrefix):], true + case bytes.HasPrefix(key, []byte(txnCommitPrefix)): + rest := key[len(txnCommitPrefix):] + if len(rest) < txnStartTSSuffixLen { + return nil, false + } + return rest[:len(rest)-txnStartTSSuffixLen], true + case bytes.HasPrefix(key, []byte(txnRollbackPrefix)): + rest := key[len(txnRollbackPrefix):] + if len(rest) < txnStartTSSuffixLen { + return nil, false + } + return rest[:len(rest)-txnStartTSSuffixLen], true + default: + return nil, false + } +} diff --git a/store/lsm_store.go b/store/lsm_store.go index b5924931..eab0f29f 100644 --- a/store/lsm_store.go +++ b/store/lsm_store.go @@ -191,9 +191,6 @@ func (s *pebbleStore) updateLastCommitTS(ts uint64) { func (s *pebbleStore) alignCommitTS(commitTS uint64) uint64 { s.mtx.Lock() defer s.mtx.Unlock() - if commitTS <= s.lastCommitTS { - commitTS = s.lastCommitTS + 1 - } s.updateLastCommitTS(commitTS) return commitTS } diff --git a/store/mvcc_store.go b/store/mvcc_store.go index 19e589aa..2b21b28d 100644 --- a/store/mvcc_store.go +++ b/store/mvcc_store.go @@ -9,6 +9,7 @@ import ( "io" "log/slog" "os" + "sort" "sync" "github.com/cockroachdb/errors" @@ -136,7 +137,7 @@ func (s *mvccStore) putVersionLocked(key, value []byte, commitTS, expireAt uint6 if existing != nil { versions, _ = existing.([]VersionedValue) } - versions = append(versions, VersionedValue{ + versions = insertVersionSorted(versions, VersionedValue{ TS: commitTS, Value: bytes.Clone(value), Tombstone: false, @@ -151,7 +152,7 @@ func (s *mvccStore) deleteVersionLocked(key []byte, commitTS uint64) { if existing != nil { versions, _ = existing.([]VersionedValue) } - versions = append(versions, VersionedValue{ + versions = insertVersionSorted(versions, VersionedValue{ TS: commitTS, Value: nil, Tombstone: true, @@ -160,6 +161,20 @@ func (s *mvccStore) deleteVersionLocked(key []byte, commitTS uint64) { s.tree.Put(bytes.Clone(key), versions) } +func insertVersionSorted(versions []VersionedValue, vv VersionedValue) []VersionedValue { + // Keep versions sorted by TS ascending so lookups can assume max TS is last. + i := sort.Search(len(versions), func(i int) bool { return versions[i].TS >= vv.TS }) + if i < len(versions) && versions[i].TS == vv.TS { + // Idempotence: overwrite same timestamp. + versions[i] = vv + return versions + } + versions = append(versions, VersionedValue{}) + copy(versions[i+1:], versions[i:]) + versions[i] = vv + return versions +} + func (s *mvccStore) PutAt(ctx context.Context, key []byte, value []byte, commitTS uint64, expireAt uint64) error { s.mtx.Lock() defer s.mtx.Unlock() @@ -192,12 +207,10 @@ func (s *mvccStore) readTS() uint64 { } func (s *mvccStore) alignCommitTS(commitTS uint64) uint64 { - ts := commitTS - if ts <= s.lastCommitTS { - ts = s.lastCommitTS + 1 + if commitTS > s.lastCommitTS { + s.lastCommitTS = commitTS } - s.lastCommitTS = ts - return ts + return commitTS } func (s *mvccStore) latestVersionLocked(key []byte) (VersionedValue, bool) { diff --git a/store/mvcc_store_prop_test.go b/store/mvcc_store_prop_test.go index c2ebf8a7..748f45b3 100644 --- a/store/mvcc_store_prop_test.go +++ b/store/mvcc_store_prop_test.go @@ -60,7 +60,9 @@ func TestMVCCStore_Property_Delete(t *testing.T) { actualPutTS := s.LastCommitTS() // 2. Delete - delTS := rapid.Uint64Range(actualPutTS, ^uint64(0)-1).Draw(t, "delTS") + // Delete must be strictly after the put. If timestamps are equal, the + // later write wins and the value is not visible at that timestamp. + delTS := rapid.Uint64Range(actualPutTS+1, ^uint64(0)-1).Draw(t, "delTS") err = s.DeleteAt(ctx, key, delTS) require.NoError(t, err) actualDelTS := s.LastCommitTS() From fda8142664785b35fb54c3da4c0f20c641621ba9 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Sun, 15 Feb 2026 21:02:33 +0900 Subject: [PATCH 02/20] Fix concurrency issues with MULTI/EXEC commands --- adapter/redis.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/adapter/redis.go b/adapter/redis.go index dd85b496..cee9b0aa 100644 --- a/adapter/redis.go +++ b/adapter/redis.go @@ -150,7 +150,9 @@ func (r *RedisServer) Run() error { name := strings.ToUpper(string(cmd.Args[0])) if state.inTxn && name != cmdExec && name != cmdDiscard && name != cmdMulti { - state.queue = append(state.queue, cmd) + // redcon reuses the underlying argument buffers; copy queued commands + // so MULTI/EXEC works reliably under concurrency and with -race. + state.queue = append(state.queue, cloneCommand(cmd)) conn.WriteString("QUEUED") return } @@ -170,6 +172,17 @@ func (r *RedisServer) Run() error { return errors.WithStack(err) } +func cloneCommand(cmd redcon.Command) redcon.Command { + out := redcon.Command{ + Raw: bytes.Clone(cmd.Raw), + Args: make([][]byte, len(cmd.Args)), + } + for i := range cmd.Args { + out.Args[i] = bytes.Clone(cmd.Args[i]) + } + return out +} + func (r *RedisServer) Stop() { _ = r.listen.Close() } @@ -233,8 +246,14 @@ func (r *RedisServer) get(conn redcon.Conn, cmd redcon.Command) { return } + key := cmd.Args[1] readTS := r.readTS() - v, err := r.readValueAt(cmd.Args[1], readTS) + // When proxying reads to the leader, let the leader choose a safe snapshot. + // Our local store watermark may lag behind a just-committed transaction. + if !r.coordinator.IsLeaderForKey(key) { + readTS = 0 + } + v, err := r.readValueAt(key, readTS) if err != nil { switch { case errors.Is(err, store.ErrKeyNotFound): @@ -1156,7 +1175,9 @@ func (r *RedisServer) tryLeaderGetAt(key []byte, ts uint64) ([]byte, error) { if err != nil { return nil, errors.WithStack(err) } - + if resp.Value == nil { + return nil, errors.WithStack(store.ErrKeyNotFound) + } return resp.Value, nil } From 35f7783eebb355b44000d4d43d02a3d05ad438dc Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Sun, 15 Feb 2026 23:57:29 +0900 Subject: [PATCH 03/20] Refactor transaction handling and error checks --- adapter/internal.go | 27 ++++-- adapter/redis_test.go | 2 +- kv/coordinator.go | 27 +----- kv/fsm.go | 22 +---- kv/shard_store.go | 87 +++++++++++++++--- kv/shard_store_txn_lock_test.go | 153 ++++++++++++++++++++++++++++++++ kv/sharded_coordinator.go | 33 ++++--- kv/sharded_integration_test.go | 2 +- kv/transaction.go | 6 +- 9 files changed, 279 insertions(+), 80 deletions(-) create mode 100644 kv/shard_store_txn_lock_test.go diff --git a/adapter/internal.go b/adapter/internal.go index 48f19cfd..adb47b52 100644 --- a/adapter/internal.go +++ b/adapter/internal.go @@ -128,7 +128,12 @@ func forwardedTxnMetaMutation(r *pb.Request, metaPrefix []byte) (*pb.Mutation, b func (i *Internal) fillForwardedTxnCommitTS(reqs []*pb.Request, startTS uint64) { const metaPrefix = "!txn|meta|" - metaMutations := make([]*pb.Mutation, 0, len(reqs)) + type metaToUpdate struct { + m *pb.Mutation + meta kv.TxnMeta + } + + metaMutations := make([]metaToUpdate, 0, len(reqs)) prefix := []byte(metaPrefix) for _, r := range reqs { m, ok := forwardedTxnMetaMutation(r, prefix) @@ -142,24 +147,28 @@ func (i *Internal) fillForwardedTxnCommitTS(reqs []*pb.Request, startTS uint64) if meta.CommitTS != 0 { continue } - metaMutations = append(metaMutations, m) + metaMutations = append(metaMutations, metaToUpdate{m: m, meta: meta}) } if len(metaMutations) == 0 { return } commitTS := startTS + 1 + if commitTS == 0 { + // Overflow: can't choose a commit timestamp strictly greater than startTS. + return + } if i.clock != nil { i.clock.Observe(startTS) commitTS = i.clock.Next() } + if commitTS <= startTS { + // Defensive: avoid writing an invalid CommitTS. + return + } - for _, m := range metaMutations { - meta, err := kv.DecodeTxnMeta(m.Value) - if err != nil { - continue - } - meta.CommitTS = commitTS - m.Value = kv.EncodeTxnMeta(meta) + for _, item := range metaMutations { + item.meta.CommitTS = commitTS + item.m.Value = kv.EncodeTxnMeta(item.meta) } } diff --git a/adapter/redis_test.go b/adapter/redis_test.go index a29386e5..3817033d 100644 --- a/adapter/redis_test.go +++ b/adapter/redis_test.go @@ -68,7 +68,7 @@ func TestRedis_follower_redirect_node_set_get_deleted(t *testing.T) { assert.Equal(t, int64(1), res3.Val()) res4 := rdb.Get(ctx, string(key)) - assert.NoError(t, res4.Err()) + assert.Equal(t, redis.Nil, res4.Err()) assert.Equal(t, "", res4.Val()) } diff --git a/kv/coordinator.go b/kv/coordinator.go index d58e743f..a75c1a05 100644 --- a/kv/coordinator.go +++ b/kv/coordinator.go @@ -1,6 +1,7 @@ package kv import ( + "bytes" "context" "sort" @@ -297,30 +298,6 @@ func primaryKeyForElems(reqs []*Elem[OP]) []byte { if len(keys) == 0 { return nil } - sort.Slice(keys, func(i, j int) bool { return bytesCompare(keys[i], keys[j]) < 0 }) + sort.Slice(keys, func(i, j int) bool { return bytes.Compare(keys[i], keys[j]) < 0 }) return keys[0] } - -func bytesCompare(a, b []byte) int { - min := len(a) - if len(b) < min { - min = len(b) - } - for i := 0; i < min; i++ { - if a[i] == b[i] { - continue - } - if a[i] < b[i] { - return -1 - } - return 1 - } - switch { - case len(a) < len(b): - return -1 - case len(a) > len(b): - return 1 - default: - return 0 - } -} diff --git a/kv/fsm.go b/kv/fsm.go index 3d5f290b..731194d0 100644 --- a/kv/fsm.go +++ b/kv/fsm.go @@ -48,7 +48,7 @@ func (f *kvFSM) Apply(l *raft.Log) interface{} { commitTS := r.Ts if r.IsTxn && (r.Phase == pb.Phase_COMMIT || r.Phase == pb.Phase_ABORT) { - meta, _, err := splitTxnMeta(r.Mutations) + meta, _, err := extractTxnMeta(r.Mutations) if err != nil { return errors.WithStack(err) } @@ -173,7 +173,7 @@ func uniqueMutations(muts []*pb.Mutation) ([]*pb.Mutation, error) { } func (f *kvFSM) handlePrepareRequest(ctx context.Context, r *pb.Request) error { - meta, muts, err := splitTxnMeta(r.Mutations) + meta, muts, err := extractTxnMeta(r.Mutations) if err != nil { return err } @@ -212,7 +212,7 @@ func (f *kvFSM) handlePrepareRequest(ctx context.Context, r *pb.Request) error { } func (f *kvFSM) handleCommitRequest(ctx context.Context, r *pb.Request) error { - meta, muts, err := splitTxnMeta(r.Mutations) + meta, muts, err := extractTxnMeta(r.Mutations) if err != nil { return err } @@ -241,7 +241,7 @@ func (f *kvFSM) handleCommitRequest(ctx context.Context, r *pb.Request) error { } func (f *kvFSM) handleAbortRequest(ctx context.Context, r *pb.Request, abortTS uint64) error { - meta, muts, err := splitTxnMeta(r.Mutations) + meta, muts, err := extractTxnMeta(r.Mutations) if err != nil { return err } @@ -486,20 +486,6 @@ func (f *kvFSM) shouldClearAbortKey(ctx context.Context, key []byte, startTS uin return true, nil } -func splitTxnMeta(muts []*pb.Mutation) (TxnMeta, []*pb.Mutation, error) { - if len(muts) == 0 || muts[0] == nil || len(muts[0].Key) == 0 { - return TxnMeta{}, nil, errors.WithStack(ErrTxnMetaMissing) - } - if !isTxnMetaKey(muts[0].Key) { - return TxnMeta{}, nil, errors.WithStack(ErrTxnMetaMissing) - } - meta, err := DecodeTxnMeta(muts[0].Value) - if err != nil { - return TxnMeta{}, nil, errors.WithStack(errors.Wrap(err, "decode txn meta")) - } - return meta, muts[1:], nil -} - func (f *kvFSM) assertNoConflictingTxnLock(ctx context.Context, key []byte, startTS uint64) error { lockBytes, err := f.store.GetAt(ctx, txnLockKey(key), ^uint64(0)) if err != nil { diff --git a/kv/shard_store.go b/kv/shard_store.go index 3de44299..4298f866 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -159,7 +159,8 @@ func (s *ShardStore) scanRouteAt(ctx context.Context, route distribution.Route, if err != nil { return nil, errors.WithStack(err) } - return kvs, nil + // Keep ScanAt behavior consistent even when running without raft. + return filterTxnInternalKVs(kvs), nil } // Reads should come from the shard's leader to avoid returning stale or @@ -170,7 +171,7 @@ func (s *ShardStore) scanRouteAt(ctx context.Context, route distribution.Route, if err != nil { return nil, errors.WithStack(err) } - return s.resolveScanLocks(ctx, kvs, ts) + return s.resolveScanLocks(ctx, g, kvs, ts) } } @@ -178,7 +179,9 @@ func (s *ShardStore) scanRouteAt(ctx context.Context, route distribution.Route, if err != nil { return nil, err } - return s.resolveScanLocks(ctx, kvs, ts) + // The leader's RawScanAt is expected to perform lock resolution and filtering + // via ShardStore.ScanAt, so avoid N+1 proxy gets here. + return filterTxnInternalKVs(kvs), nil } func (s *ShardStore) groupForID(groupID uint64) (*ShardGroup, bool) { @@ -322,29 +325,85 @@ func (s *ShardStore) maybeResolveTxnLock(ctx context.Context, g *ShardGroup, key } } -func (s *ShardStore) resolveScanLocks(ctx context.Context, kvs []*store.KVPair, ts uint64) ([]*store.KVPair, error) { +func (s *ShardStore) resolveScanLocks(ctx context.Context, g *ShardGroup, kvs []*store.KVPair, ts uint64) ([]*store.KVPair, error) { if len(kvs) == 0 { return kvs, nil } + if g == nil || g.Store == nil { + return []*store.KVPair{}, nil + } + out := make([]*store.KVPair, 0, len(kvs)) + for _, kvp := range kvs { + resolved, skip, err := s.resolveScanKVP(ctx, g, kvp, ts) + if err != nil { + return nil, err + } + if skip { + continue + } + out = append(out, resolved) + } + return out, nil +} + +func (s *ShardStore) resolveScanKVP(ctx context.Context, g *ShardGroup, kvp *store.KVPair, ts uint64) (*store.KVPair, bool, error) { + if kvp == nil { + return nil, true, nil + } + // Filter txn-internal keys from user-facing scans. + if isTxnInternalKey(kvp.Key) { + return nil, true, nil + } + + // Fast-path: if there's no lock visible at this read timestamp, use the scan + // value directly instead of re-reading it. + locked, err := scanKeyLockedAt(ctx, g, kvp.Key, ts) + if err != nil { + return nil, false, err + } + if !locked { + return kvp, false, nil + } + + v, err := s.leaderGetAt(ctx, g, kvp.Key, ts) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return nil, true, nil + } + return nil, false, err + } + return &store.KVPair{Key: kvp.Key, Value: v}, false, nil +} + +func scanKeyLockedAt(ctx context.Context, g *ShardGroup, key []byte, ts uint64) (bool, error) { + if g == nil || g.Store == nil { + return false, nil + } + _, err := g.Store.GetAt(ctx, txnLockKey(key), ts) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + return false, nil + } + return false, errors.WithStack(err) + } + return true, nil +} + +func filterTxnInternalKVs(kvs []*store.KVPair) []*store.KVPair { + if len(kvs) == 0 { + return kvs + } out := make([]*store.KVPair, 0, len(kvs)) for _, kvp := range kvs { if kvp == nil { continue } - // Filter txn-internal keys from user-facing scans. if isTxnInternalKey(kvp.Key) { continue } - v, err := s.GetAt(ctx, kvp.Key, ts) - if err != nil { - if errors.Is(err, store.ErrKeyNotFound) { - continue - } - return nil, err - } - out = append(out, &store.KVPair{Key: kvp.Key, Value: v}) + out = append(out, kvp) } - return out, nil + return out } type txnStatus int diff --git a/kv/shard_store_txn_lock_test.go b/kv/shard_store_txn_lock_test.go new file mode 100644 index 00000000..f5310df3 --- /dev/null +++ b/kv/shard_store_txn_lock_test.go @@ -0,0 +1,153 @@ +package kv + +import ( + "context" + "testing" + + "github.com/bootjp/elastickv/distribution" + pb "github.com/bootjp/elastickv/proto" + "github.com/bootjp/elastickv/store" + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" +) + +func TestShardStoreGetAt_ReturnsTxnLockedForPendingLock(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSM(st1)) + defer stop1() + + groups := map[uint64]*ShardGroup{ + 1: {Raft: r1, Store: st1, Txn: NewLeaderProxy(r1)}, + } + shardStore := NewShardStore(engine, groups) + + startTS := uint64(1) + key := []byte("k") + + prepare := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: key, LockTTLms: defaultTxnLockTTLms, CommitTS: 0})}, + {Op: pb.Op_PUT, Key: key, Value: []byte("v")}, + }, + } + _, err := groups[1].Txn.Commit([]*pb.Request{prepare}) + require.NoError(t, err) + + _, err = shardStore.GetAt(ctx, key, ^uint64(0)) + require.Error(t, err) + require.True(t, errors.Is(err, ErrTxnLocked), "expected ErrTxnLocked, got %v", err) +} + +func TestShardStoreGetAt_ResolvesCommittedSecondaryLock(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte("a"), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 2) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSM(st1)) + defer stop1() + + st2 := store.NewMVCCStore() + r2, stop2 := newSingleRaft(t, "g2", NewKvFSM(st2)) + defer stop2() + + groups := map[uint64]*ShardGroup{ + 1: {Raft: r1, Store: st1, Txn: NewLeaderProxy(r1)}, + 2: {Raft: r2, Store: st2, Txn: NewLeaderProxy(r2)}, + } + shardStore := NewShardStore(engine, groups) + + startTS := uint64(1) + commitTS := uint64(2) + + primaryKey := []byte("b") // group 1 + secondaryKey := []byte("x") + + prepareMeta := func() *pb.Mutation { + return &pb.Mutation{ + Op: pb.Op_PUT, + Key: []byte(txnMetaPrefix), + Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: defaultTxnLockTTLms, CommitTS: 0}), + } + } + + preparePrimary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + prepareMeta(), + {Op: pb.Op_PUT, Key: primaryKey, Value: []byte("v1")}, + }, + } + prepareSecondary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + prepareMeta(), + {Op: pb.Op_PUT, Key: secondaryKey, Value: []byte("v2")}, + }, + } + + _, err := groups[1].Txn.Commit([]*pb.Request{preparePrimary}) + require.NoError(t, err) + _, err = groups[2].Txn.Commit([]*pb.Request{prepareSecondary}) + require.NoError(t, err) + + commitPrimary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: 0, CommitTS: commitTS})}, + {Op: pb.Op_PUT, Key: primaryKey}, + }, + } + _, err = groups[1].Txn.Commit([]*pb.Request{commitPrimary}) + require.NoError(t, err) + + // Reading the secondary key should resolve it based on the primary commit record. + v, err := shardStore.GetAt(ctx, secondaryKey, commitTS) + require.NoError(t, err) + require.Equal(t, "v2", string(v)) +} + +func TestShardStoreScanAt_FiltersTxnInternalKeysWithoutRaft(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + + st1 := store.NewMVCCStore() + groups := map[uint64]*ShardGroup{ + 1: {Store: st1}, + } + shardStore := NewShardStore(engine, groups) + + userKey := []byte("k") + require.NoError(t, st1.PutAt(ctx, txnLockKey(userKey), []byte("lock"), 1, 0)) + require.NoError(t, st1.PutAt(ctx, userKey, []byte("v"), 1, 0)) + + kvs, err := shardStore.ScanAt(ctx, []byte(""), nil, 100, ^uint64(0)) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, userKey, kvs[0].Key) + require.Equal(t, []byte("v"), kvs[0].Value) +} diff --git a/kv/sharded_coordinator.go b/kv/sharded_coordinator.go index 917ecdbd..6fe07fa1 100644 --- a/kv/sharded_coordinator.go +++ b/kv/sharded_coordinator.go @@ -418,7 +418,11 @@ func (c *ShardedCoordinator) txnLogs(reqs *OperationGroup[OP]) ([]*pb.Request, e if len(gids) != 1 { return nil, errors.WithStack(ErrInvalidRequest) } - return buildTxnLogs(reqs.StartTS, grouped, gids), nil + commitTS := c.nextTxnTSAfter(reqs.StartTS) + if commitTS == 0 || commitTS <= reqs.StartTS { + return nil, errors.WithStack(ErrTxnCommitTSRequired) + } + return buildTxnLogs(reqs.StartTS, commitTS, grouped, gids) } func (c *ShardedCoordinator) groupMutations(reqs []*Elem[OP]) (map[uint64][]*pb.Mutation, []uint64, error) { @@ -442,17 +446,21 @@ func (c *ShardedCoordinator) groupMutations(reqs []*Elem[OP]) (map[uint64][]*pb. return grouped, gids, nil } -func buildTxnLogs(startTS uint64, grouped map[uint64][]*pb.Mutation, gids []uint64) []*pb.Request { +func buildTxnLogs(startTS uint64, commitTS uint64, grouped map[uint64][]*pb.Mutation, gids []uint64) ([]*pb.Request, error) { logs := make([]*pb.Request, 0, len(gids)*txnPhaseCount) for _, gid := range gids { muts := grouped[gid] + primaryKey := primaryKeyFromMutations(muts) + if len(primaryKey) == 0 { + return nil, errors.WithStack(ErrTxnPrimaryKeyRequired) + } logs = append(logs, &pb.Request{ IsTxn: true, Phase: pb.Phase_PREPARE, Ts: startTS, Mutations: append([]*pb.Mutation{ - {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKeyFromMutations(muts), LockTTLms: defaultTxnLockTTLms})}, + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: defaultTxnLockTTLms, CommitTS: 0})}, }, muts...), }, &pb.Request{ @@ -460,24 +468,27 @@ func buildTxnLogs(startTS uint64, grouped map[uint64][]*pb.Mutation, gids []uint Phase: pb.Phase_COMMIT, Ts: startTS, Mutations: append([]*pb.Mutation{ - {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKeyFromMutations(muts)})}, + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: 0, CommitTS: commitTS})}, }, keyMutations(muts)...), }, ) } - return logs + return logs, nil } func primaryKeyFromMutations(muts []*pb.Mutation) []byte { - if len(muts) == 0 { - return nil - } - primary := muts[0].Key - for _, m := range muts[1:] { + seen := map[string]struct{}{} + var primary []byte + for _, m := range muts { if m == nil || len(m.Key) == 0 { continue } - if bytes.Compare(m.Key, primary) < 0 { + k := string(m.Key) + if _, ok := seen[k]; ok { + continue + } + seen[k] = struct{}{} + if primary == nil || bytes.Compare(m.Key, primary) < 0 { primary = m.Key } } diff --git a/kv/sharded_integration_test.go b/kv/sharded_integration_test.go index a8caf302..02dd6cb2 100644 --- a/kv/sharded_integration_test.go +++ b/kv/sharded_integration_test.go @@ -106,7 +106,7 @@ func TestShardedCoordinatorDispatch(t *testing.T) { } } -func TestShardedCoordinatorDispatch_RejectsCrossShardTxn(t *testing.T) { +func TestShardedCoordinatorDispatch_CrossShardTxnSucceeds(t *testing.T) { ctx := context.Background() engine := distribution.NewEngine() diff --git a/kv/transaction.go b/kv/transaction.go index 6984deca..3ab23ffb 100644 --- a/kv/transaction.go +++ b/kv/transaction.go @@ -101,6 +101,10 @@ func (t *TransactionManager) Abort(reqs []*pb.Request) (*TransactionResponse, er } startTS := req.Ts abortTS := startTS + 1 + if abortTS == 0 { + // Overflow: can't choose an abort timestamp strictly greater than startTS. + continue + } meta.CommitTS = abortTS abortReqs = append(abortReqs, &pb.Request{ @@ -142,7 +146,7 @@ func extractTxnMeta(muts []*pb.Mutation) (TxnMeta, []*pb.Mutation, error) { } meta, err := DecodeTxnMeta(muts[0].Value) if err != nil { - return TxnMeta{}, nil, errors.WithStack(err) + return TxnMeta{}, nil, errors.WithStack(errors.Wrap(err, "decode txn meta")) } return meta, muts[1:], nil } From b7e219aad72995354e254d0958826862e599dde3 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Mon, 16 Feb 2026 01:14:56 +0900 Subject: [PATCH 04/20] kv/adapter: address review feedback --- adapter/internal.go | 4 +--- kv/coordinator.go | 15 ++++++--------- kv/sharded_coordinator.go | 23 +++++++++++++++++++++-- kv/txn_keys.go | 3 +++ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/adapter/internal.go b/adapter/internal.go index adb47b52..a31f06ff 100644 --- a/adapter/internal.go +++ b/adapter/internal.go @@ -126,15 +126,13 @@ func forwardedTxnMetaMutation(r *pb.Request, metaPrefix []byte) (*pb.Mutation, b } func (i *Internal) fillForwardedTxnCommitTS(reqs []*pb.Request, startTS uint64) { - const metaPrefix = "!txn|meta|" - type metaToUpdate struct { m *pb.Mutation meta kv.TxnMeta } metaMutations := make([]metaToUpdate, 0, len(reqs)) - prefix := []byte(metaPrefix) + prefix := []byte(kv.TxnMetaPrefix) for _, r := range reqs { m, ok := forwardedTxnMetaMutation(r, prefix) if !ok { diff --git a/kv/coordinator.go b/kv/coordinator.go index a75c1a05..06f81d7c 100644 --- a/kv/coordinator.go +++ b/kv/coordinator.go @@ -3,7 +3,6 @@ package kv import ( "bytes" "context" - "sort" pb "github.com/bootjp/elastickv/proto" "github.com/cockroachdb/errors" @@ -282,8 +281,8 @@ func txnRequests(startTS, commitTS, lockTTLms uint64, primaryKey []byte, reqs [] } func primaryKeyForElems(reqs []*Elem[OP]) []byte { - keys := make([][]byte, 0, len(reqs)) - seen := map[string]struct{}{} + var primary []byte + seen := make(map[string]struct{}, len(reqs)) for _, e := range reqs { if e == nil || len(e.Key) == 0 { continue @@ -293,11 +292,9 @@ func primaryKeyForElems(reqs []*Elem[OP]) []byte { continue } seen[k] = struct{}{} - keys = append(keys, e.Key) - } - if len(keys) == 0 { - return nil + if primary == nil || bytes.Compare(e.Key, primary) < 0 { + primary = e.Key + } } - sort.Slice(keys, func(i, j int) bool { return bytes.Compare(keys[i], keys[j]) < 0 }) - return keys[0] + return primary } diff --git a/kv/sharded_coordinator.go b/kv/sharded_coordinator.go index 6fe07fa1..d2eec374 100644 --- a/kv/sharded_coordinator.go +++ b/kv/sharded_coordinator.go @@ -3,6 +3,7 @@ package kv import ( "bytes" "context" + "log/slog" "sort" "github.com/bootjp/elastickv/distribution" @@ -168,6 +169,9 @@ func (c *ShardedCoordinator) commitPrimaryTxn(startTS uint64, primaryKey []byte, } func (c *ShardedCoordinator) commitSecondaryTxns(startTS uint64, primaryGid uint64, primaryKey []byte, grouped map[uint64][]*pb.Mutation, gids []uint64, commitTS uint64, maxIndex uint64) uint64 { + // Secondary commits are best-effort. If a shard is unavailable after the + // primary commits, read-time lock resolution will commit the remaining + // secondaries based on the primary commit record. meta := txnMetaMutation(primaryKey, 0, commitTS) for _, gid := range gids { if gid == primaryGid { @@ -183,7 +187,18 @@ func (c *ShardedCoordinator) commitSecondaryTxns(startTS uint64, primaryGid uint Ts: startTS, Mutations: append([]*pb.Mutation{meta}, keyMutations(grouped[gid])...), } - if r, err := g.Txn.Commit([]*pb.Request{req}); err == nil && r != nil && r.CommitIndex > maxIndex { + r, err := g.Txn.Commit([]*pb.Request{req}) + if err != nil { + slog.Warn("txn secondary commit failed", + slog.Uint64("gid", gid), + slog.String("primary_key", string(primaryKey)), + slog.Uint64("start_ts", startTS), + slog.Uint64("commit_ts", commitTS), + slog.Any("err", err), + ) + continue + } + if r != nil && r.CommitIndex > maxIndex { maxIndex = r.CommitIndex } } @@ -221,7 +236,11 @@ func (c *ShardedCoordinator) txnGroupForID(gid uint64) (*ShardGroup, error) { func (c *ShardedCoordinator) nextTxnTSAfter(startTS uint64) uint64 { if c.clock == nil { - return startTS + 1 + nextTS := startTS + 1 + if nextTS == 0 { + return startTS + } + return nextTS } ts := c.clock.Next() if ts <= startTS { diff --git a/kv/txn_keys.go b/kv/txn_keys.go index 0bcecb33..d9731192 100644 --- a/kv/txn_keys.go +++ b/kv/txn_keys.go @@ -13,6 +13,9 @@ const ( txnMetaPrefix = "!txn|meta|" ) +// TxnMetaPrefix is the key prefix used for transaction metadata mutations. +const TxnMetaPrefix = txnMetaPrefix + const txnStartTSSuffixLen = 8 func txnLockKey(userKey []byte) []byte { From daa1385a2847f6a606557a65e9bbee700e63da85 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Mon, 16 Feb 2026 21:09:36 +0900 Subject: [PATCH 05/20] kv/store: address latest PR review comments --- kv/coordinator.go | 3 +++ kv/sharded_coordinator.go | 34 ++++++++++++++++++++++++---------- store/mvcc_store.go | 3 +++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/kv/coordinator.go b/kv/coordinator.go index 06f81d7c..28a0accd 100644 --- a/kv/coordinator.go +++ b/kv/coordinator.go @@ -198,6 +198,9 @@ func (c *Coordinate) redirect(ctx context.Context, reqs *OperationGroup[OP]) (*C var requests []*pb.Request if reqs.IsTxn { primary := primaryKeyForElems(reqs.Elems) + if len(primary) == 0 { + return nil, errors.WithStack(ErrTxnPrimaryKeyRequired) + } requests = txnRequests(reqs.StartTS, 0, defaultTxnLockTTLms, primary, reqs.Elems) } else { for _, req := range reqs.Elems { diff --git a/kv/sharded_coordinator.go b/kv/sharded_coordinator.go index d2eec374..6803e0fa 100644 --- a/kv/sharded_coordinator.go +++ b/kv/sharded_coordinator.go @@ -92,15 +92,19 @@ func (c *ShardedCoordinator) dispatchTxn(startTS uint64, elems []*Elem[OP]) (*Co return nil, errors.WithStack(ErrTxnPrimaryKeyRequired) } - prepared, err := c.prewriteTxn(startTS, primaryKey, grouped, gids) + commitTS := c.nextTxnTSAfter(startTS) + if commitTS == 0 || commitTS <= startTS { + return nil, errors.WithStack(ErrTxnCommitTSRequired) + } + + prepared, err := c.prewriteTxn(startTS, commitTS, primaryKey, grouped, gids) if err != nil { return nil, err } - commitTS := c.nextTxnTSAfter(startTS) primaryGid, maxIndex, err := c.commitPrimaryTxn(startTS, primaryKey, grouped, commitTS) if err != nil { - c.abortPreparedTxn(startTS, primaryKey, prepared, abortTSFrom(commitTS)) + c.abortPreparedTxn(startTS, primaryKey, prepared, abortTSFrom(startTS, commitTS)) return nil, errors.WithStack(err) } @@ -113,7 +117,7 @@ type preparedGroup struct { keys []*pb.Mutation } -func (c *ShardedCoordinator) prewriteTxn(startTS uint64, primaryKey []byte, grouped map[uint64][]*pb.Mutation, gids []uint64) ([]preparedGroup, error) { +func (c *ShardedCoordinator) prewriteTxn(startTS, commitTS uint64, primaryKey []byte, grouped map[uint64][]*pb.Mutation, gids []uint64) ([]preparedGroup, error) { prepareMeta := txnMetaMutation(primaryKey, defaultTxnLockTTLms, 0) prepared := make([]preparedGroup, 0, len(gids)) @@ -129,7 +133,7 @@ func (c *ShardedCoordinator) prewriteTxn(startTS uint64, primaryKey []byte, grou Mutations: append([]*pb.Mutation{prepareMeta}, grouped[gid]...), } if _, err := g.Txn.Commit([]*pb.Request{req}); err != nil { - c.abortPreparedTxn(startTS, primaryKey, prepared, c.nextTxnTSAfter(startTS)) + c.abortPreparedTxn(startTS, primaryKey, prepared, abortTSFrom(startTS, commitTS)) return nil, errors.WithStack(err) } prepared = append(prepared, preparedGroup{gid: gid, keys: keyMutations(grouped[gid])}) @@ -209,6 +213,9 @@ func (c *ShardedCoordinator) abortPreparedTxn(startTS uint64, primaryKey []byte, if len(prepared) == 0 { return } + if abortTS == 0 || abortTS <= startTS { + return + } meta := txnMetaMutation(primaryKey, 0, abortTS) for _, pg := range prepared { @@ -238,7 +245,7 @@ func (c *ShardedCoordinator) nextTxnTSAfter(startTS uint64) uint64 { if c.clock == nil { nextTS := startTS + 1 if nextTS == 0 { - return startTS + return 0 } return nextTS } @@ -247,15 +254,22 @@ func (c *ShardedCoordinator) nextTxnTSAfter(startTS uint64) uint64 { c.clock.Observe(startTS) ts = c.clock.Next() } + if ts <= startTS { + return 0 + } return ts } -func abortTSFrom(commitTS uint64) uint64 { +func abortTSFrom(startTS, commitTS uint64) uint64 { abortTS := commitTS + 1 - if abortTS == 0 { - return commitTS + if abortTS > startTS { + return abortTS + } + fallback := startTS + 1 + if fallback == 0 { + return 0 } - return abortTS + return fallback } func txnMetaMutation(primaryKey []byte, lockTTLms uint64, commitTS uint64) *pb.Mutation { diff --git a/store/mvcc_store.go b/store/mvcc_store.go index 2b21b28d..34c9f1d1 100644 --- a/store/mvcc_store.go +++ b/store/mvcc_store.go @@ -163,6 +163,9 @@ func (s *mvccStore) deleteVersionLocked(key []byte, commitTS uint64) { func insertVersionSorted(versions []VersionedValue, vv VersionedValue) []VersionedValue { // Keep versions sorted by TS ascending so lookups can assume max TS is last. + if n := len(versions); n == 0 || versions[n-1].TS < vv.TS { + return append(versions, vv) + } i := sort.Search(len(versions), func(i int) bool { return versions[i].TS >= vv.TS }) if i < len(versions) && versions[i].TS == vv.TS { // Idempotence: overwrite same timestamp. From bf234fdcba955911cecfa125748b035e058cc1f7 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Tue, 17 Feb 2026 04:30:44 +0900 Subject: [PATCH 06/20] kv/fsm: harden txn key dedupe and lock validation --- kv/fsm.go | 28 +++++++++--- kv/fsm_txn_test.go | 112 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 kv/fsm_txn_test.go diff --git a/kv/fsm.go b/kv/fsm.go index 731194d0..adae4f4a 100644 --- a/kv/fsm.go +++ b/kv/fsm.go @@ -157,8 +157,11 @@ func uniqueMutations(muts []*pb.Mutation) ([]*pb.Mutation, error) { return []*pb.Mutation{}, nil } seen := make(map[string]struct{}, len(muts)) - out := make([]*pb.Mutation, 0, len(muts)) - for _, mut := range muts { + reversed := make([]*pb.Mutation, 0, len(muts)) + // Keep the last mutation per key to avoid dropping final operations like + // PUT followed by DEL in the same transactional batch. + for i := len(muts) - 1; i >= 0; i-- { + mut := muts[i] if mut == nil || len(mut.Key) == 0 { return nil, errors.WithStack(ErrInvalidRequest) } @@ -167,7 +170,12 @@ func uniqueMutations(muts []*pb.Mutation) ([]*pb.Mutation, error) { continue } seen[keyStr] = struct{}{} - out = append(out, mut) + reversed = append(reversed, mut) + } + + out := make([]*pb.Mutation, 0, len(reversed)) + for i := len(reversed) - 1; i >= 0; i-- { + out = append(out, reversed[i]) } return out, nil } @@ -298,7 +306,7 @@ func (f *kvFSM) buildCommitStoreMutations(ctx context.Context, muts []*pb.Mutati committingPrimary = true } - keyMuts, err := f.commitTxnKeyMutations(ctx, key, startTS) + keyMuts, err := f.commitTxnKeyMutations(ctx, key, meta.PrimaryKey, startTS) if err != nil { return nil, err } @@ -325,7 +333,7 @@ func (f *kvFSM) buildAbortCleanupStoreMutations(ctx context.Context, muts []*pb. abortingPrimary = true } - shouldClear, err := f.shouldClearAbortKey(ctx, key, startTS) + shouldClear, err := f.shouldClearAbortKey(ctx, key, primaryKey, startTS) if err != nil { return nil, false, err } @@ -435,7 +443,7 @@ func storeMutationForIntent(key []byte, intent txnIntent) (*store.KVPairMutation } } -func (f *kvFSM) commitTxnKeyMutations(ctx context.Context, key []byte, startTS uint64) ([]*store.KVPairMutation, error) { +func (f *kvFSM) commitTxnKeyMutations(ctx context.Context, key, primaryKey []byte, startTS uint64) ([]*store.KVPairMutation, error) { lock, ok, err := f.txnLockForCommit(ctx, key) if err != nil { return nil, err @@ -447,6 +455,9 @@ func (f *kvFSM) commitTxnKeyMutations(ctx context.Context, key []byte, startTS u if lock.StartTS != startTS { return nil, errors.Wrapf(ErrTxnLocked, "key: %s", string(key)) } + if !bytes.Equal(lock.PrimaryKey, primaryKey) { + return nil, errors.Wrapf(ErrTxnInvalidMeta, "lock primary_key mismatch for key %s", string(key)) + } intent, ok, err := f.txnIntentForCommit(ctx, key) if err != nil { @@ -468,7 +479,7 @@ func (f *kvFSM) commitTxnKeyMutations(ctx context.Context, key []byte, startTS u return out, nil } -func (f *kvFSM) shouldClearAbortKey(ctx context.Context, key []byte, startTS uint64) (bool, error) { +func (f *kvFSM) shouldClearAbortKey(ctx context.Context, key, primaryKey []byte, startTS uint64) (bool, error) { lockBytes, err := f.store.GetAt(ctx, txnLockKey(key), ^uint64(0)) if err != nil { if errors.Is(err, store.ErrKeyNotFound) { @@ -483,6 +494,9 @@ func (f *kvFSM) shouldClearAbortKey(ctx context.Context, key []byte, startTS uin if lock.StartTS != startTS { return false, nil } + if !bytes.Equal(lock.PrimaryKey, primaryKey) { + return false, errors.Wrapf(ErrTxnInvalidMeta, "abort primary_key mismatch for key %s", string(key)) + } return true, nil } diff --git a/kv/fsm_txn_test.go b/kv/fsm_txn_test.go new file mode 100644 index 00000000..bbfeea64 --- /dev/null +++ b/kv/fsm_txn_test.go @@ -0,0 +1,112 @@ +package kv + +import ( + "context" + "fmt" + "testing" + + pb "github.com/bootjp/elastickv/proto" + "github.com/bootjp/elastickv/store" + "github.com/hashicorp/raft" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func applyFSMRequest(t *testing.T, fsm *kvFSM, req *pb.Request) error { + t.Helper() + + data, err := proto.Marshal(req) + require.NoError(t, err) + + resp := fsm.Apply(&raft.Log{Type: raft.LogCommand, Data: data}) + if resp == nil { + return nil + } + if err, ok := resp.(error); ok { + return err + } + return fmt.Errorf("unexpected apply response type: %T", resp) +} + +func TestTxnDuplicateMutations_LastWriteWins(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := store.NewMVCCStore() + fsm, ok := NewKvFSM(st).(*kvFSM) + require.True(t, ok) + + startTS := uint64(10) + commitTS := uint64(20) + key := []byte("k") + + prepare := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: key, LockTTLms: defaultTxnLockTTLms})}, + {Op: pb.Op_PUT, Key: key, Value: []byte("v1")}, + {Op: pb.Op_DEL, Key: key}, + }, + } + require.NoError(t, applyFSMRequest(t, fsm, prepare)) + + commit := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: key, CommitTS: commitTS})}, + {Op: pb.Op_PUT, Key: key}, + }, + } + require.NoError(t, applyFSMRequest(t, fsm, commit)) + + _, err := st.GetAt(ctx, key, ^uint64(0)) + require.ErrorIs(t, err, store.ErrKeyNotFound) +} + +func TestCommitRejectsMismatchedPrimaryKey(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := store.NewMVCCStore() + fsm, ok := NewKvFSM(st).(*kvFSM) + require.True(t, ok) + + startTS := uint64(11) + commitTS := uint64(21) + key := []byte("k") + primary := []byte("p1") + wrongPrimary := []byte("p2") + + prepare := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primary, LockTTLms: defaultTxnLockTTLms})}, + {Op: pb.Op_PUT, Key: key, Value: []byte("v1")}, + }, + } + require.NoError(t, applyFSMRequest(t, fsm, prepare)) + + commit := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: wrongPrimary, CommitTS: commitTS})}, + {Op: pb.Op_PUT, Key: key}, + }, + } + err := applyFSMRequest(t, fsm, commit) + require.Error(t, err) + require.ErrorIs(t, err, ErrTxnInvalidMeta) + + _, err = st.GetAt(ctx, key, ^uint64(0)) + require.ErrorIs(t, err, store.ErrKeyNotFound) + _, err = st.GetAt(ctx, txnLockKey(key), ^uint64(0)) + require.NoError(t, err) +} From e2a20b072e33543c7d0eab4576d32a70b0d352fb Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Tue, 17 Feb 2026 04:34:15 +0900 Subject: [PATCH 07/20] kv: reduce scan lock lookup overhead --- kv/shard_store.go | 50 +++++++++++++++++++++++---------------- kv/sharded_coordinator.go | 10 ++++---- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/kv/shard_store.go b/kv/shard_store.go index 4298f866..34d3f09f 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -296,18 +296,36 @@ func (s *ShardStore) proxyLatestCommitTS(ctx context.Context, g *ShardGroup, key } func (s *ShardStore) maybeResolveTxnLock(ctx context.Context, g *ShardGroup, key []byte, readTS uint64) error { - // Only consider locks visible at the read timestamp. - lockBytes, err := g.Store.GetAt(ctx, txnLockKey(key), readTS) + lock, ok, err := loadTxnLockAt(ctx, g, key, readTS) + if err != nil { + return err + } + if !ok { + return nil + } + return s.resolveTxnLockForKey(ctx, g, key, lock) +} + +func loadTxnLockAt(ctx context.Context, g *ShardGroup, key []byte, ts uint64) (txnLock, bool, error) { + if g == nil || g.Store == nil { + return txnLock{}, false, nil + } + // Only consider locks visible at the provided read timestamp. + lockBytes, err := g.Store.GetAt(ctx, txnLockKey(key), ts) if err != nil { if errors.Is(err, store.ErrKeyNotFound) { - return nil + return txnLock{}, false, nil } - return errors.WithStack(err) + return txnLock{}, false, errors.WithStack(err) } lock, err := decodeTxnLock(lockBytes) if err != nil { - return errors.WithStack(err) + return txnLock{}, false, errors.WithStack(err) } + return lock, true, nil +} + +func (s *ShardStore) resolveTxnLockForKey(ctx context.Context, g *ShardGroup, key []byte, lock txnLock) error { // Check primary transaction status to decide commit/rollback. status, commitTS, err := s.primaryTxnStatus(ctx, lock.PrimaryKey, lock.StartTS) if err != nil { @@ -357,7 +375,7 @@ func (s *ShardStore) resolveScanKVP(ctx context.Context, g *ShardGroup, kvp *sto // Fast-path: if there's no lock visible at this read timestamp, use the scan // value directly instead of re-reading it. - locked, err := scanKeyLockedAt(ctx, g, kvp.Key, ts) + lock, locked, err := loadTxnLockAt(ctx, g, kvp.Key, ts) if err != nil { return nil, false, err } @@ -365,28 +383,18 @@ func (s *ShardStore) resolveScanKVP(ctx context.Context, g *ShardGroup, kvp *sto return kvp, false, nil } - v, err := s.leaderGetAt(ctx, g, kvp.Key, ts) - if err != nil { - if errors.Is(err, store.ErrKeyNotFound) { - return nil, true, nil - } + if err := s.resolveTxnLockForKey(ctx, g, kvp.Key, lock); err != nil { return nil, false, err } - return &store.KVPair{Key: kvp.Key, Value: v}, false, nil -} -func scanKeyLockedAt(ctx context.Context, g *ShardGroup, key []byte, ts uint64) (bool, error) { - if g == nil || g.Store == nil { - return false, nil - } - _, err := g.Store.GetAt(ctx, txnLockKey(key), ts) + v, err := s.localGetAt(ctx, g, kvp.Key, ts) if err != nil { if errors.Is(err, store.ErrKeyNotFound) { - return false, nil + return nil, true, nil } - return false, errors.WithStack(err) + return nil, false, err } - return true, nil + return &store.KVPair{Key: kvp.Key, Value: v}, false, nil } func filterTxnInternalKVs(kvs []*store.KVPair) []*store.KVPair { diff --git a/kv/sharded_coordinator.go b/kv/sharded_coordinator.go index 6803e0fa..f5c377ce 100644 --- a/kv/sharded_coordinator.go +++ b/kv/sharded_coordinator.go @@ -483,7 +483,7 @@ func buildTxnLogs(startTS uint64, commitTS uint64, grouped map[uint64][]*pb.Muta logs := make([]*pb.Request, 0, len(gids)*txnPhaseCount) for _, gid := range gids { muts := grouped[gid] - primaryKey := primaryKeyFromMutations(muts) + primaryKey, keys := primaryKeyAndKeyMutations(muts) if len(primaryKey) == 0 { return nil, errors.WithStack(ErrTxnPrimaryKeyRequired) } @@ -502,16 +502,17 @@ func buildTxnLogs(startTS uint64, commitTS uint64, grouped map[uint64][]*pb.Muta Ts: startTS, Mutations: append([]*pb.Mutation{ {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: 0, CommitTS: commitTS})}, - }, keyMutations(muts)...), + }, keys...), }, ) } return logs, nil } -func primaryKeyFromMutations(muts []*pb.Mutation) []byte { +func primaryKeyAndKeyMutations(muts []*pb.Mutation) ([]byte, []*pb.Mutation) { seen := map[string]struct{}{} var primary []byte + keys := make([]*pb.Mutation, 0, len(muts)) for _, m := range muts { if m == nil || len(m.Key) == 0 { continue @@ -524,8 +525,9 @@ func primaryKeyFromMutations(muts []*pb.Mutation) []byte { if primary == nil || bytes.Compare(m.Key, primary) < 0 { primary = m.Key } + keys = append(keys, &pb.Mutation{Op: pb.Op_PUT, Key: m.Key}) } - return primary + return primary, keys } func keyMutations(muts []*pb.Mutation) []*pb.Mutation { From ff9b01698a39cde1f48f0c182e38da4c1736ef54 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Tue, 17 Feb 2026 04:40:45 +0900 Subject: [PATCH 08/20] kv: batch scan lock resolution by transaction --- kv/shard_store.go | 179 +++++++++++++++++++++++++++----- kv/shard_store_txn_lock_test.go | 127 ++++++++++++++++++++++ 2 files changed, 280 insertions(+), 26 deletions(-) diff --git a/kv/shard_store.go b/kv/shard_store.go index 34d3f09f..ab565aa5 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -343,6 +343,47 @@ func (s *ShardStore) resolveTxnLockForKey(ctx context.Context, g *ShardGroup, ke } } +type scanItem struct { + kvp *store.KVPair + skip bool + locked bool +} + +type lockTxnKey struct { + startTS uint64 + primary string +} + +type lockTxnStatus struct { + status txnStatus + commitTS uint64 +} + +type lockResolutionBatch struct { + phase pb.Phase + startTS uint64 + resolveTS uint64 + primaryKey []byte + keys [][]byte + seen map[string]struct{} +} + +type scanLockPlan struct { + items []scanItem + statusCache map[lockTxnKey]lockTxnStatus + resolutionBatches map[lockTxnKey]*lockResolutionBatch + batchOrder []lockTxnKey +} + +func newScanLockPlan(size int) *scanLockPlan { + return &scanLockPlan{ + items: make([]scanItem, 0, size), + statusCache: make(map[lockTxnKey]lockTxnStatus), + resolutionBatches: make(map[lockTxnKey]*lockResolutionBatch), + batchOrder: make([]lockTxnKey, 0), + } +} + func (s *ShardStore) resolveScanLocks(ctx context.Context, g *ShardGroup, kvs []*store.KVPair, ts uint64) ([]*store.KVPair, error) { if len(kvs) == 0 { return kvs, nil @@ -350,51 +391,137 @@ func (s *ShardStore) resolveScanLocks(ctx context.Context, g *ShardGroup, kvs [] if g == nil || g.Store == nil { return []*store.KVPair{}, nil } - out := make([]*store.KVPair, 0, len(kvs)) + + plan, err := s.planScanLockResolutions(ctx, g, kvs, ts) + if err != nil { + return nil, err + } + if err := applyScanLockResolutions(g, plan); err != nil { + return nil, err + } + return s.materializeScanLockResults(ctx, g, ts, plan.items) +} + +func (s *ShardStore) planScanLockResolutions(ctx context.Context, g *ShardGroup, kvs []*store.KVPair, ts uint64) (*scanLockPlan, error) { + plan := newScanLockPlan(len(kvs)) for _, kvp := range kvs { - resolved, skip, err := s.resolveScanKVP(ctx, g, kvp, ts) - if err != nil { + if err := s.planScanLockItem(ctx, g, ts, plan, kvp); err != nil { return nil, err } - if skip { - continue - } - out = append(out, resolved) } - return out, nil + return plan, nil } -func (s *ShardStore) resolveScanKVP(ctx context.Context, g *ShardGroup, kvp *store.KVPair, ts uint64) (*store.KVPair, bool, error) { - if kvp == nil { - return nil, true, nil - } - // Filter txn-internal keys from user-facing scans. - if isTxnInternalKey(kvp.Key) { - return nil, true, nil +func (s *ShardStore) planScanLockItem(ctx context.Context, g *ShardGroup, ts uint64, plan *scanLockPlan, kvp *store.KVPair) error { + if kvp == nil || isTxnInternalKey(kvp.Key) { + plan.items = append(plan.items, scanItem{skip: true}) + return nil } - // Fast-path: if there's no lock visible at this read timestamp, use the scan - // value directly instead of re-reading it. lock, locked, err := loadTxnLockAt(ctx, g, kvp.Key, ts) if err != nil { - return nil, false, err + return err } if !locked { - return kvp, false, nil + plan.items = append(plan.items, scanItem{kvp: kvp}) + return nil + } + if len(lock.PrimaryKey) == 0 { + return errors.Wrapf(ErrTxnInvalidMeta, "missing txn primary key for key %s", string(kvp.Key)) } - if err := s.resolveTxnLockForKey(ctx, g, kvp.Key, lock); err != nil { - return nil, false, err + txnKey := lockTxnKey{startTS: lock.StartTS, primary: string(lock.PrimaryKey)} + state, err := s.cachedLockTxnStatus(ctx, plan, lock, txnKey) + if err != nil { + return err + } + phase, resolveTS, err := lockResolutionForStatus(state, lock, kvp.Key) + if err != nil { + return err } + appendScanLockResolutionBatch(plan, txnKey, phase, resolveTS, lock, kvp.Key) + plan.items = append(plan.items, scanItem{kvp: kvp, locked: true}) + return nil +} - v, err := s.localGetAt(ctx, g, kvp.Key, ts) +func (s *ShardStore) cachedLockTxnStatus(ctx context.Context, plan *scanLockPlan, lock txnLock, txnKey lockTxnKey) (lockTxnStatus, error) { + if state, ok := plan.statusCache[txnKey]; ok { + return state, nil + } + status, commitTS, err := s.primaryTxnStatus(ctx, lock.PrimaryKey, lock.StartTS) if err != nil { - if errors.Is(err, store.ErrKeyNotFound) { - return nil, true, nil + return lockTxnStatus{}, err + } + state := lockTxnStatus{status: status, commitTS: commitTS} + plan.statusCache[txnKey] = state + return state, nil +} + +func lockResolutionForStatus(state lockTxnStatus, lock txnLock, key []byte) (pb.Phase, uint64, error) { + switch state.status { + case txnStatusPending: + return pb.Phase_NONE, 0, errors.Wrapf(ErrTxnLocked, "key: %s", string(key)) + case txnStatusCommitted: + return pb.Phase_COMMIT, state.commitTS, nil + case txnStatusRolledBack: + return pb.Phase_ABORT, cleanupTS(lock.StartTS), nil + default: + return pb.Phase_NONE, 0, errors.Wrapf(ErrTxnInvalidMeta, "unknown txn status for key %s", string(key)) + } +} + +func appendScanLockResolutionBatch(plan *scanLockPlan, txnKey lockTxnKey, phase pb.Phase, resolveTS uint64, lock txnLock, key []byte) { + batch, exists := plan.resolutionBatches[txnKey] + if !exists { + batch = &lockResolutionBatch{ + phase: phase, + startTS: lock.StartTS, + resolveTS: resolveTS, + primaryKey: lock.PrimaryKey, + keys: make([][]byte, 0, 1), + seen: map[string]struct{}{}, } - return nil, false, err + plan.resolutionBatches[txnKey] = batch + plan.batchOrder = append(plan.batchOrder, txnKey) + } + keyID := string(key) + if _, duplicated := batch.seen[keyID]; duplicated { + return } - return &store.KVPair{Key: kvp.Key, Value: v}, false, nil + batch.seen[keyID] = struct{}{} + batch.keys = append(batch.keys, key) +} + +func applyScanLockResolutions(g *ShardGroup, plan *scanLockPlan) error { + for _, txnKey := range plan.batchOrder { + batch := plan.resolutionBatches[txnKey] + if err := applyTxnResolution(g, batch.phase, batch.startTS, batch.resolveTS, batch.primaryKey, batch.keys); err != nil { + return err + } + } + return nil +} + +func (s *ShardStore) materializeScanLockResults(ctx context.Context, g *ShardGroup, ts uint64, items []scanItem) ([]*store.KVPair, error) { + out := make([]*store.KVPair, 0, len(items)) + for _, item := range items { + if item.skip { + continue + } + if !item.locked { + out = append(out, item.kvp) + continue + } + v, err := s.localGetAt(ctx, g, item.kvp.Key, ts) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + continue + } + return nil, err + } + out = append(out, &store.KVPair{Key: item.kvp.Key, Value: v}) + } + return out, nil } func filterTxnInternalKVs(kvs []*store.KVPair) []*store.KVPair { diff --git a/kv/shard_store_txn_lock_test.go b/kv/shard_store_txn_lock_test.go index f5310df3..b98d66fa 100644 --- a/kv/shard_store_txn_lock_test.go +++ b/kv/shard_store_txn_lock_test.go @@ -127,6 +127,133 @@ func TestShardStoreGetAt_ResolvesCommittedSecondaryLock(t *testing.T) { require.Equal(t, "v2", string(v)) } +func TestShardStoreScanAt_ReturnsTxnLockedForPendingLock(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSM(st1)) + defer stop1() + + groups := map[uint64]*ShardGroup{ + 1: {Raft: r1, Store: st1, Txn: NewLeaderProxy(r1)}, + } + shardStore := NewShardStore(engine, groups) + + key := []byte("k") + require.NoError(t, st1.PutAt(ctx, key, []byte("old"), 1, 0)) + + startTS := uint64(2) + prepare := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: key, LockTTLms: defaultTxnLockTTLms, CommitTS: 0})}, + {Op: pb.Op_PUT, Key: key, Value: []byte("v")}, + }, + } + _, err := groups[1].Txn.Commit([]*pb.Request{prepare}) + require.NoError(t, err) + + _, err = shardStore.ScanAt(ctx, []byte(""), nil, 100, ^uint64(0)) + require.Error(t, err) + require.True(t, errors.Is(err, ErrTxnLocked), "expected ErrTxnLocked, got %v", err) +} + +func TestShardStoreScanAt_ResolvesCommittedSecondaryLocks(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte("a"), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 2) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSM(st1)) + defer stop1() + + st2 := store.NewMVCCStore() + r2, stop2 := newSingleRaft(t, "g2", NewKvFSM(st2)) + defer stop2() + + groups := map[uint64]*ShardGroup{ + 1: {Raft: r1, Store: st1, Txn: NewLeaderProxy(r1)}, + 2: {Raft: r2, Store: st2, Txn: NewLeaderProxy(r2)}, + } + shardStore := NewShardStore(engine, groups) + + startTS := uint64(2) + commitTS := uint64(3) + + primaryKey := []byte("b") + secondaryKey1 := []byte("x") + secondaryKey2 := []byte("y") + require.NoError(t, st2.PutAt(ctx, secondaryKey1, []byte("old2"), 1, 0)) + require.NoError(t, st2.PutAt(ctx, secondaryKey2, []byte("old3"), 1, 0)) + + prepareMeta := func() *pb.Mutation { + return &pb.Mutation{ + Op: pb.Op_PUT, + Key: []byte(txnMetaPrefix), + Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: defaultTxnLockTTLms, CommitTS: 0}), + } + } + + preparePrimary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + prepareMeta(), + {Op: pb.Op_PUT, Key: primaryKey, Value: []byte("v1")}, + }, + } + prepareSecondary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + prepareMeta(), + {Op: pb.Op_PUT, Key: secondaryKey1, Value: []byte("v2")}, + {Op: pb.Op_PUT, Key: secondaryKey2, Value: []byte("v3")}, + }, + } + + _, err := groups[1].Txn.Commit([]*pb.Request{preparePrimary}) + require.NoError(t, err) + _, err = groups[2].Txn.Commit([]*pb.Request{prepareSecondary}) + require.NoError(t, err) + + commitPrimary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: 0, CommitTS: commitTS})}, + {Op: pb.Op_PUT, Key: primaryKey}, + }, + } + _, err = groups[1].Txn.Commit([]*pb.Request{commitPrimary}) + require.NoError(t, err) + + kvs, err := shardStore.ScanAt(ctx, []byte("w"), nil, 100, commitTS) + require.NoError(t, err) + require.Len(t, kvs, 2) + + got := map[string]string{} + for _, kvp := range kvs { + got[string(kvp.Key)] = string(kvp.Value) + } + require.Equal(t, "v2", got[string(secondaryKey1)]) + require.Equal(t, "v3", got[string(secondaryKey2)]) +} + func TestShardStoreScanAt_FiltersTxnInternalKeysWithoutRaft(t *testing.T) { t.Parallel() From e25b9443c33b7608e2406bd5278e74b9032afb6f Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Tue, 17 Feb 2026 05:00:40 +0900 Subject: [PATCH 09/20] kv/adapter: harden txn ttl and timestamp overflow --- adapter/internal.go | 32 ++++++++++------ adapter/internal_test.go | 80 +++++++++++++++++++++++++++++++++++++++ kv/fsm.go | 7 +--- kv/fsm_txn_test.go | 33 ++++++++++++++++ kv/hlc_wall.go | 10 +++++ kv/sharded_coordinator.go | 22 +++++++---- kv/transaction.go | 4 +- kv/txn_consts.go | 3 ++ 8 files changed, 165 insertions(+), 26 deletions(-) create mode 100644 adapter/internal_test.go diff --git a/adapter/internal.go b/adapter/internal.go index a31f06ff..70d127fa 100644 --- a/adapter/internal.go +++ b/adapter/internal.go @@ -30,13 +30,19 @@ var _ pb.InternalServer = (*Internal)(nil) var ErrNotLeader = errors.New("not leader") var ErrLeaderNotFound = errors.New("leader not found") +var ErrTxnTimestampOverflow = errors.New("txn timestamp overflow") func (i *Internal) Forward(_ context.Context, req *pb.ForwardRequest) (*pb.ForwardResponse, error) { if i.raft.State() != raft.Leader { return nil, errors.WithStack(ErrNotLeader) } - i.stampTimestamps(req) + if err := i.stampTimestamps(req); err != nil { + return &pb.ForwardResponse{ + Success: false, + CommitIndex: 0, + }, errors.WithStack(err) + } r, err := i.transactionManager.Commit(req.Requests) if err != nil { @@ -52,16 +58,16 @@ func (i *Internal) Forward(_ context.Context, req *pb.ForwardRequest) (*pb.Forwa }, nil } -func (i *Internal) stampTimestamps(req *pb.ForwardRequest) { +func (i *Internal) stampTimestamps(req *pb.ForwardRequest) error { if req == nil { - return + return nil } if req.IsTxn { - i.stampTxnTimestamps(req.Requests) - return + return i.stampTxnTimestamps(req.Requests) } i.stampRawTimestamps(req.Requests) + return nil } func (i *Internal) stampRawTimestamps(reqs []*pb.Request) { @@ -80,7 +86,7 @@ func (i *Internal) stampRawTimestamps(reqs []*pb.Request) { } } -func (i *Internal) stampTxnTimestamps(reqs []*pb.Request) { +func (i *Internal) stampTxnTimestamps(reqs []*pb.Request) error { startTS := forwardedTxnStartTS(reqs) if startTS == 0 { if i.clock == nil { @@ -89,6 +95,9 @@ func (i *Internal) stampTxnTimestamps(reqs []*pb.Request) { startTS = i.clock.Next() } } + if startTS == ^uint64(0) { + return errors.WithStack(ErrTxnTimestampOverflow) + } // Assign the unified timestamp to all requests in the transaction. for _, r := range reqs { @@ -97,7 +106,7 @@ func (i *Internal) stampTxnTimestamps(reqs []*pb.Request) { } } - i.fillForwardedTxnCommitTS(reqs, startTS) + return i.fillForwardedTxnCommitTS(reqs, startTS) } func forwardedTxnStartTS(reqs []*pb.Request) uint64 { @@ -125,7 +134,7 @@ func forwardedTxnMetaMutation(r *pb.Request, metaPrefix []byte) (*pb.Mutation, b return r.Mutations[0], true } -func (i *Internal) fillForwardedTxnCommitTS(reqs []*pb.Request, startTS uint64) { +func (i *Internal) fillForwardedTxnCommitTS(reqs []*pb.Request, startTS uint64) error { type metaToUpdate struct { m *pb.Mutation meta kv.TxnMeta @@ -148,13 +157,13 @@ func (i *Internal) fillForwardedTxnCommitTS(reqs []*pb.Request, startTS uint64) metaMutations = append(metaMutations, metaToUpdate{m: m, meta: meta}) } if len(metaMutations) == 0 { - return + return nil } commitTS := startTS + 1 if commitTS == 0 { // Overflow: can't choose a commit timestamp strictly greater than startTS. - return + return errors.WithStack(ErrTxnTimestampOverflow) } if i.clock != nil { i.clock.Observe(startTS) @@ -162,11 +171,12 @@ func (i *Internal) fillForwardedTxnCommitTS(reqs []*pb.Request, startTS uint64) } if commitTS <= startTS { // Defensive: avoid writing an invalid CommitTS. - return + return errors.WithStack(ErrTxnTimestampOverflow) } for _, item := range metaMutations { item.meta.CommitTS = commitTS item.m.Value = kv.EncodeTxnMeta(item.meta) } + return nil } diff --git a/adapter/internal_test.go b/adapter/internal_test.go new file mode 100644 index 00000000..8fc75be2 --- /dev/null +++ b/adapter/internal_test.go @@ -0,0 +1,80 @@ +package adapter + +import ( + "testing" + + "github.com/bootjp/elastickv/kv" + pb "github.com/bootjp/elastickv/proto" + "github.com/stretchr/testify/require" +) + +func TestStampTxnTimestamps_RejectsMaxStartTS(t *testing.T) { + t.Parallel() + + i := &Internal{} + reqs := []*pb.Request{ + { + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: ^uint64(0), + Mutations: []*pb.Mutation{ + { + Op: pb.Op_PUT, + Key: []byte(kv.TxnMetaPrefix), + Value: kv.EncodeTxnMeta(kv.TxnMeta{PrimaryKey: []byte("k"), CommitTS: 0}), + }, + }, + }, + } + + err := i.stampTxnTimestamps(reqs) + require.ErrorIs(t, err, ErrTxnTimestampOverflow) +} + +func TestFillForwardedTxnCommitTS_RejectsOverflow(t *testing.T) { + t.Parallel() + + i := &Internal{} + reqs := []*pb.Request{ + { + IsTxn: true, + Phase: pb.Phase_COMMIT, + Mutations: []*pb.Mutation{ + { + Op: pb.Op_PUT, + Key: []byte(kv.TxnMetaPrefix), + Value: kv.EncodeTxnMeta(kv.TxnMeta{PrimaryKey: []byte("k"), CommitTS: 0}), + }, + }, + }, + } + + err := i.fillForwardedTxnCommitTS(reqs, ^uint64(0)) + require.ErrorIs(t, err, ErrTxnTimestampOverflow) +} + +func TestFillForwardedTxnCommitTS_AssignsCommitTS(t *testing.T) { + t.Parallel() + + i := &Internal{} + startTS := uint64(10) + reqs := []*pb.Request{ + { + IsTxn: true, + Phase: pb.Phase_COMMIT, + Mutations: []*pb.Mutation{ + { + Op: pb.Op_PUT, + Key: []byte(kv.TxnMetaPrefix), + Value: kv.EncodeTxnMeta(kv.TxnMeta{PrimaryKey: []byte("k"), CommitTS: 0}), + }, + }, + }, + } + + require.NoError(t, i.fillForwardedTxnCommitTS(reqs, startTS)) + + meta, err := kv.DecodeTxnMeta(reqs[0].Mutations[0].Value) + require.NoError(t, err) + require.Equal(t, startTS+1, meta.CommitTS) +} diff --git a/kv/fsm.go b/kv/fsm.go index adae4f4a..efeba734 100644 --- a/kv/fsm.go +++ b/kv/fsm.go @@ -201,12 +201,7 @@ func (f *kvFSM) handlePrepareRequest(ctx context.Context, r *pb.Request) error { return errors.WithStack(err) } - ttlMs := meta.LockTTLms - if ttlMs == 0 { - // Default when callers don't specify TTL (for example, Redis MULTI/EXEC). - ttlMs = defaultTxnLockTTLms - } - expireAt := hlcWallFromNowMs(ttlMs) + expireAt := txnLockExpireAt(meta.LockTTLms) storeMuts, err := f.buildPrepareStoreMutations(ctx, uniq, meta.PrimaryKey, startTS, expireAt) if err != nil { diff --git a/kv/fsm_txn_test.go b/kv/fsm_txn_test.go index bbfeea64..fe1cd1eb 100644 --- a/kv/fsm_txn_test.go +++ b/kv/fsm_txn_test.go @@ -110,3 +110,36 @@ func TestCommitRejectsMismatchedPrimaryKey(t *testing.T) { _, err = st.GetAt(ctx, txnLockKey(key), ^uint64(0)) require.NoError(t, err) } + +func TestPrepareClampsHugeLockTTL(t *testing.T) { + t.Parallel() + + ctx := context.Background() + st := store.NewMVCCStore() + fsm, ok := NewKvFSM(st).(*kvFSM) + require.True(t, ok) + + startTS := uint64(12) + key := []byte("k") + prepare := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: key, LockTTLms: ^uint64(0)})}, + {Op: pb.Op_PUT, Key: key, Value: []byte("v")}, + }, + } + require.NoError(t, applyFSMRequest(t, fsm, prepare)) + + lockBytes, err := st.GetAt(ctx, txnLockKey(key), ^uint64(0)) + require.NoError(t, err) + lock, err := decodeTxnLock(lockBytes) + require.NoError(t, err) + require.NotEqual(t, ^uint64(0), lock.TTLExpireAt) + + now := hlcWallNow() + require.Greater(t, lock.TTLExpireAt, now) + maxDelta := maxTxnLockTTLms << hlcLogicalBits + require.LessOrEqual(t, lock.TTLExpireAt-now, maxDelta) +} diff --git a/kv/hlc_wall.go b/kv/hlc_wall.go index a2dd05cb..51dbddb8 100644 --- a/kv/hlc_wall.go +++ b/kv/hlc_wall.go @@ -26,3 +26,13 @@ func hlcWallFromNowMs(deltaMs uint64) uint64 { } return now + delta } + +func txnLockExpireAt(ttlMs uint64) uint64 { + if ttlMs == 0 { + ttlMs = defaultTxnLockTTLms + } + if ttlMs > maxTxnLockTTLms { + ttlMs = maxTxnLockTTLms + } + return hlcWallFromNowMs(ttlMs) +} diff --git a/kv/sharded_coordinator.go b/kv/sharded_coordinator.go index f5c377ce..2fb51e71 100644 --- a/kv/sharded_coordinator.go +++ b/kv/sharded_coordinator.go @@ -261,15 +261,23 @@ func (c *ShardedCoordinator) nextTxnTSAfter(startTS uint64) uint64 { } func abortTSFrom(startTS, commitTS uint64) uint64 { - abortTS := commitTS + 1 - if abortTS > startTS { - return abortTS + const maxUint64 = ^uint64(0) + + // Prefer commitTS+1 when representable and strictly greater than startTS. + if commitTS < maxUint64 { + abortTS := commitTS + 1 + if abortTS > startTS { + return abortTS + } } - fallback := startTS + 1 - if fallback == 0 { - return 0 + + // Fallback to startTS+1 when representable. + if startTS < maxUint64 { + return startTS + 1 } - return fallback + + // No representable timestamp exists that is strictly greater than startTS. + return 0 } func txnMetaMutation(primaryKey []byte, lockTTLms uint64, commitTS uint64) *pb.Mutation { diff --git a/kv/transaction.go b/kv/transaction.go index 3ab23ffb..e95f962b 100644 --- a/kv/transaction.go +++ b/kv/transaction.go @@ -100,8 +100,8 @@ func (t *TransactionManager) Abort(reqs []*pb.Request) (*TransactionResponse, er continue } startTS := req.Ts - abortTS := startTS + 1 - if abortTS == 0 { + abortTS := abortTSFrom(startTS, startTS) + if abortTS <= startTS { // Overflow: can't choose an abort timestamp strictly greater than startTS. continue } diff --git a/kv/txn_consts.go b/kv/txn_consts.go index c16866e5..17f6419e 100644 --- a/kv/txn_consts.go +++ b/kv/txn_consts.go @@ -2,6 +2,9 @@ package kv const ( defaultTxnLockTTLms uint64 = 30_000 + // Keep lock TTL bounded to avoid effectively-permanent locks from malformed + // or extreme client-provided TTL values. + maxTxnLockTTLms uint64 = 86_400_000 // 24h txnPrepareStoreMutationFactor = 2 From e480042a3c7dc8198ac877ee8bd6c5ea976c743d Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Tue, 17 Feb 2026 05:26:01 +0900 Subject: [PATCH 10/20] kv/adapter: handle latest PR review issues --- adapter/grpc.go | 4 +- adapter/grpc_test.go | 22 +- adapter/redis.go | 2 +- kv/coordinator.go | 3 + kv/coordinator_txn_test.go | 40 ++ kv/leader_routed_store.go | 2 +- kv/shard_store.go | 136 +++- kv/shard_store_txn_lock_test.go | 116 ++++ proto/service.pb.go | 1107 ++++++++++++++++++++++--------- proto/service.proto | 1 + proto/service_grpc.pb.go | 70 +- 11 files changed, 1116 insertions(+), 387 deletions(-) create mode 100644 kv/coordinator_txn_test.go diff --git a/adapter/grpc.go b/adapter/grpc.go index aa74f8fa..a046f9e8 100644 --- a/adapter/grpc.go +++ b/adapter/grpc.go @@ -87,7 +87,7 @@ func (r *GRPCServer) RawGet(ctx context.Context, req *pb.RawGetRequest) (*pb.Raw v, err := r.store.GetAt(ctx, req.Key, readTS) if errors.Is(err, store.ErrKeyNotFound) { - return &pb.RawGetResponse{Value: nil}, nil + return &pb.RawGetResponse{Value: nil, Exists: false}, nil } if err != nil { return nil, errors.WithStack(err) @@ -97,7 +97,7 @@ func (r *GRPCServer) RawGet(ctx context.Context, req *pb.RawGetRequest) (*pb.Raw slog.String("key", string(req.Key)), slog.String("value", string(v))) - return &pb.RawGetResponse{Value: v}, nil + return &pb.RawGetResponse{Value: v, Exists: true}, nil } func (r *GRPCServer) RawLatestCommitTS(ctx context.Context, req *pb.RawLatestCommitTSRequest) (*pb.RawLatestCommitTSResponse, error) { diff --git a/adapter/grpc_test.go b/adapter/grpc_test.go index 1d16f68b..a6b69d62 100644 --- a/adapter/grpc_test.go +++ b/adapter/grpc_test.go @@ -36,13 +36,33 @@ func Test_value_can_be_deleted(t *testing.T) { resp, err := c.RawGet(context.TODO(), &pb.RawGetRequest{Key: key}) assert.NoError(t, err, "Get RPC failed") assert.Nil(t, err) + assert.True(t, resp.Exists) assert.Equal(t, want, resp.Value) _, err = c.RawDelete(context.TODO(), &pb.RawDeleteRequest{Key: key}) assert.NoError(t, err, "Delete RPC failed") - _, err = c.RawGet(context.TODO(), &pb.RawGetRequest{Key: key}) + resp, err = c.RawGet(context.TODO(), &pb.RawGetRequest{Key: key}) assert.NoError(t, err, "Get RPC failed") + assert.False(t, resp.Exists) +} + +func Test_grpc_raw_get_empty_value(t *testing.T) { + t.Parallel() + nodes, adders, _ := createNode(t, 3) + c := rawKVClient(t, adders) + defer shutdown(nodes) + + key := []byte("empty-key") + empty := []byte{} + + _, err := c.RawPut(context.Background(), &pb.RawPutRequest{Key: key, Value: empty}) + assert.NoError(t, err, "Put RPC failed") + + resp, err := c.RawGet(context.TODO(), &pb.RawGetRequest{Key: key}) + assert.NoError(t, err, "Get RPC failed") + assert.True(t, resp.Exists) + assert.Equal(t, 0, len(resp.Value)) } func Test_grpc_scan(t *testing.T) { diff --git a/adapter/redis.go b/adapter/redis.go index cee9b0aa..fd63984e 100644 --- a/adapter/redis.go +++ b/adapter/redis.go @@ -1175,7 +1175,7 @@ func (r *RedisServer) tryLeaderGetAt(key []byte, ts uint64) ([]byte, error) { if err != nil { return nil, errors.WithStack(err) } - if resp.Value == nil { + if !resp.GetExists() { return nil, errors.WithStack(store.ErrKeyNotFound) } return resp.Value, nil diff --git a/kv/coordinator.go b/kv/coordinator.go index 28a0accd..628e1397 100644 --- a/kv/coordinator.go +++ b/kv/coordinator.go @@ -112,6 +112,9 @@ func (c *Coordinate) dispatchTxn(reqs []*Elem[OP], startTS uint64) (*CoordinateR c.clock.Observe(startTS) commitTS = c.clock.Next() } + if commitTS <= startTS { + return nil, errors.WithStack(ErrTxnCommitTSRequired) + } logs := txnRequests(startTS, commitTS, defaultTxnLockTTLms, primary, reqs) diff --git a/kv/coordinator_txn_test.go b/kv/coordinator_txn_test.go new file mode 100644 index 00000000..21e9d9fb --- /dev/null +++ b/kv/coordinator_txn_test.go @@ -0,0 +1,40 @@ +package kv + +import ( + "testing" + + pb "github.com/bootjp/elastickv/proto" + "github.com/stretchr/testify/require" +) + +type stubTransactional struct { + commits int +} + +func (s *stubTransactional) Commit(_ []*pb.Request) (*TransactionResponse, error) { + s.commits++ + return &TransactionResponse{}, nil +} + +func (s *stubTransactional) Abort(_ []*pb.Request) (*TransactionResponse, error) { + return &TransactionResponse{}, nil +} + +func TestCoordinateDispatchTxn_RejectsNonMonotonicCommitTS(t *testing.T) { + t.Parallel() + + tx := &stubTransactional{} + c := &Coordinate{ + transactionManager: tx, + clock: NewHLC(), + } + + startTS := ^uint64(0) + c.clock.Observe(startTS) + + _, err := c.dispatchTxn([]*Elem[OP]{ + {Op: Put, Key: []byte("k"), Value: []byte("v")}, + }, startTS) + require.ErrorIs(t, err, ErrTxnCommitTSRequired) + require.Equal(t, 0, tx.commits) +} diff --git a/kv/leader_routed_store.go b/kv/leader_routed_store.go index 43fe1a45..da0a5f70 100644 --- a/kv/leader_routed_store.go +++ b/kv/leader_routed_store.go @@ -66,7 +66,7 @@ func (s *LeaderRoutedStore) proxyRawGet(ctx context.Context, key []byte, ts uint if err != nil { return nil, errors.WithStack(err) } - if resp.Value == nil { + if !resp.GetExists() { return nil, store.ErrKeyNotFound } return resp.Value, nil diff --git a/kv/shard_store.go b/kv/shard_store.go index ab565aa5..a41b009f 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -165,14 +165,8 @@ func (s *ShardStore) scanRouteAt(ctx context.Context, route distribution.Route, // Reads should come from the shard's leader to avoid returning stale or // incomplete results when this node is a follower for a given shard. - if g.Raft.State() == raft.Leader { - if err := g.Raft.VerifyLeader().Error(); err == nil { - kvs, err := g.Store.ScanAt(ctx, start, end, limit, ts) - if err != nil { - return nil, errors.WithStack(err) - } - return s.resolveScanLocks(ctx, g, kvs, ts) - } + if isVerifiedRaftLeader(g.Raft) { + return s.scanRouteAtLeader(ctx, g, start, end, limit, ts) } kvs, err := s.proxyRawScanAt(ctx, g, start, end, limit, ts) @@ -184,6 +178,18 @@ func (s *ShardStore) scanRouteAt(ctx context.Context, route distribution.Route, return filterTxnInternalKVs(kvs), nil } +func (s *ShardStore) scanRouteAtLeader(ctx context.Context, g *ShardGroup, start []byte, end []byte, limit int, ts uint64) ([]*store.KVPair, error) { + kvs, err := g.Store.ScanAt(ctx, start, end, limit, ts) + if err != nil { + return nil, errors.WithStack(err) + } + lockKVs, err := scanTxnLockRangeAt(ctx, g, start, end, limit, ts) + if err != nil { + return nil, err + } + return s.resolveScanLocks(ctx, g, kvs, lockKVs, ts) +} + func (s *ShardStore) groupForID(groupID uint64) (*ShardGroup, bool) { g, ok := s.groups[groupID] return g, ok @@ -370,6 +376,7 @@ type lockResolutionBatch struct { type scanLockPlan struct { items []scanItem + itemIndex map[string]int statusCache map[lockTxnKey]lockTxnStatus resolutionBatches map[lockTxnKey]*lockResolutionBatch batchOrder []lockTxnKey @@ -378,21 +385,22 @@ type scanLockPlan struct { func newScanLockPlan(size int) *scanLockPlan { return &scanLockPlan{ items: make([]scanItem, 0, size), + itemIndex: make(map[string]int, size), statusCache: make(map[lockTxnKey]lockTxnStatus), resolutionBatches: make(map[lockTxnKey]*lockResolutionBatch), batchOrder: make([]lockTxnKey, 0), } } -func (s *ShardStore) resolveScanLocks(ctx context.Context, g *ShardGroup, kvs []*store.KVPair, ts uint64) ([]*store.KVPair, error) { - if len(kvs) == 0 { +func (s *ShardStore) resolveScanLocks(ctx context.Context, g *ShardGroup, kvs []*store.KVPair, lockKVs []*store.KVPair, ts uint64) ([]*store.KVPair, error) { + if len(kvs) == 0 && len(lockKVs) == 0 { return kvs, nil } if g == nil || g.Store == nil { return []*store.KVPair{}, nil } - plan, err := s.planScanLockResolutions(ctx, g, kvs, ts) + plan, err := s.planScanLockResolutions(ctx, g, kvs, lockKVs, ts) if err != nil { return nil, err } @@ -402,8 +410,13 @@ func (s *ShardStore) resolveScanLocks(ctx context.Context, g *ShardGroup, kvs [] return s.materializeScanLockResults(ctx, g, ts, plan.items) } -func (s *ShardStore) planScanLockResolutions(ctx context.Context, g *ShardGroup, kvs []*store.KVPair, ts uint64) (*scanLockPlan, error) { - plan := newScanLockPlan(len(kvs)) +func (s *ShardStore) planScanLockResolutions(ctx context.Context, g *ShardGroup, kvs []*store.KVPair, lockKVs []*store.KVPair, ts uint64) (*scanLockPlan, error) { + plan := newScanLockPlan(len(kvs) + len(lockKVs)) + for _, kvp := range lockKVs { + if err := s.planScanLockFromLockKVP(ctx, plan, kvp); err != nil { + return nil, err + } + } for _, kvp := range kvs { if err := s.planScanLockItem(ctx, g, ts, plan, kvp); err != nil { return nil, err @@ -412,22 +425,49 @@ func (s *ShardStore) planScanLockResolutions(ctx context.Context, g *ShardGroup, return plan, nil } +func (s *ShardStore) planScanLockFromLockKVP(ctx context.Context, plan *scanLockPlan, kvp *store.KVPair) error { + if kvp == nil { + return nil + } + userKey, ok := txnUserKeyFromLockKey(kvp.Key) + if !ok { + return nil + } + + lock, err := decodeTxnLock(kvp.Value) + if err != nil { + return errors.WithStack(err) + } + if len(lock.PrimaryKey) == 0 { + return errors.Wrapf(ErrTxnInvalidMeta, "missing txn primary key for key %s", string(userKey)) + } + + return s.planLockedUserKey(ctx, plan, userKey, lock) +} + func (s *ShardStore) planScanLockItem(ctx context.Context, g *ShardGroup, ts uint64, plan *scanLockPlan, kvp *store.KVPair) error { if kvp == nil || isTxnInternalKey(kvp.Key) { plan.items = append(plan.items, scanItem{skip: true}) return nil } + if _, exists := plan.itemIndex[string(kvp.Key)]; exists { + return nil + } lock, locked, err := loadTxnLockAt(ctx, g, kvp.Key, ts) if err != nil { return err } if !locked { - plan.items = append(plan.items, scanItem{kvp: kvp}) + appendScanItem(plan, kvp, false) return nil } + return s.planLockedUserKey(ctx, plan, kvp.Key, lock) +} + +func (s *ShardStore) planLockedUserKey(ctx context.Context, plan *scanLockPlan, userKey []byte, lock txnLock) error { if len(lock.PrimaryKey) == 0 { - return errors.Wrapf(ErrTxnInvalidMeta, "missing txn primary key for key %s", string(kvp.Key)) + return errors.Wrapf(ErrTxnInvalidMeta, "missing txn primary key for key %s", string(userKey)) } txnKey := lockTxnKey{startTS: lock.StartTS, primary: string(lock.PrimaryKey)} @@ -435,12 +475,12 @@ func (s *ShardStore) planScanLockItem(ctx context.Context, g *ShardGroup, ts uin if err != nil { return err } - phase, resolveTS, err := lockResolutionForStatus(state, lock, kvp.Key) + phase, resolveTS, err := lockResolutionForStatus(state, lock, userKey) if err != nil { return err } - appendScanLockResolutionBatch(plan, txnKey, phase, resolveTS, lock, kvp.Key) - plan.items = append(plan.items, scanItem{kvp: kvp, locked: true}) + appendScanLockResolutionBatch(plan, txnKey, phase, resolveTS, lock, userKey) + appendScanItem(plan, &store.KVPair{Key: userKey}, true) return nil } @@ -492,6 +532,64 @@ func appendScanLockResolutionBatch(plan *scanLockPlan, txnKey lockTxnKey, phase batch.keys = append(batch.keys, key) } +func appendScanItem(plan *scanLockPlan, kvp *store.KVPair, locked bool) { + if kvp == nil || len(kvp.Key) == 0 { + return + } + keyID := string(kvp.Key) + if idx, exists := plan.itemIndex[keyID]; exists { + if locked { + plan.items[idx].locked = true + } + return + } + + plan.itemIndex[keyID] = len(plan.items) + plan.items = append(plan.items, scanItem{kvp: kvp, locked: locked}) +} + +func txnUserKeyFromLockKey(lockKey []byte) ([]byte, bool) { + if !bytes.HasPrefix(lockKey, []byte(txnLockPrefix)) { + return nil, false + } + return bytes.Clone(lockKey[len(txnLockPrefix):]), true +} + +func scanTxnLockRangeAt(ctx context.Context, g *ShardGroup, start []byte, end []byte, limit int, ts uint64) ([]*store.KVPair, error) { + if g == nil || g.Store == nil || limit <= 0 { + return []*store.KVPair{}, nil + } + lockStart, lockEnd := txnLockScanBounds(start, end) + lockKVs, err := g.Store.ScanAt(ctx, lockStart, lockEnd, limit, ts) + if err != nil { + return nil, errors.WithStack(err) + } + return lockKVs, nil +} + +func txnLockScanBounds(start []byte, end []byte) ([]byte, []byte) { + lockStart := txnLockKey(start) + if end != nil { + return lockStart, txnLockKey(end) + } + return lockStart, prefixScanEnd([]byte(txnLockPrefix)) +} + +func prefixScanEnd(prefix []byte) []byte { + if len(prefix) == 0 { + return nil + } + out := bytes.Clone(prefix) + for i := len(out) - 1; i >= 0; i-- { + if out[i] == ^byte(0) { + continue + } + out[i]++ + return out[:i+1] + } + return nil +} + func applyScanLockResolutions(g *ShardGroup, plan *scanLockPlan) error { for _, txnKey := range plan.batchOrder { batch := plan.resolutionBatches[txnKey] @@ -781,7 +879,7 @@ func (s *ShardStore) proxyRawGet(ctx context.Context, g *ShardGroup, key []byte, if err != nil { return nil, errors.WithStack(err) } - if resp.Value == nil { + if !resp.GetExists() { return nil, store.ErrKeyNotFound } return resp.Value, nil diff --git a/kv/shard_store_txn_lock_test.go b/kv/shard_store_txn_lock_test.go index b98d66fa..a267826e 100644 --- a/kv/shard_store_txn_lock_test.go +++ b/kv/shard_store_txn_lock_test.go @@ -165,6 +165,44 @@ func TestShardStoreScanAt_ReturnsTxnLockedForPendingLock(t *testing.T) { require.True(t, errors.Is(err, ErrTxnLocked), "expected ErrTxnLocked, got %v", err) } +func TestShardStoreScanAt_ReturnsTxnLockedForPendingLockWithoutCommittedValue(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSM(st1)) + defer stop1() + + groups := map[uint64]*ShardGroup{ + 1: {Raft: r1, Store: st1, Txn: NewLeaderProxy(r1)}, + } + shardStore := NewShardStore(engine, groups) + + key := []byte("k") + startTS := uint64(1) + prepare := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: key, LockTTLms: defaultTxnLockTTLms, CommitTS: 0})}, + {Op: pb.Op_PUT, Key: key, Value: []byte("v")}, + }, + } + _, err := groups[1].Txn.Commit([]*pb.Request{prepare}) + require.NoError(t, err) + + // User-key range does not include raw !txn|lock|... keys, so lock-only + // pending writes must still be detected through lock-range scanning. + _, err = shardStore.ScanAt(ctx, []byte("k"), []byte("l"), 100, ^uint64(0)) + require.Error(t, err) + require.True(t, errors.Is(err, ErrTxnLocked), "expected ErrTxnLocked, got %v", err) +} + func TestShardStoreScanAt_ResolvesCommittedSecondaryLocks(t *testing.T) { t.Parallel() @@ -254,6 +292,84 @@ func TestShardStoreScanAt_ResolvesCommittedSecondaryLocks(t *testing.T) { require.Equal(t, "v3", got[string(secondaryKey2)]) } +func TestShardStoreScanAt_ResolvesCommittedSecondaryLockWithoutCommittedValue(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte("a"), []byte("m"), 1) + engine.UpdateRoute([]byte("m"), nil, 2) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSM(st1)) + defer stop1() + + st2 := store.NewMVCCStore() + r2, stop2 := newSingleRaft(t, "g2", NewKvFSM(st2)) + defer stop2() + + groups := map[uint64]*ShardGroup{ + 1: {Raft: r1, Store: st1, Txn: NewLeaderProxy(r1)}, + 2: {Raft: r2, Store: st2, Txn: NewLeaderProxy(r2)}, + } + shardStore := NewShardStore(engine, groups) + + startTS := uint64(1) + commitTS := uint64(2) + primaryKey := []byte("b") + secondaryKey := []byte("x") + + prepareMeta := func() *pb.Mutation { + return &pb.Mutation{ + Op: pb.Op_PUT, + Key: []byte(txnMetaPrefix), + Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: defaultTxnLockTTLms, CommitTS: 0}), + } + } + + preparePrimary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + prepareMeta(), + {Op: pb.Op_PUT, Key: primaryKey, Value: []byte("v1")}, + }, + } + prepareSecondary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: startTS, + Mutations: []*pb.Mutation{ + prepareMeta(), + {Op: pb.Op_PUT, Key: secondaryKey, Value: []byte("v2")}, + }, + } + _, err := groups[1].Txn.Commit([]*pb.Request{preparePrimary}) + require.NoError(t, err) + _, err = groups[2].Txn.Commit([]*pb.Request{prepareSecondary}) + require.NoError(t, err) + + commitPrimary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: startTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: primaryKey, LockTTLms: 0, CommitTS: commitTS})}, + {Op: pb.Op_PUT, Key: primaryKey}, + }, + } + _, err = groups[1].Txn.Commit([]*pb.Request{commitPrimary}) + require.NoError(t, err) + + kvs, err := shardStore.ScanAt(ctx, []byte("x"), []byte("z"), 100, commitTS) + require.NoError(t, err) + require.Len(t, kvs, 1) + require.Equal(t, secondaryKey, kvs[0].Key) + require.Equal(t, []byte("v2"), kvs[0].Value) +} + func TestShardStoreScanAt_FiltersTxnInternalKeysWithoutRaft(t *testing.T) { t.Parallel() diff --git a/proto/service.pb.go b/proto/service.pb.go index cc39a904..a7f19270 100644 --- a/proto/service.pb.go +++ b/proto/service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.11 -// protoc v3.21.12 +// protoc-gen-go v1.34.2 +// protoc v5.29.3 // source: service.proto package proto @@ -11,7 +11,6 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" - unsafe "unsafe" ) const ( @@ -22,18 +21,21 @@ const ( ) type RawPutRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` } func (x *RawPutRequest) Reset() { *x = RawPutRequest{} - mi := &file_service_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawPutRequest) String() string { @@ -44,7 +46,7 @@ func (*RawPutRequest) ProtoMessage() {} func (x *RawPutRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[0] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -74,18 +76,21 @@ func (x *RawPutRequest) GetValue() []byte { } type RawPutResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` } func (x *RawPutResponse) Reset() { *x = RawPutResponse{} - mi := &file_service_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawPutResponse) String() string { @@ -96,7 +101,7 @@ func (*RawPutResponse) ProtoMessage() {} func (x *RawPutResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[1] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -126,18 +131,21 @@ func (x *RawPutResponse) GetSuccess() bool { } type RawGetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Ts uint64 `protobuf:"varint,3,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Ts uint64 `protobuf:"varint,3,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC } func (x *RawGetRequest) Reset() { *x = RawGetRequest{} - mi := &file_service_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawGetRequest) String() string { @@ -148,7 +156,7 @@ func (*RawGetRequest) ProtoMessage() {} func (x *RawGetRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[2] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -178,18 +186,22 @@ func (x *RawGetRequest) GetTs() uint64 { } type RawGetResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"` } func (x *RawGetResponse) Reset() { *x = RawGetResponse{} - mi := &file_service_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawGetResponse) String() string { @@ -200,7 +212,7 @@ func (*RawGetResponse) ProtoMessage() {} func (x *RawGetResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[3] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -229,18 +241,28 @@ func (x *RawGetResponse) GetValue() []byte { return nil } +func (x *RawGetResponse) GetExists() bool { + if x != nil { + return x.Exists + } + return false +} + type RawDeleteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` } func (x *RawDeleteRequest) Reset() { *x = RawDeleteRequest{} - mi := &file_service_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawDeleteRequest) String() string { @@ -251,7 +273,7 @@ func (*RawDeleteRequest) ProtoMessage() {} func (x *RawDeleteRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[4] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -274,18 +296,21 @@ func (x *RawDeleteRequest) GetKey() []byte { } type RawDeleteResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` } func (x *RawDeleteResponse) Reset() { *x = RawDeleteResponse{} - mi := &file_service_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawDeleteResponse) String() string { @@ -296,7 +321,7 @@ func (*RawDeleteResponse) ProtoMessage() {} func (x *RawDeleteResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[5] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -326,17 +351,20 @@ func (x *RawDeleteResponse) GetSuccess() bool { } type RawLatestCommitTSRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` } func (x *RawLatestCommitTSRequest) Reset() { *x = RawLatestCommitTSRequest{} - mi := &file_service_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawLatestCommitTSRequest) String() string { @@ -347,7 +375,7 @@ func (*RawLatestCommitTSRequest) ProtoMessage() {} func (x *RawLatestCommitTSRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[6] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -370,18 +398,21 @@ func (x *RawLatestCommitTSRequest) GetKey() []byte { } type RawLatestCommitTSResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Ts uint64 `protobuf:"varint,1,opt,name=ts,proto3" json:"ts,omitempty"` - Exists bool `protobuf:"varint,2,opt,name=exists,proto3" json:"exists,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ts uint64 `protobuf:"varint,1,opt,name=ts,proto3" json:"ts,omitempty"` + Exists bool `protobuf:"varint,2,opt,name=exists,proto3" json:"exists,omitempty"` } func (x *RawLatestCommitTSResponse) Reset() { *x = RawLatestCommitTSResponse{} - mi := &file_service_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawLatestCommitTSResponse) String() string { @@ -392,7 +423,7 @@ func (*RawLatestCommitTSResponse) ProtoMessage() {} func (x *RawLatestCommitTSResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[7] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -422,20 +453,23 @@ func (x *RawLatestCommitTSResponse) GetExists() bool { } type RawScanAtRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` - EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` - Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // validated against host int size; large values may be rejected - Ts uint64 `protobuf:"varint,4,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` + EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` + Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // validated against host int size; large values may be rejected + Ts uint64 `protobuf:"varint,4,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC } func (x *RawScanAtRequest) Reset() { *x = RawScanAtRequest{} - mi := &file_service_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawScanAtRequest) String() string { @@ -446,7 +480,7 @@ func (*RawScanAtRequest) ProtoMessage() {} func (x *RawScanAtRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[8] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -490,18 +524,21 @@ func (x *RawScanAtRequest) GetTs() uint64 { } type RawKVPair struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` } func (x *RawKVPair) Reset() { *x = RawKVPair{} - mi := &file_service_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawKVPair) String() string { @@ -512,7 +549,7 @@ func (*RawKVPair) ProtoMessage() {} func (x *RawKVPair) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[9] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -542,17 +579,20 @@ func (x *RawKVPair) GetValue() []byte { } type RawScanAtResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kv []*RawKVPair `protobuf:"bytes,1,rep,name=kv,proto3" json:"kv,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Kv []*RawKVPair `protobuf:"bytes,1,rep,name=kv,proto3" json:"kv,omitempty"` } func (x *RawScanAtResponse) Reset() { *x = RawScanAtResponse{} - mi := &file_service_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RawScanAtResponse) String() string { @@ -563,7 +603,7 @@ func (*RawScanAtResponse) ProtoMessage() {} func (x *RawScanAtResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[10] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -586,18 +626,21 @@ func (x *RawScanAtResponse) GetKv() []*RawKVPair { } type PutRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` } func (x *PutRequest) Reset() { *x = PutRequest{} - mi := &file_service_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *PutRequest) String() string { @@ -608,7 +651,7 @@ func (*PutRequest) ProtoMessage() {} func (x *PutRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[11] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -638,18 +681,21 @@ func (x *PutRequest) GetValue() []byte { } type PutResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` } func (x *PutResponse) Reset() { *x = PutResponse{} - mi := &file_service_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *PutResponse) String() string { @@ -660,7 +706,7 @@ func (*PutResponse) ProtoMessage() {} func (x *PutResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[12] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -690,17 +736,20 @@ func (x *PutResponse) GetSuccess() bool { } type DeleteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` } func (x *DeleteRequest) Reset() { *x = DeleteRequest{} - mi := &file_service_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *DeleteRequest) String() string { @@ -711,7 +760,7 @@ func (*DeleteRequest) ProtoMessage() {} func (x *DeleteRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[13] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -734,18 +783,21 @@ func (x *DeleteRequest) GetKey() []byte { } type DeleteResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` } func (x *DeleteResponse) Reset() { *x = DeleteResponse{} - mi := &file_service_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *DeleteResponse) String() string { @@ -756,7 +808,7 @@ func (*DeleteResponse) ProtoMessage() {} func (x *DeleteResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[14] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -786,17 +838,20 @@ func (x *DeleteResponse) GetSuccess() bool { } type GetRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` } func (x *GetRequest) Reset() { *x = GetRequest{} - mi := &file_service_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *GetRequest) String() string { @@ -807,7 +862,7 @@ func (*GetRequest) ProtoMessage() {} func (x *GetRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[15] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -830,18 +885,21 @@ func (x *GetRequest) GetKey() []byte { } type GetResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` } func (x *GetResponse) Reset() { *x = GetResponse{} - mi := &file_service_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *GetResponse) String() string { @@ -852,7 +910,7 @@ func (*GetResponse) ProtoMessage() {} func (x *GetResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[16] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -882,18 +940,21 @@ func (x *GetResponse) GetValue() []byte { } type KeyError struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - Retryable bool `protobuf:"varint,2,opt,name=retryable,proto3" json:"retryable,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Retryable bool `protobuf:"varint,2,opt,name=retryable,proto3" json:"retryable,omitempty"` } func (x *KeyError) Reset() { *x = KeyError{} - mi := &file_service_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *KeyError) String() string { @@ -904,7 +965,7 @@ func (*KeyError) ProtoMessage() {} func (x *KeyError) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[17] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -934,19 +995,22 @@ func (x *KeyError) GetRetryable() bool { } type Kv struct { - state protoimpl.MessageState `protogen:"open.v1"` - Error *KeyError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` - Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Error *KeyError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` } func (x *Kv) Reset() { *x = Kv{} - mi := &file_service_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *Kv) String() string { @@ -957,7 +1021,7 @@ func (*Kv) ProtoMessage() {} func (x *Kv) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[18] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -994,19 +1058,22 @@ func (x *Kv) GetValue() []byte { } type ScanRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` - EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` - Limit uint64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` + EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` + Limit uint64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` } func (x *ScanRequest) Reset() { *x = ScanRequest{} - mi := &file_service_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *ScanRequest) String() string { @@ -1017,7 +1084,7 @@ func (*ScanRequest) ProtoMessage() {} func (x *ScanRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[19] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1054,17 +1121,20 @@ func (x *ScanRequest) GetLimit() uint64 { } type ScanResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Kv []*Kv `protobuf:"bytes,1,rep,name=kv,proto3" json:"kv,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Kv []*Kv `protobuf:"bytes,1,rep,name=kv,proto3" json:"kv,omitempty"` } func (x *ScanResponse) Reset() { *x = ScanResponse{} - mi := &file_service_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *ScanResponse) String() string { @@ -1075,7 +1145,7 @@ func (*ScanResponse) ProtoMessage() {} func (x *ScanResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[20] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1098,22 +1168,25 @@ func (x *ScanResponse) GetKv() []*Kv { } type PreWriteRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + // mutations is the list of mutations to apply atomically. Mutations []*Kv `protobuf:"bytes,2,rep,name=mutations,proto3" json:"mutations,omitempty"` // start_ts is the timestamp of the transaction. StartTs uint64 `protobuf:"varint,3,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` // lock_ttl is the TTL of the lock in milliseconds. - LockTtl uint64 `protobuf:"varint,4,opt,name=lock_ttl,json=lockTtl,proto3" json:"lock_ttl,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + LockTtl uint64 `protobuf:"varint,4,opt,name=lock_ttl,json=lockTtl,proto3" json:"lock_ttl,omitempty"` } func (x *PreWriteRequest) Reset() { *x = PreWriteRequest{} - mi := &file_service_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *PreWriteRequest) String() string { @@ -1124,7 +1197,7 @@ func (*PreWriteRequest) ProtoMessage() {} func (x *PreWriteRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[21] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1161,17 +1234,20 @@ func (x *PreWriteRequest) GetLockTtl() uint64 { } type PreCommitResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Errors []*KeyError `protobuf:"bytes,1,rep,name=errors,proto3" json:"errors,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Errors []*KeyError `protobuf:"bytes,1,rep,name=errors,proto3" json:"errors,omitempty"` } func (x *PreCommitResponse) Reset() { *x = PreCommitResponse{} - mi := &file_service_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *PreCommitResponse) String() string { @@ -1182,7 +1258,7 @@ func (*PreCommitResponse) ProtoMessage() {} func (x *PreCommitResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[22] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1205,19 +1281,22 @@ func (x *PreCommitResponse) GetErrors() []*KeyError { } type CommitRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - // start_ts is the timestamp of prewrite request. - StartTs uint64 `protobuf:"varint,1,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` - Keys [][]byte `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // start_ts is the timestamp of prewrite request. + StartTs uint64 `protobuf:"varint,1,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` + Keys [][]byte `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"` } func (x *CommitRequest) Reset() { *x = CommitRequest{} - mi := &file_service_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *CommitRequest) String() string { @@ -1228,7 +1307,7 @@ func (*CommitRequest) ProtoMessage() {} func (x *CommitRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[23] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1258,19 +1337,22 @@ func (x *CommitRequest) GetKeys() [][]byte { } type CommitResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` - Errors []*KeyError `protobuf:"bytes,3,rep,name=errors,proto3" json:"errors,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` + Errors []*KeyError `protobuf:"bytes,3,rep,name=errors,proto3" json:"errors,omitempty"` } func (x *CommitResponse) Reset() { *x = CommitResponse{} - mi := &file_service_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *CommitResponse) String() string { @@ -1281,7 +1363,7 @@ func (*CommitResponse) ProtoMessage() {} func (x *CommitResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[24] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1318,17 +1400,20 @@ func (x *CommitResponse) GetErrors() []*KeyError { } type RollbackRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - StartTs uint64 `protobuf:"varint,1,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StartTs uint64 `protobuf:"varint,1,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` } func (x *RollbackRequest) Reset() { *x = RollbackRequest{} - mi := &file_service_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RollbackRequest) String() string { @@ -1339,7 +1424,7 @@ func (*RollbackRequest) ProtoMessage() {} func (x *RollbackRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[25] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1362,17 +1447,20 @@ func (x *RollbackRequest) GetStartTs() uint64 { } type RollbackResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - unknownFields protoimpl.UnknownFields + state protoimpl.MessageState sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` } func (x *RollbackResponse) Reset() { *x = RollbackResponse{} - mi := &file_service_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) + if protoimpl.UnsafeEnabled { + mi := &file_service_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } } func (x *RollbackResponse) String() string { @@ -1383,7 +1471,7 @@ func (*RollbackResponse) ProtoMessage() {} func (x *RollbackResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[26] - if x != nil { + if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1407,113 +1495,171 @@ func (x *RollbackResponse) GetSuccess() bool { var File_service_proto protoreflect.FileDescriptor -const file_service_proto_rawDesc = "" + - "\n" + - "\rservice.proto\"7\n" + - "\rRawPutRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value\"M\n" + - "\x0eRawPutResponse\x12!\n" + - "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\"1\n" + - "\rRawGetRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\x12\x0e\n" + - "\x02ts\x18\x03 \x01(\x04R\x02ts\"J\n" + - "\x0eRawGetResponse\x12\"\n" + - "\rread_at_index\x18\x01 \x01(\x04R\vreadAtIndex\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value\"$\n" + - "\x10RawDeleteRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\"P\n" + - "\x11RawDeleteResponse\x12!\n" + - "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\",\n" + - "\x18RawLatestCommitTSRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\"C\n" + - "\x19RawLatestCommitTSResponse\x12\x0e\n" + - "\x02ts\x18\x01 \x01(\x04R\x02ts\x12\x16\n" + - "\x06exists\x18\x02 \x01(\bR\x06exists\"n\n" + - "\x10RawScanAtRequest\x12\x1b\n" + - "\tstart_key\x18\x01 \x01(\fR\bstartKey\x12\x17\n" + - "\aend_key\x18\x02 \x01(\fR\x06endKey\x12\x14\n" + - "\x05limit\x18\x03 \x01(\x03R\x05limit\x12\x0e\n" + - "\x02ts\x18\x04 \x01(\x04R\x02ts\"3\n" + - "\tRawKVPair\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value\"/\n" + - "\x11RawScanAtResponse\x12\x1a\n" + - "\x02kv\x18\x01 \x03(\v2\n" + - ".RawKVPairR\x02kv\"4\n" + - "\n" + - "PutRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value\"J\n" + - "\vPutResponse\x12!\n" + - "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\"!\n" + - "\rDeleteRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\"M\n" + - "\x0eDeleteResponse\x12!\n" + - "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\"\x1e\n" + - "\n" + - "GetRequest\x12\x10\n" + - "\x03key\x18\x01 \x01(\fR\x03key\"G\n" + - "\vGetResponse\x12\"\n" + - "\rread_at_index\x18\x01 \x01(\x04R\vreadAtIndex\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value\"B\n" + - "\bKeyError\x12\x18\n" + - "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" + - "\tretryable\x18\x02 \x01(\bR\tretryable\"M\n" + - "\x02Kv\x12\x1f\n" + - "\x05error\x18\x01 \x01(\v2\t.KeyErrorR\x05error\x12\x10\n" + - "\x03key\x18\x02 \x01(\fR\x03key\x12\x14\n" + - "\x05value\x18\x03 \x01(\fR\x05value\"Y\n" + - "\vScanRequest\x12\x1b\n" + - "\tstart_key\x18\x01 \x01(\fR\bstartKey\x12\x17\n" + - "\aend_key\x18\x02 \x01(\fR\x06endKey\x12\x14\n" + - "\x05limit\x18\x03 \x01(\x04R\x05limit\"#\n" + - "\fScanResponse\x12\x13\n" + - "\x02kv\x18\x01 \x03(\v2\x03.KvR\x02kv\"j\n" + - "\x0fPreWriteRequest\x12!\n" + - "\tmutations\x18\x02 \x03(\v2\x03.KvR\tmutations\x12\x19\n" + - "\bstart_ts\x18\x03 \x01(\x04R\astartTs\x12\x19\n" + - "\block_ttl\x18\x04 \x01(\x04R\alockTtl\"6\n" + - "\x11PreCommitResponse\x12!\n" + - "\x06errors\x18\x01 \x03(\v2\t.KeyErrorR\x06errors\">\n" + - "\rCommitRequest\x12\x19\n" + - "\bstart_ts\x18\x01 \x01(\x04R\astartTs\x12\x12\n" + - "\x04keys\x18\x02 \x03(\fR\x04keys\"p\n" + - "\x0eCommitResponse\x12!\n" + - "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + - "\asuccess\x18\x02 \x01(\bR\asuccess\x12!\n" + - "\x06errors\x18\x03 \x03(\v2\t.KeyErrorR\x06errors\",\n" + - "\x0fRollbackRequest\x12\x19\n" + - "\bstart_ts\x18\x01 \x01(\x04R\astartTs\",\n" + - "\x10RollbackResponse\x12\x18\n" + - "\asuccess\x18\x01 \x01(\bR\asuccess2\x9b\x02\n" + - "\x05RawKV\x12+\n" + - "\x06RawPut\x12\x0e.RawPutRequest\x1a\x0f.RawPutResponse\"\x00\x12+\n" + - "\x06RawGet\x12\x0e.RawGetRequest\x1a\x0f.RawGetResponse\"\x00\x124\n" + - "\tRawDelete\x12\x11.RawDeleteRequest\x1a\x12.RawDeleteResponse\"\x00\x12L\n" + - "\x11RawLatestCommitTS\x12\x19.RawLatestCommitTSRequest\x1a\x1a.RawLatestCommitTSResponse\"\x00\x124\n" + - "\tRawScanAt\x12\x11.RawScanAtRequest\x1a\x12.RawScanAtResponse\"\x002\xc1\x02\n" + - "\x0fTransactionalKV\x12\"\n" + - "\x03Put\x12\v.PutRequest\x1a\f.PutResponse\"\x00\x12\"\n" + - "\x03Get\x12\v.GetRequest\x1a\f.GetResponse\"\x00\x12+\n" + - "\x06Delete\x12\x0e.DeleteRequest\x1a\x0f.DeleteResponse\"\x00\x12%\n" + - "\x04Scan\x12\f.ScanRequest\x1a\r.ScanResponse\"\x00\x122\n" + - "\bPreWrite\x12\x10.PreWriteRequest\x1a\x12.PreCommitResponse\"\x00\x12+\n" + - "\x06Commit\x12\x0e.CommitRequest\x1a\x0f.CommitResponse\"\x00\x121\n" + - "\bRollback\x12\x10.RollbackRequest\x1a\x11.RollbackResponse\"\x00B#Z!github.com/bootjp/elastickv/protob\x06proto3" +var file_service_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x37, 0x0a, 0x0d, 0x52, 0x61, 0x77, 0x50, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4d, 0x0a, 0x0e, 0x52, 0x61, 0x77, 0x50, + 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x31, 0x0a, 0x0d, 0x52, 0x61, 0x77, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x73, 0x22, 0x62, 0x0a, 0x0e, 0x52, 0x61, + 0x77, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x41, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, + 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x24, + 0x0a, 0x10, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x11, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, + 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, + 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, + 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x2c, 0x0a, 0x18, 0x52, 0x61, 0x77, 0x4c, 0x61, 0x74, + 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x53, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x22, 0x43, 0x0a, 0x19, 0x52, 0x61, 0x77, 0x4c, 0x61, 0x74, 0x65, 0x73, + 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x53, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x6e, 0x0a, 0x10, 0x52, 0x61, 0x77, + 0x53, 0x63, 0x61, 0x6e, 0x41, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, + 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x65, 0x6e, + 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x65, 0x6e, 0x64, + 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x73, 0x22, 0x33, 0x0a, 0x09, 0x52, 0x61, 0x77, + 0x4b, 0x56, 0x50, 0x61, 0x69, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2f, + 0x0a, 0x11, 0x52, 0x61, 0x77, 0x53, 0x63, 0x61, 0x6e, 0x41, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x02, 0x6b, 0x76, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x0a, 0x2e, 0x52, 0x61, 0x77, 0x4b, 0x56, 0x50, 0x61, 0x69, 0x72, 0x52, 0x02, 0x6b, 0x76, 0x22, + 0x34, 0x0a, 0x0a, 0x50, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4a, 0x0a, 0x0b, 0x50, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, + 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x22, 0x21, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4d, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x63, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x22, 0x1e, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x22, 0x47, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x69, 0x6e, + 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x41, + 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x42, 0x0a, 0x08, + 0x4b, 0x65, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x72, 0x79, 0x61, 0x62, 0x6c, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x74, 0x72, 0x79, 0x61, 0x62, 0x6c, 0x65, + 0x22, 0x4d, 0x0a, 0x02, 0x4b, 0x76, 0x12, 0x1f, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x4b, 0x65, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, + 0x59, 0x0a, 0x0b, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x65, + 0x6e, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x65, 0x6e, + 0x64, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x23, 0x0a, 0x0c, 0x53, 0x63, + 0x61, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x13, 0x0a, 0x02, 0x6b, 0x76, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x03, 0x2e, 0x4b, 0x76, 0x52, 0x02, 0x6b, 0x76, 0x22, + 0x6a, 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x21, 0x0a, 0x09, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x03, 0x2e, 0x4b, 0x76, 0x52, 0x09, 0x6d, 0x75, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, + 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x74, 0x6c, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x07, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x74, 0x6c, 0x22, 0x36, 0x0a, 0x11, 0x50, + 0x72, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x21, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x09, 0x2e, 0x4b, 0x65, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0x22, 0x3e, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, + 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x6b, + 0x65, 0x79, 0x73, 0x22, 0x70, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, + 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x63, 0x6f, 0x6d, + 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x12, 0x21, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x4b, 0x65, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x2c, 0x0a, 0x0f, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x54, 0x73, 0x22, 0x2c, 0x0a, 0x10, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, + 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x32, 0x9b, 0x02, 0x0a, 0x05, 0x52, 0x61, 0x77, 0x4b, 0x56, 0x12, 0x2b, 0x0a, 0x06, 0x52, + 0x61, 0x77, 0x50, 0x75, 0x74, 0x12, 0x0e, 0x2e, 0x52, 0x61, 0x77, 0x50, 0x75, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x52, 0x61, 0x77, 0x50, 0x75, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x06, 0x52, 0x61, 0x77, 0x47, + 0x65, 0x74, 0x12, 0x0e, 0x2e, 0x52, 0x61, 0x77, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x52, 0x61, 0x77, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x12, 0x11, 0x2e, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x11, 0x52, + 0x61, 0x77, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x53, + 0x12, 0x19, 0x2e, 0x52, 0x61, 0x77, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x54, 0x53, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x52, 0x61, + 0x77, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x53, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x52, 0x61, 0x77, + 0x53, 0x63, 0x61, 0x6e, 0x41, 0x74, 0x12, 0x11, 0x2e, 0x52, 0x61, 0x77, 0x53, 0x63, 0x61, 0x6e, + 0x41, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x52, 0x61, 0x77, 0x53, + 0x63, 0x61, 0x6e, 0x41, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x32, + 0xc1, 0x02, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, + 0x6c, 0x4b, 0x56, 0x12, 0x22, 0x0a, 0x03, 0x50, 0x75, 0x74, 0x12, 0x0b, 0x2e, 0x50, 0x75, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x50, 0x75, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x22, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x0b, + 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x47, 0x65, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x06, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x0e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x25, 0x0a, 0x04, 0x53, 0x63, 0x61, 0x6e, + 0x12, 0x0c, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, + 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x32, 0x0a, 0x08, 0x50, 0x72, 0x65, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x10, 0x2e, 0x50, 0x72, + 0x65, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, + 0x50, 0x72, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x0e, 0x2e, + 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, + 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x31, 0x0a, 0x08, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x2e, 0x52, + 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, + 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x6a, 0x70, 0x2f, 0x65, 0x6c, 0x61, 0x73, 0x74, 0x69, 0x63, + 0x6b, 0x76, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} var ( file_service_proto_rawDescOnce sync.Once - file_service_proto_rawDescData []byte + file_service_proto_rawDescData = file_service_proto_rawDesc ) func file_service_proto_rawDescGZIP() []byte { file_service_proto_rawDescOnce.Do(func() { - file_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_service_proto_rawDesc), len(file_service_proto_rawDesc))) + file_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_service_proto_rawDescData) }) return file_service_proto_rawDescData } @@ -1591,11 +1737,337 @@ func file_service_proto_init() { if File_service_proto != nil { return } + if !protoimpl.UnsafeEnabled { + file_service_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*RawPutRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*RawPutResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*RawGetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*RawGetResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*RawDeleteRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*RawDeleteResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*RawLatestCommitTSRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*RawLatestCommitTSResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*RawScanAtRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[9].Exporter = func(v any, i int) any { + switch v := v.(*RawKVPair); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[10].Exporter = func(v any, i int) any { + switch v := v.(*RawScanAtResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[11].Exporter = func(v any, i int) any { + switch v := v.(*PutRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[12].Exporter = func(v any, i int) any { + switch v := v.(*PutResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[13].Exporter = func(v any, i int) any { + switch v := v.(*DeleteRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[14].Exporter = func(v any, i int) any { + switch v := v.(*DeleteResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[15].Exporter = func(v any, i int) any { + switch v := v.(*GetRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[16].Exporter = func(v any, i int) any { + switch v := v.(*GetResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[17].Exporter = func(v any, i int) any { + switch v := v.(*KeyError); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[18].Exporter = func(v any, i int) any { + switch v := v.(*Kv); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[19].Exporter = func(v any, i int) any { + switch v := v.(*ScanRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[20].Exporter = func(v any, i int) any { + switch v := v.(*ScanResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[21].Exporter = func(v any, i int) any { + switch v := v.(*PreWriteRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[22].Exporter = func(v any, i int) any { + switch v := v.(*PreCommitResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[23].Exporter = func(v any, i int) any { + switch v := v.(*CommitRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[24].Exporter = func(v any, i int) any { + switch v := v.(*CommitResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[25].Exporter = func(v any, i int) any { + switch v := v.(*RollbackRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_service_proto_msgTypes[26].Exporter = func(v any, i int) any { + switch v := v.(*RollbackResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_service_proto_rawDesc), len(file_service_proto_rawDesc)), + RawDescriptor: file_service_proto_rawDesc, NumEnums: 0, NumMessages: 27, NumExtensions: 0, @@ -1606,6 +2078,7 @@ func file_service_proto_init() { MessageInfos: file_service_proto_msgTypes, }.Build() File_service_proto = out.File + file_service_proto_rawDesc = nil file_service_proto_goTypes = nil file_service_proto_depIdxs = nil } diff --git a/proto/service.proto b/proto/service.proto index 2e52a778..4240a938 100644 --- a/proto/service.proto +++ b/proto/service.proto @@ -39,6 +39,7 @@ message RawGetRequest { message RawGetResponse { uint64 read_at_index = 1; bytes value = 2; + bool exists = 3; } message RawDeleteRequest { diff --git a/proto/service_grpc.pb.go b/proto/service_grpc.pb.go index 631c8814..2aed4711 100644 --- a/proto/service_grpc.pb.go +++ b/proto/service_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 -// - protoc v3.21.12 +// - protoc-gen-go-grpc v1.4.0 +// - protoc v5.29.3 // source: service.proto package proto @@ -15,8 +15,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 +// Requires gRPC-Go v1.62.0 or later. +const _ = grpc.SupportPackageIsVersion8 const ( RawKV_RawPut_FullMethodName = "/RawKV/RawPut" @@ -97,7 +97,7 @@ func (c *rawKVClient) RawScanAt(ctx context.Context, in *RawScanAtRequest, opts // RawKVServer is the server API for RawKV service. // All implementations must embed UnimplementedRawKVServer -// for forward compatibility. +// for forward compatibility type RawKVServer interface { RawPut(context.Context, *RawPutRequest) (*RawPutResponse, error) RawGet(context.Context, *RawGetRequest) (*RawGetResponse, error) @@ -107,30 +107,26 @@ type RawKVServer interface { mustEmbedUnimplementedRawKVServer() } -// UnimplementedRawKVServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedRawKVServer struct{} +// UnimplementedRawKVServer must be embedded to have forward compatible implementations. +type UnimplementedRawKVServer struct { +} func (UnimplementedRawKVServer) RawPut(context.Context, *RawPutRequest) (*RawPutResponse, error) { - return nil, status.Error(codes.Unimplemented, "method RawPut not implemented") + return nil, status.Errorf(codes.Unimplemented, "method RawPut not implemented") } func (UnimplementedRawKVServer) RawGet(context.Context, *RawGetRequest) (*RawGetResponse, error) { - return nil, status.Error(codes.Unimplemented, "method RawGet not implemented") + return nil, status.Errorf(codes.Unimplemented, "method RawGet not implemented") } func (UnimplementedRawKVServer) RawDelete(context.Context, *RawDeleteRequest) (*RawDeleteResponse, error) { - return nil, status.Error(codes.Unimplemented, "method RawDelete not implemented") + return nil, status.Errorf(codes.Unimplemented, "method RawDelete not implemented") } func (UnimplementedRawKVServer) RawLatestCommitTS(context.Context, *RawLatestCommitTSRequest) (*RawLatestCommitTSResponse, error) { - return nil, status.Error(codes.Unimplemented, "method RawLatestCommitTS not implemented") + return nil, status.Errorf(codes.Unimplemented, "method RawLatestCommitTS not implemented") } func (UnimplementedRawKVServer) RawScanAt(context.Context, *RawScanAtRequest) (*RawScanAtResponse, error) { - return nil, status.Error(codes.Unimplemented, "method RawScanAt not implemented") + return nil, status.Errorf(codes.Unimplemented, "method RawScanAt not implemented") } func (UnimplementedRawKVServer) mustEmbedUnimplementedRawKVServer() {} -func (UnimplementedRawKVServer) testEmbeddedByValue() {} // UnsafeRawKVServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to RawKVServer will @@ -140,13 +136,6 @@ type UnsafeRawKVServer interface { } func RegisterRawKVServer(s grpc.ServiceRegistrar, srv RawKVServer) { - // If the following call panics, it indicates UnimplementedRawKVServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } s.RegisterService(&RawKV_ServiceDesc, srv) } @@ -375,7 +364,7 @@ func (c *transactionalKVClient) Rollback(ctx context.Context, in *RollbackReques // TransactionalKVServer is the server API for TransactionalKV service. // All implementations must embed UnimplementedTransactionalKVServer -// for forward compatibility. +// for forward compatibility type TransactionalKVServer interface { Put(context.Context, *PutRequest) (*PutResponse, error) Get(context.Context, *GetRequest) (*GetResponse, error) @@ -387,36 +376,32 @@ type TransactionalKVServer interface { mustEmbedUnimplementedTransactionalKVServer() } -// UnimplementedTransactionalKVServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedTransactionalKVServer struct{} +// UnimplementedTransactionalKVServer must be embedded to have forward compatible implementations. +type UnimplementedTransactionalKVServer struct { +} func (UnimplementedTransactionalKVServer) Put(context.Context, *PutRequest) (*PutResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Put not implemented") + return nil, status.Errorf(codes.Unimplemented, "method Put not implemented") } func (UnimplementedTransactionalKVServer) Get(context.Context, *GetRequest) (*GetResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Get not implemented") + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") } func (UnimplementedTransactionalKVServer) Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Delete not implemented") + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") } func (UnimplementedTransactionalKVServer) Scan(context.Context, *ScanRequest) (*ScanResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Scan not implemented") + return nil, status.Errorf(codes.Unimplemented, "method Scan not implemented") } func (UnimplementedTransactionalKVServer) PreWrite(context.Context, *PreWriteRequest) (*PreCommitResponse, error) { - return nil, status.Error(codes.Unimplemented, "method PreWrite not implemented") + return nil, status.Errorf(codes.Unimplemented, "method PreWrite not implemented") } func (UnimplementedTransactionalKVServer) Commit(context.Context, *CommitRequest) (*CommitResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Commit not implemented") + return nil, status.Errorf(codes.Unimplemented, "method Commit not implemented") } func (UnimplementedTransactionalKVServer) Rollback(context.Context, *RollbackRequest) (*RollbackResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Rollback not implemented") + return nil, status.Errorf(codes.Unimplemented, "method Rollback not implemented") } func (UnimplementedTransactionalKVServer) mustEmbedUnimplementedTransactionalKVServer() {} -func (UnimplementedTransactionalKVServer) testEmbeddedByValue() {} // UnsafeTransactionalKVServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to TransactionalKVServer will @@ -426,13 +411,6 @@ type UnsafeTransactionalKVServer interface { } func RegisterTransactionalKVServer(s grpc.ServiceRegistrar, srv TransactionalKVServer) { - // If the following call panics, it indicates UnimplementedTransactionalKVServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } s.RegisterService(&TransactionalKV_ServiceDesc, srv) } From 965985a5ad6513fb5bd26f8019ef3347475353e5 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Tue, 17 Feb 2026 05:40:12 +0900 Subject: [PATCH 11/20] kv/adapter: keep RawGet compatibility across versions --- adapter/redis.go | 4 +++- kv/leader_routed_store.go | 4 +++- kv/shard_store.go | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/adapter/redis.go b/adapter/redis.go index fd63984e..ae759d5b 100644 --- a/adapter/redis.go +++ b/adapter/redis.go @@ -1175,7 +1175,9 @@ func (r *RedisServer) tryLeaderGetAt(key []byte, ts uint64) ([]byte, error) { if err != nil { return nil, errors.WithStack(err) } - if !resp.GetExists() { + // Compatibility with older nodes that don't set RawGetResponse.exists: + // treat non-empty value as found even when exists=false. + if !resp.GetExists() && len(resp.GetValue()) == 0 { return nil, errors.WithStack(store.ErrKeyNotFound) } return resp.Value, nil diff --git a/kv/leader_routed_store.go b/kv/leader_routed_store.go index da0a5f70..ddee9a0e 100644 --- a/kv/leader_routed_store.go +++ b/kv/leader_routed_store.go @@ -66,7 +66,9 @@ func (s *LeaderRoutedStore) proxyRawGet(ctx context.Context, key []byte, ts uint if err != nil { return nil, errors.WithStack(err) } - if !resp.GetExists() { + // Compatibility with older nodes that don't set RawGetResponse.exists: + // treat non-empty value as found even when exists=false. + if !resp.GetExists() && len(resp.GetValue()) == 0 { return nil, store.ErrKeyNotFound } return resp.Value, nil diff --git a/kv/shard_store.go b/kv/shard_store.go index a41b009f..43cd4689 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -879,7 +879,9 @@ func (s *ShardStore) proxyRawGet(ctx context.Context, g *ShardGroup, key []byte, if err != nil { return nil, errors.WithStack(err) } - if !resp.GetExists() { + // Compatibility with older nodes that don't set RawGetResponse.exists: + // treat non-empty value as found even when exists=false. + if !resp.GetExists() && len(resp.GetValue()) == 0 { return nil, store.ErrKeyNotFound } return resp.Value, nil From 11c13cae0f3a0afe213ccbc91f618e5e0ebc1af1 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Tue, 17 Feb 2026 06:08:30 +0900 Subject: [PATCH 12/20] kv: harden lock resolution and txn commit retries --- adapter/redis.go | 4 +- kv/leader_routed_store.go | 4 +- kv/shard_store.go | 150 ++++- kv/shard_store_txn_lock_test.go | 69 ++ kv/sharded_coordinator.go | 31 +- proto/service.pb.go | 1100 +++++++++---------------------- proto/service_grpc.pb.go | 68 +- 7 files changed, 595 insertions(+), 831 deletions(-) diff --git a/adapter/redis.go b/adapter/redis.go index ae759d5b..49cd433e 100644 --- a/adapter/redis.go +++ b/adapter/redis.go @@ -1176,8 +1176,8 @@ func (r *RedisServer) tryLeaderGetAt(key []byte, ts uint64) ([]byte, error) { return nil, errors.WithStack(err) } // Compatibility with older nodes that don't set RawGetResponse.exists: - // treat non-empty value as found even when exists=false. - if !resp.GetExists() && len(resp.GetValue()) == 0 { + // treat any non-nil payload as found even when exists=false. + if !resp.GetExists() && resp.GetValue() == nil { return nil, errors.WithStack(store.ErrKeyNotFound) } return resp.Value, nil diff --git a/kv/leader_routed_store.go b/kv/leader_routed_store.go index ddee9a0e..f8e76970 100644 --- a/kv/leader_routed_store.go +++ b/kv/leader_routed_store.go @@ -67,8 +67,8 @@ func (s *LeaderRoutedStore) proxyRawGet(ctx context.Context, key []byte, ts uint return nil, errors.WithStack(err) } // Compatibility with older nodes that don't set RawGetResponse.exists: - // treat non-empty value as found even when exists=false. - if !resp.GetExists() && len(resp.GetValue()) == 0 { + // treat any non-nil payload as found even when exists=false. + if !resp.GetExists() && resp.GetValue() == nil { return nil, store.ErrKeyNotFound } return resp.Value, nil diff --git a/kv/shard_store.go b/kv/shard_store.go index 43cd4689..79e93e33 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -183,13 +183,37 @@ func (s *ShardStore) scanRouteAtLeader(ctx context.Context, g *ShardGroup, start if err != nil { return nil, errors.WithStack(err) } - lockKVs, err := scanTxnLockRangeAt(ctx, g, start, end, limit, ts) + lockEnd := scanLockUpperBoundForKVs(kvs, end) + lockKVs, err := scanTxnLockRangeAt(ctx, g, start, lockEnd, ts) if err != nil { return nil, err } return s.resolveScanLocks(ctx, g, kvs, lockKVs, ts) } +func scanLockUpperBoundForKVs(kvs []*store.KVPair, scanEnd []byte) []byte { + lastUserKey := lastNonInternalScanKey(kvs) + if len(lastUserKey) == 0 { + return scanEnd + } + bound := nextScanCursor(lastUserKey) + if scanEnd == nil || bytes.Compare(bound, scanEnd) < 0 { + return bound + } + return scanEnd +} + +func lastNonInternalScanKey(kvs []*store.KVPair) []byte { + for i := len(kvs) - 1; i >= 0; i-- { + kvp := kvs[i] + if kvp == nil || isTxnInternalKey(kvp.Key) { + continue + } + return kvp.Key + } + return nil +} + func (s *ShardStore) groupForID(groupID uint64) (*ShardGroup, bool) { g, ok := s.groups[groupID] return g, ok @@ -555,16 +579,59 @@ func txnUserKeyFromLockKey(lockKey []byte) ([]byte, bool) { return bytes.Clone(lockKey[len(txnLockPrefix):]), true } -func scanTxnLockRangeAt(ctx context.Context, g *ShardGroup, start []byte, end []byte, limit int, ts uint64) ([]*store.KVPair, error) { - if g == nil || g.Store == nil || limit <= 0 { +func scanTxnLockRangeAt(ctx context.Context, g *ShardGroup, start []byte, end []byte, ts uint64) ([]*store.KVPair, error) { + if g == nil || g.Store == nil { return []*store.KVPair{}, nil } + lockStart, lockEnd := txnLockScanBounds(start, end) - lockKVs, err := g.Store.ScanAt(ctx, lockStart, lockEnd, limit, ts) + return scanTxnLockPagesAt(ctx, g.Store, lockStart, lockEnd, ts) +} + +func scanTxnLockPagesAt(ctx context.Context, st store.MVCCStore, start []byte, end []byte, ts uint64) ([]*store.KVPair, error) { + out := make([]*store.KVPair, 0) + cursor := start + for { + lockKVs, nextCursor, done, err := scanTxnLockPageAt(ctx, st, cursor, end, ts) + if err != nil { + return nil, err + } + out = append(out, lockKVs...) + if done { + return out, nil + } + cursor = nextCursor + } +} + +func scanTxnLockPageAt(ctx context.Context, st store.MVCCStore, start []byte, end []byte, ts uint64) ([]*store.KVPair, []byte, bool, error) { + const lockPageLimit = 256 + + lockKVs, err := st.ScanAt(ctx, start, end, lockPageLimit, ts) if err != nil { - return nil, errors.WithStack(err) + return nil, nil, false, errors.WithStack(err) + } + if len(lockKVs) == 0 || len(lockKVs) < lockPageLimit { + return lockKVs, nil, true, nil + } + + last := lockKVs[len(lockKVs)-1] + if last == nil || len(last.Key) == 0 { + return lockKVs, nil, true, nil } - return lockKVs, nil + + nextCursor := nextScanCursor(last.Key) + if end != nil && bytes.Compare(nextCursor, end) >= 0 { + return lockKVs, nil, true, nil + } + + return lockKVs, nextCursor, false, nil +} + +func nextScanCursor(lastKey []byte) []byte { + next := make([]byte, len(lastKey)+1) + copy(next, lastKey) + return next } func txnLockScanBounds(start []byte, end []byte) ([]byte, []byte) { @@ -648,35 +715,77 @@ const ( ) func (s *ShardStore) primaryTxnStatus(ctx context.Context, primaryKey []byte, startTS uint64) (txnStatus, uint64, error) { - commitTS, committed, err := s.txnCommitTS(ctx, primaryKey, startTS) + status, commitTS, done, err := s.primaryTxnRecordedStatus(ctx, primaryKey, startTS) + if err != nil || done { + return status, commitTS, err + } + + lock, locked, err := s.primaryTxnLock(ctx, primaryKey, startTS) if err != nil { return txnStatusPending, 0, err } + if !locked { + return txnStatusRolledBack, 0, nil + } + if !txnLockExpired(lock) { + return txnStatusPending, 0, nil + } + return s.expiredPrimaryTxnStatus(ctx, primaryKey, startTS) +} + +func (s *ShardStore) primaryTxnRecordedStatus(ctx context.Context, primaryKey []byte, startTS uint64) (txnStatus, uint64, bool, error) { + commitTS, committed, err := s.txnCommitTS(ctx, primaryKey, startTS) + if err != nil { + return txnStatusPending, 0, false, err + } if committed { - return txnStatusCommitted, commitTS, nil + return txnStatusCommitted, commitTS, true, nil } rolledBack, err := s.hasTxnRollback(ctx, primaryKey, startTS) if err != nil { - return txnStatusPending, 0, err + return txnStatusPending, 0, false, err } if rolledBack { - return txnStatusRolledBack, 0, nil + return txnStatusRolledBack, 0, true, nil } + return txnStatusPending, 0, false, nil +} +func (s *ShardStore) primaryTxnLock(ctx context.Context, primaryKey []byte, startTS uint64) (txnLock, bool, error) { lock, ok, err := s.loadTxnLock(ctx, primaryKey) if err != nil { - return txnStatusPending, 0, err + return txnLock{}, false, err } if !ok || lock.StartTS != startTS { - return txnStatusRolledBack, 0, nil + return txnLock{}, false, nil } + return lock, true, nil +} + +func txnLockExpired(lock txnLock) bool { + return lock.TTLExpireAt != 0 && hlcWallNow() > lock.TTLExpireAt +} - if lock.TTLExpireAt != 0 && hlcWallNow() > lock.TTLExpireAt { - s.bestEffortAbortPrimary(primaryKey, startTS) +func (s *ShardStore) expiredPrimaryTxnStatus(ctx context.Context, primaryKey []byte, startTS uint64) (txnStatus, uint64, error) { + aborted, err := s.tryAbortExpiredPrimary(primaryKey, startTS) + if err != nil { + return s.statusAfterAbortFailure(ctx, primaryKey, startTS) + } + if aborted { return txnStatusRolledBack, 0, nil } + return txnStatusPending, 0, nil +} +func (s *ShardStore) statusAfterAbortFailure(ctx context.Context, primaryKey []byte, startTS uint64) (txnStatus, uint64, error) { + if commitTS, committed, err := s.txnCommitTS(ctx, primaryKey, startTS); err == nil && committed { + return txnStatusCommitted, commitTS, nil + } + if rolledBack, err := s.hasTxnRollback(ctx, primaryKey, startTS); err == nil && rolledBack { + return txnStatusRolledBack, 0, nil + } + // Keep reads conservative when timeout cleanup cannot be confirmed. return txnStatusPending, 0, nil } @@ -721,12 +830,15 @@ func (s *ShardStore) loadTxnLock(ctx context.Context, primaryKey []byte) (txnLoc return lock, true, nil } -func (s *ShardStore) bestEffortAbortPrimary(primaryKey []byte, startTS uint64) { +func (s *ShardStore) tryAbortExpiredPrimary(primaryKey []byte, startTS uint64) (bool, error) { pg, ok := s.groupForKey(primaryKey) if !ok || pg == nil || pg.Txn == nil { - return + return false, nil + } + if err := applyTxnResolution(pg, pb.Phase_ABORT, startTS, cleanupTS(startTS), primaryKey, [][]byte{primaryKey}); err != nil { + return false, err } - _ = applyTxnResolution(pg, pb.Phase_ABORT, startTS, cleanupTS(startTS), primaryKey, [][]byte{primaryKey}) + return true, nil } func applyTxnResolution(g *ShardGroup, phase pb.Phase, startTS, commitTS uint64, primaryKey []byte, keys [][]byte) error { @@ -880,8 +992,8 @@ func (s *ShardStore) proxyRawGet(ctx context.Context, g *ShardGroup, key []byte, return nil, errors.WithStack(err) } // Compatibility with older nodes that don't set RawGetResponse.exists: - // treat non-empty value as found even when exists=false. - if !resp.GetExists() && len(resp.GetValue()) == 0 { + // treat any non-nil payload as found even when exists=false. + if !resp.GetExists() && resp.GetValue() == nil { return nil, store.ErrKeyNotFound } return resp.Value, nil diff --git a/kv/shard_store_txn_lock_test.go b/kv/shard_store_txn_lock_test.go index a267826e..f6a78819 100644 --- a/kv/shard_store_txn_lock_test.go +++ b/kv/shard_store_txn_lock_test.go @@ -203,6 +203,75 @@ func TestShardStoreScanAt_ReturnsTxnLockedForPendingLockWithoutCommittedValue(t require.True(t, errors.Is(err, ErrTxnLocked), "expected ErrTxnLocked, got %v", err) } +func TestShardStoreScanAt_ReturnsTxnLockedWhenPendingLockExceedsUserLimit(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + + st1 := store.NewMVCCStore() + r1, stop1 := newSingleRaft(t, "g1", NewKvFSM(st1)) + defer stop1() + + groups := map[uint64]*ShardGroup{ + 1: {Raft: r1, Store: st1, Txn: NewLeaderProxy(r1)}, + } + shardStore := NewShardStore(engine, groups) + + // A normal committed user key so ScanAt can return data if lock checks miss. + require.NoError(t, st1.PutAt(ctx, []byte("c"), []byte("visible"), 1, 0)) + + committedPrimary := []byte("p") + committedSecondary := []byte("a") + committedStartTS := uint64(2) + committedCommitTS := uint64(3) + prepareCommitted := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: committedStartTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: committedPrimary, LockTTLms: defaultTxnLockTTLms, CommitTS: 0})}, + {Op: pb.Op_PUT, Key: committedPrimary, Value: []byte("vp")}, + {Op: pb.Op_PUT, Key: committedSecondary, Value: []byte("va")}, + }, + } + _, err := groups[1].Txn.Commit([]*pb.Request{prepareCommitted}) + require.NoError(t, err) + commitCommittedPrimary := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: committedStartTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: committedPrimary, CommitTS: committedCommitTS})}, + {Op: pb.Op_PUT, Key: committedPrimary}, + }, + } + _, err = groups[1].Txn.Commit([]*pb.Request{commitCommittedPrimary}) + require.NoError(t, err) + + // Create a later pending lock-only write that must block the scan. + pendingPrimary := []byte("b") + pendingStartTS := uint64(4) + preparePending := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: pendingStartTS, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte(txnMetaPrefix), Value: EncodeTxnMeta(TxnMeta{PrimaryKey: pendingPrimary, LockTTLms: defaultTxnLockTTLms, CommitTS: 0})}, + {Op: pb.Op_PUT, Key: pendingPrimary, Value: []byte("vb")}, + }, + } + _, err = groups[1].Txn.Commit([]*pb.Request{preparePending}) + require.NoError(t, err) + + // limit=1 should not hide pending locks after one resolved lock. + _, err = shardStore.ScanAt(ctx, []byte("a"), []byte("z"), 1, ^uint64(0)) + require.Error(t, err) + require.True(t, errors.Is(err, ErrTxnLocked), "expected ErrTxnLocked, got %v", err) +} + func TestShardStoreScanAt_ResolvesCommittedSecondaryLocks(t *testing.T) { t.Parallel() diff --git a/kv/sharded_coordinator.go b/kv/sharded_coordinator.go index 2fb51e71..ceb042ae 100644 --- a/kv/sharded_coordinator.go +++ b/kv/sharded_coordinator.go @@ -5,6 +5,7 @@ import ( "context" "log/slog" "sort" + "time" "github.com/bootjp/elastickv/distribution" pb "github.com/bootjp/elastickv/proto" @@ -19,7 +20,12 @@ type ShardGroup struct { Txn Transactional } -const txnPhaseCount = 2 +const ( + txnPhaseCount = 2 + + txnSecondaryCommitRetryAttempts = 3 + txnSecondaryCommitRetryBackoff = 20 * time.Millisecond +) // ShardedCoordinator routes operations to shard-specific raft groups. // It issues timestamps via a shared HLC and uses ShardRouter to dispatch. @@ -175,7 +181,8 @@ func (c *ShardedCoordinator) commitPrimaryTxn(startTS uint64, primaryKey []byte, func (c *ShardedCoordinator) commitSecondaryTxns(startTS uint64, primaryGid uint64, primaryKey []byte, grouped map[uint64][]*pb.Mutation, gids []uint64, commitTS uint64, maxIndex uint64) uint64 { // Secondary commits are best-effort. If a shard is unavailable after the // primary commits, read-time lock resolution will commit the remaining - // secondaries based on the primary commit record. + // secondaries based on the primary commit record. Retry a few times to + // absorb short leader-election windows and reduce lock lag. meta := txnMetaMutation(primaryKey, 0, commitTS) for _, gid := range gids { if gid == primaryGid { @@ -191,7 +198,7 @@ func (c *ShardedCoordinator) commitSecondaryTxns(startTS uint64, primaryGid uint Ts: startTS, Mutations: append([]*pb.Mutation{meta}, keyMutations(grouped[gid])...), } - r, err := g.Txn.Commit([]*pb.Request{req}) + r, err := commitSecondaryWithRetry(g, req) if err != nil { slog.Warn("txn secondary commit failed", slog.Uint64("gid", gid), @@ -209,6 +216,24 @@ func (c *ShardedCoordinator) commitSecondaryTxns(startTS uint64, primaryGid uint return maxIndex } +func commitSecondaryWithRetry(g *ShardGroup, req *pb.Request) (*TransactionResponse, error) { + if g == nil || g.Txn == nil || req == nil { + return nil, errors.WithStack(ErrInvalidRequest) + } + var lastErr error + for attempt := 0; attempt < txnSecondaryCommitRetryAttempts; attempt++ { + resp, err := g.Txn.Commit([]*pb.Request{req}) + if err == nil { + return resp, nil + } + lastErr = err + if attempt+1 < txnSecondaryCommitRetryAttempts { + time.Sleep(txnSecondaryCommitRetryBackoff * time.Duration(attempt+1)) + } + } + return nil, errors.WithStack(lastErr) +} + func (c *ShardedCoordinator) abortPreparedTxn(startTS uint64, primaryKey []byte, prepared []preparedGroup, abortTS uint64) { if len(prepared) == 0 { return diff --git a/proto/service.pb.go b/proto/service.pb.go index a7f19270..11bc5ee0 100644 --- a/proto/service.pb.go +++ b/proto/service.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: service.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -21,21 +22,18 @@ const ( ) type RawPutRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields - - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawPutRequest) Reset() { *x = RawPutRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawPutRequest) String() string { @@ -46,7 +44,7 @@ func (*RawPutRequest) ProtoMessage() {} func (x *RawPutRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -76,21 +74,18 @@ func (x *RawPutRequest) GetValue() []byte { } type RawPutResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` unknownFields protoimpl.UnknownFields - - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawPutResponse) Reset() { *x = RawPutResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawPutResponse) String() string { @@ -101,7 +96,7 @@ func (*RawPutResponse) ProtoMessage() {} func (x *RawPutResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -131,21 +126,18 @@ func (x *RawPutResponse) GetSuccess() bool { } type RawGetRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Ts uint64 `protobuf:"varint,3,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC unknownFields protoimpl.UnknownFields - - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Ts uint64 `protobuf:"varint,3,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC + sizeCache protoimpl.SizeCache } func (x *RawGetRequest) Reset() { *x = RawGetRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawGetRequest) String() string { @@ -156,7 +148,7 @@ func (*RawGetRequest) ProtoMessage() {} func (x *RawGetRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -186,22 +178,19 @@ func (x *RawGetRequest) GetTs() uint64 { } type RawGetResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"` unknownFields protoimpl.UnknownFields - - ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawGetResponse) Reset() { *x = RawGetResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawGetResponse) String() string { @@ -212,7 +201,7 @@ func (*RawGetResponse) ProtoMessage() {} func (x *RawGetResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -249,20 +238,17 @@ func (x *RawGetResponse) GetExists() bool { } type RawDeleteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields - - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawDeleteRequest) Reset() { *x = RawDeleteRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawDeleteRequest) String() string { @@ -273,7 +259,7 @@ func (*RawDeleteRequest) ProtoMessage() {} func (x *RawDeleteRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -296,21 +282,18 @@ func (x *RawDeleteRequest) GetKey() []byte { } type RawDeleteResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` unknownFields protoimpl.UnknownFields - - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawDeleteResponse) Reset() { *x = RawDeleteResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawDeleteResponse) String() string { @@ -321,7 +304,7 @@ func (*RawDeleteResponse) ProtoMessage() {} func (x *RawDeleteResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -351,20 +334,17 @@ func (x *RawDeleteResponse) GetSuccess() bool { } type RawLatestCommitTSRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields - - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawLatestCommitTSRequest) Reset() { *x = RawLatestCommitTSRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawLatestCommitTSRequest) String() string { @@ -375,7 +355,7 @@ func (*RawLatestCommitTSRequest) ProtoMessage() {} func (x *RawLatestCommitTSRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -398,21 +378,18 @@ func (x *RawLatestCommitTSRequest) GetKey() []byte { } type RawLatestCommitTSResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ts uint64 `protobuf:"varint,1,opt,name=ts,proto3" json:"ts,omitempty"` + Exists bool `protobuf:"varint,2,opt,name=exists,proto3" json:"exists,omitempty"` unknownFields protoimpl.UnknownFields - - Ts uint64 `protobuf:"varint,1,opt,name=ts,proto3" json:"ts,omitempty"` - Exists bool `protobuf:"varint,2,opt,name=exists,proto3" json:"exists,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawLatestCommitTSResponse) Reset() { *x = RawLatestCommitTSResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawLatestCommitTSResponse) String() string { @@ -423,7 +400,7 @@ func (*RawLatestCommitTSResponse) ProtoMessage() {} func (x *RawLatestCommitTSResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -453,23 +430,20 @@ func (x *RawLatestCommitTSResponse) GetExists() bool { } type RawScanAtRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` + EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` + Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // validated against host int size; large values may be rejected + Ts uint64 `protobuf:"varint,4,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC unknownFields protoimpl.UnknownFields - - StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` - EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` - Limit int64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // validated against host int size; large values may be rejected - Ts uint64 `protobuf:"varint,4,opt,name=ts,proto3" json:"ts,omitempty"` // optional read timestamp; if zero, server uses current HLC + sizeCache protoimpl.SizeCache } func (x *RawScanAtRequest) Reset() { *x = RawScanAtRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawScanAtRequest) String() string { @@ -480,7 +454,7 @@ func (*RawScanAtRequest) ProtoMessage() {} func (x *RawScanAtRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -524,21 +498,18 @@ func (x *RawScanAtRequest) GetTs() uint64 { } type RawKVPair struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields - - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawKVPair) Reset() { *x = RawKVPair{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawKVPair) String() string { @@ -549,7 +520,7 @@ func (*RawKVPair) ProtoMessage() {} func (x *RawKVPair) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -579,20 +550,17 @@ func (x *RawKVPair) GetValue() []byte { } type RawScanAtResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Kv []*RawKVPair `protobuf:"bytes,1,rep,name=kv,proto3" json:"kv,omitempty"` unknownFields protoimpl.UnknownFields - - Kv []*RawKVPair `protobuf:"bytes,1,rep,name=kv,proto3" json:"kv,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RawScanAtResponse) Reset() { *x = RawScanAtResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawScanAtResponse) String() string { @@ -603,7 +571,7 @@ func (*RawScanAtResponse) ProtoMessage() {} func (x *RawScanAtResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -626,21 +594,18 @@ func (x *RawScanAtResponse) GetKv() []*RawKVPair { } type PutRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields - - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PutRequest) Reset() { *x = PutRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PutRequest) String() string { @@ -651,7 +616,7 @@ func (*PutRequest) ProtoMessage() {} func (x *PutRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -681,21 +646,18 @@ func (x *PutRequest) GetValue() []byte { } type PutResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` unknownFields protoimpl.UnknownFields - - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PutResponse) Reset() { *x = PutResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PutResponse) String() string { @@ -706,7 +668,7 @@ func (*PutResponse) ProtoMessage() {} func (x *PutResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -736,20 +698,17 @@ func (x *PutResponse) GetSuccess() bool { } type DeleteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields - - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteRequest) Reset() { *x = DeleteRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteRequest) String() string { @@ -760,7 +719,7 @@ func (*DeleteRequest) ProtoMessage() {} func (x *DeleteRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[13] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -783,21 +742,18 @@ func (x *DeleteRequest) GetKey() []byte { } type DeleteResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` unknownFields protoimpl.UnknownFields - - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DeleteResponse) Reset() { *x = DeleteResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DeleteResponse) String() string { @@ -808,7 +764,7 @@ func (*DeleteResponse) ProtoMessage() {} func (x *DeleteResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -838,20 +794,17 @@ func (x *DeleteResponse) GetSuccess() bool { } type GetRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` unknownFields protoimpl.UnknownFields - - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetRequest) Reset() { *x = GetRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetRequest) String() string { @@ -862,7 +815,7 @@ func (*GetRequest) ProtoMessage() {} func (x *GetRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -885,21 +838,18 @@ func (x *GetRequest) GetKey() []byte { } type GetResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` + Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields - - ReadAtIndex uint64 `protobuf:"varint,1,opt,name=read_at_index,json=readAtIndex,proto3" json:"read_at_index,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetResponse) Reset() { *x = GetResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetResponse) String() string { @@ -910,7 +860,7 @@ func (*GetResponse) ProtoMessage() {} func (x *GetResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -940,21 +890,18 @@ func (x *GetResponse) GetValue() []byte { } type KeyError struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Retryable bool `protobuf:"varint,2,opt,name=retryable,proto3" json:"retryable,omitempty"` unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - Retryable bool `protobuf:"varint,2,opt,name=retryable,proto3" json:"retryable,omitempty"` + sizeCache protoimpl.SizeCache } func (x *KeyError) Reset() { *x = KeyError{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *KeyError) String() string { @@ -965,7 +912,7 @@ func (*KeyError) ProtoMessage() {} func (x *KeyError) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[17] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -995,22 +942,19 @@ func (x *KeyError) GetRetryable() bool { } type Kv struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Error *KeyError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` unknownFields protoimpl.UnknownFields - - Error *KeyError `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` - Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` - Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Kv) Reset() { *x = Kv{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Kv) String() string { @@ -1021,7 +965,7 @@ func (*Kv) ProtoMessage() {} func (x *Kv) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[18] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1058,22 +1002,19 @@ func (x *Kv) GetValue() []byte { } type ScanRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` + EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` + Limit uint64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` unknownFields protoimpl.UnknownFields - - StartKey []byte `protobuf:"bytes,1,opt,name=start_key,json=startKey,proto3" json:"start_key,omitempty"` - EndKey []byte `protobuf:"bytes,2,opt,name=end_key,json=endKey,proto3" json:"end_key,omitempty"` - Limit uint64 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ScanRequest) Reset() { *x = ScanRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ScanRequest) String() string { @@ -1084,7 +1025,7 @@ func (*ScanRequest) ProtoMessage() {} func (x *ScanRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[19] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1121,20 +1062,17 @@ func (x *ScanRequest) GetLimit() uint64 { } type ScanResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Kv []*Kv `protobuf:"bytes,1,rep,name=kv,proto3" json:"kv,omitempty"` unknownFields protoimpl.UnknownFields - - Kv []*Kv `protobuf:"bytes,1,rep,name=kv,proto3" json:"kv,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ScanResponse) Reset() { *x = ScanResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ScanResponse) String() string { @@ -1145,7 +1083,7 @@ func (*ScanResponse) ProtoMessage() {} func (x *ScanResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1168,25 +1106,22 @@ func (x *ScanResponse) GetKv() []*Kv { } type PreWriteRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // mutations is the list of mutations to apply atomically. Mutations []*Kv `protobuf:"bytes,2,rep,name=mutations,proto3" json:"mutations,omitempty"` // start_ts is the timestamp of the transaction. StartTs uint64 `protobuf:"varint,3,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` // lock_ttl is the TTL of the lock in milliseconds. - LockTtl uint64 `protobuf:"varint,4,opt,name=lock_ttl,json=lockTtl,proto3" json:"lock_ttl,omitempty"` + LockTtl uint64 `protobuf:"varint,4,opt,name=lock_ttl,json=lockTtl,proto3" json:"lock_ttl,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PreWriteRequest) Reset() { *x = PreWriteRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PreWriteRequest) String() string { @@ -1197,7 +1132,7 @@ func (*PreWriteRequest) ProtoMessage() {} func (x *PreWriteRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1234,20 +1169,17 @@ func (x *PreWriteRequest) GetLockTtl() uint64 { } type PreCommitResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Errors []*KeyError `protobuf:"bytes,1,rep,name=errors,proto3" json:"errors,omitempty"` unknownFields protoimpl.UnknownFields - - Errors []*KeyError `protobuf:"bytes,1,rep,name=errors,proto3" json:"errors,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PreCommitResponse) Reset() { *x = PreCommitResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PreCommitResponse) String() string { @@ -1258,7 +1190,7 @@ func (*PreCommitResponse) ProtoMessage() {} func (x *PreCommitResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1281,22 +1213,19 @@ func (x *PreCommitResponse) GetErrors() []*KeyError { } type CommitRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // start_ts is the timestamp of prewrite request. - StartTs uint64 `protobuf:"varint,1,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` - Keys [][]byte `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"` + StartTs uint64 `protobuf:"varint,1,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` + Keys [][]byte `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CommitRequest) Reset() { *x = CommitRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CommitRequest) String() string { @@ -1307,7 +1236,7 @@ func (*CommitRequest) ProtoMessage() {} func (x *CommitRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[23] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1337,22 +1266,19 @@ func (x *CommitRequest) GetKeys() [][]byte { } type CommitResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` + Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` + Errors []*KeyError `protobuf:"bytes,3,rep,name=errors,proto3" json:"errors,omitempty"` unknownFields protoimpl.UnknownFields - - CommitIndex uint64 `protobuf:"varint,1,opt,name=commit_index,json=commitIndex,proto3" json:"commit_index,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` - Errors []*KeyError `protobuf:"bytes,3,rep,name=errors,proto3" json:"errors,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CommitResponse) Reset() { *x = CommitResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CommitResponse) String() string { @@ -1363,7 +1289,7 @@ func (*CommitResponse) ProtoMessage() {} func (x *CommitResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1400,20 +1326,17 @@ func (x *CommitResponse) GetErrors() []*KeyError { } type RollbackRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + StartTs uint64 `protobuf:"varint,1,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` unknownFields protoimpl.UnknownFields - - StartTs uint64 `protobuf:"varint,1,opt,name=start_ts,json=startTs,proto3" json:"start_ts,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RollbackRequest) Reset() { *x = RollbackRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RollbackRequest) String() string { @@ -1424,7 +1347,7 @@ func (*RollbackRequest) ProtoMessage() {} func (x *RollbackRequest) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[25] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1447,20 +1370,17 @@ func (x *RollbackRequest) GetStartTs() uint64 { } type RollbackResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` unknownFields protoimpl.UnknownFields - - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RollbackResponse) Reset() { *x = RollbackResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_service_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_service_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RollbackResponse) String() string { @@ -1471,7 +1391,7 @@ func (*RollbackResponse) ProtoMessage() {} func (x *RollbackResponse) ProtoReflect() protoreflect.Message { mi := &file_service_proto_msgTypes[26] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1495,171 +1415,114 @@ func (x *RollbackResponse) GetSuccess() bool { var File_service_proto protoreflect.FileDescriptor -var file_service_proto_rawDesc = []byte{ - 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0x37, 0x0a, 0x0d, 0x52, 0x61, 0x77, 0x50, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4d, 0x0a, 0x0e, 0x52, 0x61, 0x77, 0x50, - 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, - 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, - 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x31, 0x0a, 0x0d, 0x52, 0x61, 0x77, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x73, 0x22, 0x62, 0x0a, 0x0e, 0x52, 0x61, - 0x77, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, - 0x72, 0x65, 0x61, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x41, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x24, - 0x0a, 0x10, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x22, 0x50, 0x0a, 0x11, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, - 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, - 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x2c, 0x0a, 0x18, 0x52, 0x61, 0x77, 0x4c, 0x61, 0x74, - 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x53, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x22, 0x43, 0x0a, 0x19, 0x52, 0x61, 0x77, 0x4c, 0x61, 0x74, 0x65, 0x73, - 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x53, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, - 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x6e, 0x0a, 0x10, 0x52, 0x61, 0x77, - 0x53, 0x63, 0x61, 0x6e, 0x41, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, - 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x65, 0x6e, - 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x65, 0x6e, 0x64, - 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x73, 0x22, 0x33, 0x0a, 0x09, 0x52, 0x61, 0x77, - 0x4b, 0x56, 0x50, 0x61, 0x69, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2f, - 0x0a, 0x11, 0x52, 0x61, 0x77, 0x53, 0x63, 0x61, 0x6e, 0x41, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x02, 0x6b, 0x76, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x0a, 0x2e, 0x52, 0x61, 0x77, 0x4b, 0x56, 0x50, 0x61, 0x69, 0x72, 0x52, 0x02, 0x6b, 0x76, 0x22, - 0x34, 0x0a, 0x0a, 0x50, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x4a, 0x0a, 0x0b, 0x50, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, 0x69, - 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x22, 0x21, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4d, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, - 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x63, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x22, 0x1e, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x22, 0x47, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x69, 0x6e, - 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x41, - 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x42, 0x0a, 0x08, - 0x4b, 0x65, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x74, 0x72, 0x79, 0x61, 0x62, 0x6c, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x74, 0x72, 0x79, 0x61, 0x62, 0x6c, 0x65, - 0x22, 0x4d, 0x0a, 0x02, 0x4b, 0x76, 0x12, 0x1f, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x4b, 0x65, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, - 0x59, 0x0a, 0x0b, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, - 0x0a, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x65, - 0x6e, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x65, 0x6e, - 0x64, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x23, 0x0a, 0x0c, 0x53, 0x63, - 0x61, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x13, 0x0a, 0x02, 0x6b, 0x76, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x03, 0x2e, 0x4b, 0x76, 0x52, 0x02, 0x6b, 0x76, 0x22, - 0x6a, 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x21, 0x0a, 0x09, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x03, 0x2e, 0x4b, 0x76, 0x52, 0x09, 0x6d, 0x75, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, - 0x12, 0x19, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x74, 0x6c, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x07, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x74, 0x6c, 0x22, 0x36, 0x0a, 0x11, 0x50, - 0x72, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x21, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x09, 0x2e, 0x4b, 0x65, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x73, 0x22, 0x3e, 0x0a, 0x0d, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x73, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x73, 0x12, - 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x6b, - 0x65, 0x79, 0x73, 0x22, 0x70, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x5f, - 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x63, 0x6f, 0x6d, - 0x6d, 0x69, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, - 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x12, 0x21, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x4b, 0x65, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x06, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x2c, 0x0a, 0x0f, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, - 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x5f, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x54, 0x73, 0x22, 0x2c, 0x0a, 0x10, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x32, 0x9b, 0x02, 0x0a, 0x05, 0x52, 0x61, 0x77, 0x4b, 0x56, 0x12, 0x2b, 0x0a, 0x06, 0x52, - 0x61, 0x77, 0x50, 0x75, 0x74, 0x12, 0x0e, 0x2e, 0x52, 0x61, 0x77, 0x50, 0x75, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x52, 0x61, 0x77, 0x50, 0x75, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x06, 0x52, 0x61, 0x77, 0x47, - 0x65, 0x74, 0x12, 0x0e, 0x2e, 0x52, 0x61, 0x77, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x52, 0x61, 0x77, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x12, 0x11, 0x2e, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x52, 0x61, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4c, 0x0a, 0x11, 0x52, - 0x61, 0x77, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x53, - 0x12, 0x19, 0x2e, 0x52, 0x61, 0x77, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x54, 0x53, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x52, 0x61, - 0x77, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x54, 0x53, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x09, 0x52, 0x61, 0x77, - 0x53, 0x63, 0x61, 0x6e, 0x41, 0x74, 0x12, 0x11, 0x2e, 0x52, 0x61, 0x77, 0x53, 0x63, 0x61, 0x6e, - 0x41, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x52, 0x61, 0x77, 0x53, - 0x63, 0x61, 0x6e, 0x41, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x32, - 0xc1, 0x02, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, - 0x6c, 0x4b, 0x56, 0x12, 0x22, 0x0a, 0x03, 0x50, 0x75, 0x74, 0x12, 0x0b, 0x2e, 0x50, 0x75, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x50, 0x75, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x22, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x0b, - 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x47, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x06, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x0e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x25, 0x0a, 0x04, 0x53, 0x63, 0x61, 0x6e, - 0x12, 0x0c, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, - 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x32, 0x0a, 0x08, 0x50, 0x72, 0x65, 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x10, 0x2e, 0x50, 0x72, - 0x65, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, - 0x50, 0x72, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x12, 0x0e, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x31, 0x0a, 0x08, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x10, 0x2e, 0x52, - 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, - 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x6a, 0x70, 0x2f, 0x65, 0x6c, 0x61, 0x73, 0x74, 0x69, 0x63, - 0x6b, 0x76, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_service_proto_rawDesc = "" + + "\n" + + "\rservice.proto\"7\n" + + "\rRawPutRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value\"M\n" + + "\x0eRawPutResponse\x12!\n" + + "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\"1\n" + + "\rRawGetRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\x12\x0e\n" + + "\x02ts\x18\x03 \x01(\x04R\x02ts\"b\n" + + "\x0eRawGetResponse\x12\"\n" + + "\rread_at_index\x18\x01 \x01(\x04R\vreadAtIndex\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value\x12\x16\n" + + "\x06exists\x18\x03 \x01(\bR\x06exists\"$\n" + + "\x10RawDeleteRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\"P\n" + + "\x11RawDeleteResponse\x12!\n" + + "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\",\n" + + "\x18RawLatestCommitTSRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\"C\n" + + "\x19RawLatestCommitTSResponse\x12\x0e\n" + + "\x02ts\x18\x01 \x01(\x04R\x02ts\x12\x16\n" + + "\x06exists\x18\x02 \x01(\bR\x06exists\"n\n" + + "\x10RawScanAtRequest\x12\x1b\n" + + "\tstart_key\x18\x01 \x01(\fR\bstartKey\x12\x17\n" + + "\aend_key\x18\x02 \x01(\fR\x06endKey\x12\x14\n" + + "\x05limit\x18\x03 \x01(\x03R\x05limit\x12\x0e\n" + + "\x02ts\x18\x04 \x01(\x04R\x02ts\"3\n" + + "\tRawKVPair\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value\"/\n" + + "\x11RawScanAtResponse\x12\x1a\n" + + "\x02kv\x18\x01 \x03(\v2\n" + + ".RawKVPairR\x02kv\"4\n" + + "\n" + + "PutRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value\"J\n" + + "\vPutResponse\x12!\n" + + "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\"!\n" + + "\rDeleteRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\"M\n" + + "\x0eDeleteResponse\x12!\n" + + "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\"\x1e\n" + + "\n" + + "GetRequest\x12\x10\n" + + "\x03key\x18\x01 \x01(\fR\x03key\"G\n" + + "\vGetResponse\x12\"\n" + + "\rread_at_index\x18\x01 \x01(\x04R\vreadAtIndex\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value\"B\n" + + "\bKeyError\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12\x1c\n" + + "\tretryable\x18\x02 \x01(\bR\tretryable\"M\n" + + "\x02Kv\x12\x1f\n" + + "\x05error\x18\x01 \x01(\v2\t.KeyErrorR\x05error\x12\x10\n" + + "\x03key\x18\x02 \x01(\fR\x03key\x12\x14\n" + + "\x05value\x18\x03 \x01(\fR\x05value\"Y\n" + + "\vScanRequest\x12\x1b\n" + + "\tstart_key\x18\x01 \x01(\fR\bstartKey\x12\x17\n" + + "\aend_key\x18\x02 \x01(\fR\x06endKey\x12\x14\n" + + "\x05limit\x18\x03 \x01(\x04R\x05limit\"#\n" + + "\fScanResponse\x12\x13\n" + + "\x02kv\x18\x01 \x03(\v2\x03.KvR\x02kv\"j\n" + + "\x0fPreWriteRequest\x12!\n" + + "\tmutations\x18\x02 \x03(\v2\x03.KvR\tmutations\x12\x19\n" + + "\bstart_ts\x18\x03 \x01(\x04R\astartTs\x12\x19\n" + + "\block_ttl\x18\x04 \x01(\x04R\alockTtl\"6\n" + + "\x11PreCommitResponse\x12!\n" + + "\x06errors\x18\x01 \x03(\v2\t.KeyErrorR\x06errors\">\n" + + "\rCommitRequest\x12\x19\n" + + "\bstart_ts\x18\x01 \x01(\x04R\astartTs\x12\x12\n" + + "\x04keys\x18\x02 \x03(\fR\x04keys\"p\n" + + "\x0eCommitResponse\x12!\n" + + "\fcommit_index\x18\x01 \x01(\x04R\vcommitIndex\x12\x18\n" + + "\asuccess\x18\x02 \x01(\bR\asuccess\x12!\n" + + "\x06errors\x18\x03 \x03(\v2\t.KeyErrorR\x06errors\",\n" + + "\x0fRollbackRequest\x12\x19\n" + + "\bstart_ts\x18\x01 \x01(\x04R\astartTs\",\n" + + "\x10RollbackResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess2\x9b\x02\n" + + "\x05RawKV\x12+\n" + + "\x06RawPut\x12\x0e.RawPutRequest\x1a\x0f.RawPutResponse\"\x00\x12+\n" + + "\x06RawGet\x12\x0e.RawGetRequest\x1a\x0f.RawGetResponse\"\x00\x124\n" + + "\tRawDelete\x12\x11.RawDeleteRequest\x1a\x12.RawDeleteResponse\"\x00\x12L\n" + + "\x11RawLatestCommitTS\x12\x19.RawLatestCommitTSRequest\x1a\x1a.RawLatestCommitTSResponse\"\x00\x124\n" + + "\tRawScanAt\x12\x11.RawScanAtRequest\x1a\x12.RawScanAtResponse\"\x002\xc1\x02\n" + + "\x0fTransactionalKV\x12\"\n" + + "\x03Put\x12\v.PutRequest\x1a\f.PutResponse\"\x00\x12\"\n" + + "\x03Get\x12\v.GetRequest\x1a\f.GetResponse\"\x00\x12+\n" + + "\x06Delete\x12\x0e.DeleteRequest\x1a\x0f.DeleteResponse\"\x00\x12%\n" + + "\x04Scan\x12\f.ScanRequest\x1a\r.ScanResponse\"\x00\x122\n" + + "\bPreWrite\x12\x10.PreWriteRequest\x1a\x12.PreCommitResponse\"\x00\x12+\n" + + "\x06Commit\x12\x0e.CommitRequest\x1a\x0f.CommitResponse\"\x00\x121\n" + + "\bRollback\x12\x10.RollbackRequest\x1a\x11.RollbackResponse\"\x00B#Z!github.com/bootjp/elastickv/protob\x06proto3" var ( file_service_proto_rawDescOnce sync.Once - file_service_proto_rawDescData = file_service_proto_rawDesc + file_service_proto_rawDescData []byte ) func file_service_proto_rawDescGZIP() []byte { file_service_proto_rawDescOnce.Do(func() { - file_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_service_proto_rawDescData) + file_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_service_proto_rawDesc), len(file_service_proto_rawDesc))) }) return file_service_proto_rawDescData } @@ -1737,337 +1600,11 @@ func file_service_proto_init() { if File_service_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_service_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*RawPutRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*RawPutResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[2].Exporter = func(v any, i int) any { - switch v := v.(*RawGetRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*RawGetResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*RawDeleteRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[5].Exporter = func(v any, i int) any { - switch v := v.(*RawDeleteResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[6].Exporter = func(v any, i int) any { - switch v := v.(*RawLatestCommitTSRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[7].Exporter = func(v any, i int) any { - switch v := v.(*RawLatestCommitTSResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[8].Exporter = func(v any, i int) any { - switch v := v.(*RawScanAtRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[9].Exporter = func(v any, i int) any { - switch v := v.(*RawKVPair); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[10].Exporter = func(v any, i int) any { - switch v := v.(*RawScanAtResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[11].Exporter = func(v any, i int) any { - switch v := v.(*PutRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[12].Exporter = func(v any, i int) any { - switch v := v.(*PutResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[13].Exporter = func(v any, i int) any { - switch v := v.(*DeleteRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[14].Exporter = func(v any, i int) any { - switch v := v.(*DeleteResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[15].Exporter = func(v any, i int) any { - switch v := v.(*GetRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[16].Exporter = func(v any, i int) any { - switch v := v.(*GetResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[17].Exporter = func(v any, i int) any { - switch v := v.(*KeyError); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[18].Exporter = func(v any, i int) any { - switch v := v.(*Kv); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[19].Exporter = func(v any, i int) any { - switch v := v.(*ScanRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[20].Exporter = func(v any, i int) any { - switch v := v.(*ScanResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[21].Exporter = func(v any, i int) any { - switch v := v.(*PreWriteRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[22].Exporter = func(v any, i int) any { - switch v := v.(*PreCommitResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[23].Exporter = func(v any, i int) any { - switch v := v.(*CommitRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[24].Exporter = func(v any, i int) any { - switch v := v.(*CommitResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[25].Exporter = func(v any, i int) any { - switch v := v.(*RollbackRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_service_proto_msgTypes[26].Exporter = func(v any, i int) any { - switch v := v.(*RollbackResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_service_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_service_proto_rawDesc), len(file_service_proto_rawDesc)), NumEnums: 0, NumMessages: 27, NumExtensions: 0, @@ -2078,7 +1615,6 @@ func file_service_proto_init() { MessageInfos: file_service_proto_msgTypes, }.Build() File_service_proto = out.File - file_service_proto_rawDesc = nil file_service_proto_goTypes = nil file_service_proto_depIdxs = nil } diff --git a/proto/service_grpc.pb.go b/proto/service_grpc.pb.go index 2aed4711..445aa5ec 100644 --- a/proto/service_grpc.pb.go +++ b/proto/service_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.4.0 +// - protoc-gen-go-grpc v1.6.1 // - protoc v5.29.3 // source: service.proto @@ -15,8 +15,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.62.0 or later. -const _ = grpc.SupportPackageIsVersion8 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( RawKV_RawPut_FullMethodName = "/RawKV/RawPut" @@ -97,7 +97,7 @@ func (c *rawKVClient) RawScanAt(ctx context.Context, in *RawScanAtRequest, opts // RawKVServer is the server API for RawKV service. // All implementations must embed UnimplementedRawKVServer -// for forward compatibility +// for forward compatibility. type RawKVServer interface { RawPut(context.Context, *RawPutRequest) (*RawPutResponse, error) RawGet(context.Context, *RawGetRequest) (*RawGetResponse, error) @@ -107,26 +107,30 @@ type RawKVServer interface { mustEmbedUnimplementedRawKVServer() } -// UnimplementedRawKVServer must be embedded to have forward compatible implementations. -type UnimplementedRawKVServer struct { -} +// UnimplementedRawKVServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRawKVServer struct{} func (UnimplementedRawKVServer) RawPut(context.Context, *RawPutRequest) (*RawPutResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RawPut not implemented") + return nil, status.Error(codes.Unimplemented, "method RawPut not implemented") } func (UnimplementedRawKVServer) RawGet(context.Context, *RawGetRequest) (*RawGetResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RawGet not implemented") + return nil, status.Error(codes.Unimplemented, "method RawGet not implemented") } func (UnimplementedRawKVServer) RawDelete(context.Context, *RawDeleteRequest) (*RawDeleteResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RawDelete not implemented") + return nil, status.Error(codes.Unimplemented, "method RawDelete not implemented") } func (UnimplementedRawKVServer) RawLatestCommitTS(context.Context, *RawLatestCommitTSRequest) (*RawLatestCommitTSResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RawLatestCommitTS not implemented") + return nil, status.Error(codes.Unimplemented, "method RawLatestCommitTS not implemented") } func (UnimplementedRawKVServer) RawScanAt(context.Context, *RawScanAtRequest) (*RawScanAtResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method RawScanAt not implemented") + return nil, status.Error(codes.Unimplemented, "method RawScanAt not implemented") } func (UnimplementedRawKVServer) mustEmbedUnimplementedRawKVServer() {} +func (UnimplementedRawKVServer) testEmbeddedByValue() {} // UnsafeRawKVServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to RawKVServer will @@ -136,6 +140,13 @@ type UnsafeRawKVServer interface { } func RegisterRawKVServer(s grpc.ServiceRegistrar, srv RawKVServer) { + // If the following call panics, it indicates UnimplementedRawKVServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&RawKV_ServiceDesc, srv) } @@ -364,7 +375,7 @@ func (c *transactionalKVClient) Rollback(ctx context.Context, in *RollbackReques // TransactionalKVServer is the server API for TransactionalKV service. // All implementations must embed UnimplementedTransactionalKVServer -// for forward compatibility +// for forward compatibility. type TransactionalKVServer interface { Put(context.Context, *PutRequest) (*PutResponse, error) Get(context.Context, *GetRequest) (*GetResponse, error) @@ -376,32 +387,36 @@ type TransactionalKVServer interface { mustEmbedUnimplementedTransactionalKVServer() } -// UnimplementedTransactionalKVServer must be embedded to have forward compatible implementations. -type UnimplementedTransactionalKVServer struct { -} +// UnimplementedTransactionalKVServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTransactionalKVServer struct{} func (UnimplementedTransactionalKVServer) Put(context.Context, *PutRequest) (*PutResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Put not implemented") + return nil, status.Error(codes.Unimplemented, "method Put not implemented") } func (UnimplementedTransactionalKVServer) Get(context.Context, *GetRequest) (*GetResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") + return nil, status.Error(codes.Unimplemented, "method Get not implemented") } func (UnimplementedTransactionalKVServer) Delete(context.Context, *DeleteRequest) (*DeleteResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") + return nil, status.Error(codes.Unimplemented, "method Delete not implemented") } func (UnimplementedTransactionalKVServer) Scan(context.Context, *ScanRequest) (*ScanResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Scan not implemented") + return nil, status.Error(codes.Unimplemented, "method Scan not implemented") } func (UnimplementedTransactionalKVServer) PreWrite(context.Context, *PreWriteRequest) (*PreCommitResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method PreWrite not implemented") + return nil, status.Error(codes.Unimplemented, "method PreWrite not implemented") } func (UnimplementedTransactionalKVServer) Commit(context.Context, *CommitRequest) (*CommitResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Commit not implemented") + return nil, status.Error(codes.Unimplemented, "method Commit not implemented") } func (UnimplementedTransactionalKVServer) Rollback(context.Context, *RollbackRequest) (*RollbackResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Rollback not implemented") + return nil, status.Error(codes.Unimplemented, "method Rollback not implemented") } func (UnimplementedTransactionalKVServer) mustEmbedUnimplementedTransactionalKVServer() {} +func (UnimplementedTransactionalKVServer) testEmbeddedByValue() {} // UnsafeTransactionalKVServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to TransactionalKVServer will @@ -411,6 +426,13 @@ type UnsafeTransactionalKVServer interface { } func RegisterTransactionalKVServer(s grpc.ServiceRegistrar, srv TransactionalKVServer) { + // If the following call panics, it indicates UnimplementedTransactionalKVServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&TransactionalKV_ServiceDesc, srv) } From b2bb811c0da495455e7ef6fe7c483cb46286cf32 Mon Sep 17 00:00:00 2001 From: "Yoshiaki Ueda (bootjp)" Date: Tue, 17 Feb 2026 06:11:14 +0900 Subject: [PATCH 13/20] test: add txn primary-key and timestamp stamping coverage --- adapter/internal_test.go | 60 ++++++++++++++++++++++++++++++ kv/coordinator_txn_test.go | 16 ++++++++ kv/sharded_coordinator_txn_test.go | 25 +++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 kv/sharded_coordinator_txn_test.go diff --git a/adapter/internal_test.go b/adapter/internal_test.go index 8fc75be2..615e95c0 100644 --- a/adapter/internal_test.go +++ b/adapter/internal_test.go @@ -78,3 +78,63 @@ func TestFillForwardedTxnCommitTS_AssignsCommitTS(t *testing.T) { require.NoError(t, err) require.Equal(t, startTS+1, meta.CommitTS) } + +func TestFillForwardedTxnCommitTS_PreservesExistingCommitTS(t *testing.T) { + t.Parallel() + + i := &Internal{} + reqs := []*pb.Request{ + { + IsTxn: true, + Phase: pb.Phase_COMMIT, + Mutations: []*pb.Mutation{ + { + Op: pb.Op_PUT, + Key: []byte(kv.TxnMetaPrefix), + Value: kv.EncodeTxnMeta(kv.TxnMeta{PrimaryKey: []byte("k"), CommitTS: 42}), + }, + }, + }, + } + + require.NoError(t, i.fillForwardedTxnCommitTS(reqs, 10)) + meta, err := kv.DecodeTxnMeta(reqs[0].Mutations[0].Value) + require.NoError(t, err) + require.Equal(t, uint64(42), meta.CommitTS) +} + +func TestStampTxnTimestamps_UsesSingleTxnStartTS(t *testing.T) { + t.Parallel() + + i := &Internal{} + prepare := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_PREPARE, + Ts: 0, + Mutations: []*pb.Mutation{ + {Op: pb.Op_PUT, Key: []byte("k"), Value: []byte("v")}, + }, + } + commit := &pb.Request{ + IsTxn: true, + Phase: pb.Phase_COMMIT, + Ts: 9, + Mutations: []*pb.Mutation{ + { + Op: pb.Op_PUT, + Key: []byte(kv.TxnMetaPrefix), + Value: kv.EncodeTxnMeta(kv.TxnMeta{PrimaryKey: []byte("k"), CommitTS: 0}), + }, + {Op: pb.Op_PUT, Key: []byte("k")}, + }, + } + reqs := []*pb.Request{prepare, commit} + + require.NoError(t, i.stampTxnTimestamps(reqs)) + require.Equal(t, uint64(9), prepare.Ts) + require.Equal(t, uint64(9), commit.Ts) + + meta, err := kv.DecodeTxnMeta(commit.Mutations[0].Value) + require.NoError(t, err) + require.Greater(t, meta.CommitTS, uint64(9)) +} diff --git a/kv/coordinator_txn_test.go b/kv/coordinator_txn_test.go index 21e9d9fb..64f87bad 100644 --- a/kv/coordinator_txn_test.go +++ b/kv/coordinator_txn_test.go @@ -38,3 +38,19 @@ func TestCoordinateDispatchTxn_RejectsNonMonotonicCommitTS(t *testing.T) { require.ErrorIs(t, err, ErrTxnCommitTSRequired) require.Equal(t, 0, tx.commits) } + +func TestCoordinateDispatchTxn_RejectsMissingPrimaryKey(t *testing.T) { + t.Parallel() + + tx := &stubTransactional{} + c := &Coordinate{ + transactionManager: tx, + clock: NewHLC(), + } + + _, err := c.dispatchTxn([]*Elem[OP]{ + {Op: Put, Key: nil, Value: []byte("v")}, + }, 1) + require.ErrorIs(t, err, ErrTxnPrimaryKeyRequired) + require.Equal(t, 0, tx.commits) +} diff --git a/kv/sharded_coordinator_txn_test.go b/kv/sharded_coordinator_txn_test.go new file mode 100644 index 00000000..966356e5 --- /dev/null +++ b/kv/sharded_coordinator_txn_test.go @@ -0,0 +1,25 @@ +package kv + +import ( + "context" + "testing" + + "github.com/bootjp/elastickv/distribution" + "github.com/stretchr/testify/require" +) + +func TestShardedCoordinatorDispatchTxn_RejectsMissingPrimaryKey(t *testing.T) { + t.Parallel() + + engine := distribution.NewEngine() + engine.UpdateRoute([]byte(""), nil, 1) + + coord := NewShardedCoordinator(engine, map[uint64]*ShardGroup{}, 0, NewHLC(), nil) + _, err := coord.Dispatch(context.Background(), &OperationGroup[OP]{ + IsTxn: true, + Elems: []*Elem[OP]{ + {Op: Put, Key: nil, Value: []byte("v")}, + }, + }) + require.ErrorIs(t, err, ErrTxnPrimaryKeyRequired) +} From d44348e7810566f489664ed5edbaf393930ad37f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:21:42 +0000 Subject: [PATCH 14/20] Initial plan From 8eb0f7a2f7508bf4d4a00da946e09e43e0d3ad17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:25:46 +0000 Subject: [PATCH 15/20] Fix timestamp overflow in lock cleanup operations Replace cleanupTS with abortTSFrom to handle overflow when startTS==^uint64(0). Add explicit overflow checks to prevent invalid abort timestamps that violate the FSM requirement that resolveTS > startTS, which could leave locks permanently unresolved. Co-authored-by: bootjp <1306365+bootjp@users.noreply.github.com> --- kv/shard_store.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/kv/shard_store.go b/kv/shard_store.go index 79e93e33..39e45abc 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -365,7 +365,12 @@ func (s *ShardStore) resolveTxnLockForKey(ctx context.Context, g *ShardGroup, ke case txnStatusCommitted: return applyTxnResolution(g, pb.Phase_COMMIT, lock.StartTS, commitTS, lock.PrimaryKey, [][]byte{key}) case txnStatusRolledBack: - return applyTxnResolution(g, pb.Phase_ABORT, lock.StartTS, cleanupTS(lock.StartTS), lock.PrimaryKey, [][]byte{key}) + abortTS := abortTSFrom(lock.StartTS, lock.StartTS) + if abortTS <= lock.StartTS { + // Overflow: can't choose an abort timestamp strictly greater than startTS. + return errors.Wrapf(ErrTxnLocked, "key: %s (timestamp overflow)", string(key)) + } + return applyTxnResolution(g, pb.Phase_ABORT, lock.StartTS, abortTS, lock.PrimaryKey, [][]byte{key}) case txnStatusPending: return errors.Wrapf(ErrTxnLocked, "key: %s", string(key)) default: @@ -528,7 +533,12 @@ func lockResolutionForStatus(state lockTxnStatus, lock txnLock, key []byte) (pb. case txnStatusCommitted: return pb.Phase_COMMIT, state.commitTS, nil case txnStatusRolledBack: - return pb.Phase_ABORT, cleanupTS(lock.StartTS), nil + abortTS := abortTSFrom(lock.StartTS, lock.StartTS) + if abortTS <= lock.StartTS { + // Overflow: can't choose an abort timestamp strictly greater than startTS. + return pb.Phase_NONE, 0, errors.Wrapf(ErrTxnLocked, "key: %s (timestamp overflow)", string(key)) + } + return pb.Phase_ABORT, abortTS, nil default: return pb.Phase_NONE, 0, errors.Wrapf(ErrTxnInvalidMeta, "unknown txn status for key %s", string(key)) } @@ -835,7 +845,12 @@ func (s *ShardStore) tryAbortExpiredPrimary(primaryKey []byte, startTS uint64) ( if !ok || pg == nil || pg.Txn == nil { return false, nil } - if err := applyTxnResolution(pg, pb.Phase_ABORT, startTS, cleanupTS(startTS), primaryKey, [][]byte{primaryKey}); err != nil { + abortTS := abortTSFrom(startTS, startTS) + if abortTS <= startTS { + // Overflow: can't choose an abort timestamp strictly greater than startTS. + return false, nil + } + if err := applyTxnResolution(pg, pb.Phase_ABORT, startTS, abortTS, primaryKey, [][]byte{primaryKey}); err != nil { return false, err } return true, nil @@ -859,14 +874,7 @@ func applyTxnResolution(g *ShardGroup, phase pb.Phase, startTS, commitTS uint64, return errors.WithStack(err) } -func cleanupTS(startTS uint64) uint64 { - now := hlcWallNow() - next := startTS + 1 - if now > next { - return now - } - return next -} + // ApplyMutations applies a batch of mutations to the correct shard store. // From 9b2ef8bd32d99561d976b3a134c8d68885cfd711 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:27:20 +0000 Subject: [PATCH 16/20] Use commitTS parameter in abortTSFrom calls where available Pass commitTS from primaryTxnStatus to abortTSFrom for semantic clarity and consistency with usage elsewhere in the codebase. Co-authored-by: bootjp <1306365+bootjp@users.noreply.github.com> --- kv/shard_store.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kv/shard_store.go b/kv/shard_store.go index 39e45abc..88690d83 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -365,7 +365,7 @@ func (s *ShardStore) resolveTxnLockForKey(ctx context.Context, g *ShardGroup, ke case txnStatusCommitted: return applyTxnResolution(g, pb.Phase_COMMIT, lock.StartTS, commitTS, lock.PrimaryKey, [][]byte{key}) case txnStatusRolledBack: - abortTS := abortTSFrom(lock.StartTS, lock.StartTS) + abortTS := abortTSFrom(lock.StartTS, commitTS) if abortTS <= lock.StartTS { // Overflow: can't choose an abort timestamp strictly greater than startTS. return errors.Wrapf(ErrTxnLocked, "key: %s (timestamp overflow)", string(key)) @@ -533,7 +533,7 @@ func lockResolutionForStatus(state lockTxnStatus, lock txnLock, key []byte) (pb. case txnStatusCommitted: return pb.Phase_COMMIT, state.commitTS, nil case txnStatusRolledBack: - abortTS := abortTSFrom(lock.StartTS, lock.StartTS) + abortTS := abortTSFrom(lock.StartTS, state.commitTS) if abortTS <= lock.StartTS { // Overflow: can't choose an abort timestamp strictly greater than startTS. return pb.Phase_NONE, 0, errors.Wrapf(ErrTxnLocked, "key: %s (timestamp overflow)", string(key)) From 65ede5cb70054d3fa824758b8a6a4e604ec917ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:28:31 +0000 Subject: [PATCH 17/20] Add comment clarifying tryAbortExpiredPrimary timestamp handling Explain why startTS is passed twice to abortTSFrom: no commitTS is available when aborting an expired lock with no commit record. Co-authored-by: bootjp <1306365+bootjp@users.noreply.github.com> --- kv/shard_store.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kv/shard_store.go b/kv/shard_store.go index 88690d83..229268e7 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -845,6 +845,8 @@ func (s *ShardStore) tryAbortExpiredPrimary(primaryKey []byte, startTS uint64) ( if !ok || pg == nil || pg.Txn == nil { return false, nil } + // No commitTS available here; we're aborting an expired lock with no commit record. + // abortTSFrom will use startTS+1 if representable. abortTS := abortTSFrom(startTS, startTS) if abortTS <= startTS { // Overflow: can't choose an abort timestamp strictly greater than startTS. From 5099174a89162094a3ae6332a06a96f88b93201a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:29:26 +0000 Subject: [PATCH 18/20] Pass 0 for commitTS in tryAbortExpiredPrimary for clarity Use 0 instead of startTS as the second parameter to abortTSFrom to explicitly indicate that no commitTS is available in this context. Co-authored-by: bootjp <1306365+bootjp@users.noreply.github.com> --- kv/shard_store.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kv/shard_store.go b/kv/shard_store.go index 229268e7..a68beb19 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -846,8 +846,9 @@ func (s *ShardStore) tryAbortExpiredPrimary(primaryKey []byte, startTS uint64) ( return false, nil } // No commitTS available here; we're aborting an expired lock with no commit record. - // abortTSFrom will use startTS+1 if representable. - abortTS := abortTSFrom(startTS, startTS) + // Pass 0 for commitTS to explicitly indicate it's not available; abortTSFrom will + // use startTS+1 if representable. + abortTS := abortTSFrom(startTS, 0) if abortTS <= startTS { // Overflow: can't choose an abort timestamp strictly greater than startTS. return false, nil From 3ff3e3b0a29ec3b495a966b023c70f753c5e405b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:37:56 +0000 Subject: [PATCH 19/20] Fix gci linter error - remove extra blank lines Remove extra blank lines left after deleting cleanupTS function to fix golangci-lint gci formatting check. Co-authored-by: bootjp <1306365+bootjp@users.noreply.github.com> --- kv/shard_store.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/kv/shard_store.go b/kv/shard_store.go index a68beb19..f67a953c 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -877,8 +877,6 @@ func applyTxnResolution(g *ShardGroup, phase pb.Phase, startTS, commitTS uint64, return errors.WithStack(err) } - - // ApplyMutations applies a batch of mutations to the correct shard store. // // All mutations must belong to the same shard. Cross-shard mutation batches are From a3477b605c84e7774e5c681513b3bb5f46eca23a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 21:41:32 +0000 Subject: [PATCH 20/20] Add defensive programming comments for overflow checks Clarify that while uint64 overflow is not expected in normal operation, the overflow checks are defensive programming to handle the edge case where startTS==^uint64(0) or a bug causes overflow, preventing violation of the FSM invariant resolveTS > startTS. Co-authored-by: bootjp <1306365+bootjp@users.noreply.github.com> --- kv/shard_store.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kv/shard_store.go b/kv/shard_store.go index f67a953c..2b53ee92 100644 --- a/kv/shard_store.go +++ b/kv/shard_store.go @@ -367,7 +367,9 @@ func (s *ShardStore) resolveTxnLockForKey(ctx context.Context, g *ShardGroup, ke case txnStatusRolledBack: abortTS := abortTSFrom(lock.StartTS, commitTS) if abortTS <= lock.StartTS { - // Overflow: can't choose an abort timestamp strictly greater than startTS. + // Defensive check: While uint64 overflow is not expected in normal operation, + // this handles the edge case where startTS==^uint64(0) or a bug causes overflow. + // Prevents violating the FSM invariant resolveTS > startTS (fsm.go:258). return errors.Wrapf(ErrTxnLocked, "key: %s (timestamp overflow)", string(key)) } return applyTxnResolution(g, pb.Phase_ABORT, lock.StartTS, abortTS, lock.PrimaryKey, [][]byte{key}) @@ -535,7 +537,9 @@ func lockResolutionForStatus(state lockTxnStatus, lock txnLock, key []byte) (pb. case txnStatusRolledBack: abortTS := abortTSFrom(lock.StartTS, state.commitTS) if abortTS <= lock.StartTS { - // Overflow: can't choose an abort timestamp strictly greater than startTS. + // Defensive check: While uint64 overflow is not expected in normal operation, + // this handles the edge case where startTS==^uint64(0) or a bug causes overflow. + // Prevents violating the FSM invariant resolveTS > startTS (fsm.go:258). return pb.Phase_NONE, 0, errors.Wrapf(ErrTxnLocked, "key: %s (timestamp overflow)", string(key)) } return pb.Phase_ABORT, abortTS, nil @@ -850,7 +854,9 @@ func (s *ShardStore) tryAbortExpiredPrimary(primaryKey []byte, startTS uint64) ( // use startTS+1 if representable. abortTS := abortTSFrom(startTS, 0) if abortTS <= startTS { - // Overflow: can't choose an abort timestamp strictly greater than startTS. + // Defensive check: While uint64 overflow is not expected in normal operation, + // this handles the edge case where startTS==^uint64(0) or a bug causes overflow. + // Prevents violating the FSM invariant resolveTS > startTS (fsm.go:258). return false, nil } if err := applyTxnResolution(pg, pb.Phase_ABORT, startTS, abortTS, primaryKey, [][]byte{primaryKey}); err != nil {