@@ -118,6 +118,89 @@ func TestCommitIsIdempotentAfterCommitRecordExists(t *testing.T) {
118118 require .Equal (t , commitTS , gotCommitTS )
119119}
120120
121+ func TestCommitIsIdempotentOnSecondaryShardWhenKeyAlreadyCommitted (t * testing.T ) {
122+ t .Parallel ()
123+
124+ ctx := context .Background ()
125+ st := store .NewMVCCStore ()
126+ fsm , ok := NewKvFSM (st ).(* kvFSM )
127+ require .True (t , ok )
128+
129+ startTS := uint64 (15 )
130+ commitTS := uint64 (25 )
131+ primaryKey := []byte ("p" ) // lives on another shard; no txnCommitKey here
132+ userKey := []byte ("k" )
133+
134+ // PREPARE: write lock and intent for userKey.
135+ prepare := & pb.Request {
136+ IsTxn : true ,
137+ Phase : pb .Phase_PREPARE ,
138+ Ts : startTS ,
139+ Mutations : []* pb.Mutation {
140+ {Op : pb .Op_PUT , Key : []byte (txnMetaPrefix ), Value : EncodeTxnMeta (TxnMeta {PrimaryKey : primaryKey , LockTTLms : defaultTxnLockTTLms })},
141+ {Op : pb .Op_PUT , Key : userKey , Value : []byte ("v" )},
142+ },
143+ }
144+ require .NoError (t , applyFSMRequest (t , fsm , prepare ))
145+
146+ // Simulate a partial commit: the data key is written at commitTS but the
147+ // txn lock/intent are still present (inconsistent state that the
148+ // secondary-shard idempotency check must handle).
149+ require .NoError (t , st .PutAt (ctx , userKey , []byte ("v" ), commitTS , 0 ))
150+
151+ // COMMIT on this secondary shard (no txnCommitKey for primaryKey here).
152+ // Without the secondary-shard LatestCommitTS check this would fail with a
153+ // write-conflict error because userKey@commitTS > startTS.
154+ commit := & pb.Request {
155+ IsTxn : true ,
156+ Phase : pb .Phase_COMMIT ,
157+ Ts : startTS ,
158+ Mutations : []* pb.Mutation {
159+ {Op : pb .Op_PUT , Key : []byte (txnMetaPrefix ), Value : EncodeTxnMeta (TxnMeta {PrimaryKey : primaryKey , CommitTS : commitTS })},
160+ {Op : pb .Op_PUT , Key : userKey },
161+ },
162+ }
163+ require .NoError (t , applyFSMRequest (t , fsm , commit ))
164+
165+ // The committed value should still be readable.
166+ v , err := st .GetAt (ctx , userKey , ^ uint64 (0 ))
167+ require .NoError (t , err )
168+ require .Equal (t , []byte ("v" ), v )
169+
170+ // The lock and intent should be cleaned up.
171+ _ , err = st .GetAt (ctx , txnLockKey (userKey ), ^ uint64 (0 ))
172+ require .ErrorIs (t , err , store .ErrKeyNotFound )
173+ _ , err = st .GetAt (ctx , txnIntentKey (userKey ), ^ uint64 (0 ))
174+ require .ErrorIs (t , err , store .ErrKeyNotFound )
175+ }
176+
177+ func TestCommitPropagatesSecondaryShardLatestCommitTSError (t * testing.T ) {
178+ t .Parallel ()
179+
180+ underlying := store .NewMVCCStore ()
181+ userKey := []byte ("k" )
182+ errorStore := erroringLatestCommitStore {MVCCStore : underlying , key : userKey }
183+ fsm , ok := NewKvFSM (errorStore ).(* kvFSM )
184+ require .True (t , ok )
185+
186+ startTS := uint64 (16 )
187+ commitTS := uint64 (26 )
188+ primaryKey := []byte ("p" ) // no txnCommitKey in store → secondary-shard path
189+
190+ commit := & pb.Request {
191+ IsTxn : true ,
192+ Phase : pb .Phase_COMMIT ,
193+ Ts : startTS ,
194+ Mutations : []* pb.Mutation {
195+ {Op : pb .Op_PUT , Key : []byte (txnMetaPrefix ), Value : EncodeTxnMeta (TxnMeta {PrimaryKey : primaryKey , CommitTS : commitTS })},
196+ {Op : pb .Op_PUT , Key : userKey },
197+ },
198+ }
199+ err := applyFSMRequest (t , fsm , commit )
200+ require .Error (t , err )
201+ require .ErrorIs (t , err , ErrTestLatestCommitTS )
202+ }
203+
121204func TestCommitRejectsMissingPrimaryKey (t * testing.T ) {
122205 t .Parallel ()
123206
0 commit comments