@@ -11,10 +11,14 @@ import (
11
11
"testing"
12
12
13
13
"github.com/cockroachdb/cockroach/pkg/kv/kvpb"
14
+ "github.com/cockroachdb/cockroach/pkg/kv/kvserver/concurrency/isolation"
15
+ "github.com/cockroachdb/cockroach/pkg/kv/kvserver/concurrency/lock"
14
16
"github.com/cockroachdb/cockroach/pkg/roachpb"
15
17
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
16
18
"github.com/cockroachdb/cockroach/pkg/storage"
19
+ "github.com/cockroachdb/cockroach/pkg/util/hlc"
17
20
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
21
+ "github.com/cockroachdb/cockroach/pkg/util/log"
18
22
"github.com/stretchr/testify/require"
19
23
)
20
24
@@ -106,3 +110,251 @@ func TestGetResumeSpan(t *testing.T) {
106
110
})
107
111
}
108
112
}
113
+
114
+ // TestExpectExclusionSince tests evaluation of the ExpectExclusionSince option
115
+ // on all commands that support it.
116
+ func TestExpectExclusionSince (t * testing.T ) {
117
+ defer leaktest .AfterTest (t )()
118
+ defer log .Scope (t ).Close (t )
119
+
120
+ ctx := context .Background ()
121
+
122
+ var (
123
+ key = roachpb .Key ([]byte {'a' })
124
+ value = roachpb .MakeValueFromString ("woohoo" )
125
+
126
+ clock = hlc .NewClockForTesting (nil )
127
+ settings = cluster .MakeTestingClusterSettings ()
128
+ evalCtx = (& MockEvalCtx {Clock : clock , ClusterSettings : settings }).EvalContext ()
129
+ writeTS = clock .Now ()
130
+ )
131
+
132
+ putWithTxn := func (key roachpb.Key , value roachpb.Value , exclusionTS hlc.Timestamp , db storage.Engine , txn * roachpb.Transaction ) error {
133
+ putResp := kvpb.PutResponse {}
134
+ ts := writeTS
135
+ if txn != nil {
136
+ ts = txn .ReadTimestamp
137
+ }
138
+ _ , err := Put (ctx , db , CommandArgs {
139
+ EvalCtx : evalCtx ,
140
+ Header : kvpb.Header {
141
+ Txn : txn ,
142
+ Timestamp : ts ,
143
+ },
144
+ Args : & kvpb.PutRequest {
145
+ ExpectExclusionSince : exclusionTS ,
146
+ RequestHeader : kvpb.RequestHeader {Key : key },
147
+ Value : value ,
148
+ },
149
+ }, & putResp )
150
+ return err
151
+ }
152
+
153
+ getWithTxn := func (key roachpb.Key , exclusionTS hlc.Timestamp ,
154
+ str lock.Strength , dur lock.Durability , db storage.Engine , txn * roachpb.Transaction ) error {
155
+ getResp := kvpb.GetResponse {}
156
+ ts := writeTS
157
+ if txn != nil {
158
+ ts = txn .ReadTimestamp
159
+ }
160
+ _ , err := Get (ctx , db , CommandArgs {
161
+ EvalCtx : evalCtx ,
162
+ Header : kvpb.Header {
163
+ Txn : txn ,
164
+ Timestamp : ts ,
165
+ },
166
+ Args : & kvpb.GetRequest {
167
+ LockNonExisting : true ,
168
+ ExpectExclusionSince : exclusionTS ,
169
+ KeyLockingStrength : str ,
170
+ KeyLockingDurability : dur ,
171
+ RequestHeader : kvpb.RequestHeader {Key : key },
172
+ },
173
+ }, & getResp )
174
+ return err
175
+ }
176
+
177
+ // We'll setup each test in a new engine. Placing a write or lock on `key` at
178
+ // `writeTS`.
179
+ type existingWriteType string
180
+ const (
181
+ intentWrite existingWriteType = "intent"
182
+ committedWrite existingWriteType = "committed"
183
+ replicatedLock existingWriteType = "replicated-lock"
184
+ )
185
+
186
+ var (
187
+ beforeExistingTS = writeTS .Prev ()
188
+ equalExistingTS = writeTS
189
+ afterExistingTS = writeTS .Next ()
190
+ )
191
+
192
+ type testCase struct {
193
+ writeType existingWriteType
194
+ exclusionTS hlc.Timestamp
195
+ expectExclusionViolation bool
196
+ expectLockConflictError bool
197
+ }
198
+
199
+ setup := func (t * testing.T , writeType existingWriteType ) (storage.Engine , * roachpb.Transaction ) {
200
+ db := storage .NewDefaultInMemForTesting ()
201
+
202
+ var txn * roachpb.Transaction
203
+ if writeType == intentWrite || writeType == replicatedLock {
204
+ txn1 := roachpb .MakeTransaction ("test" , nil , /* baseKey */
205
+ isolation .Serializable ,
206
+ roachpb .NormalUserPriority , writeTS , 0 , 0 , 0 , false /* omitInRangefeeds */ )
207
+ txn = & txn1
208
+ }
209
+ if writeType == replicatedLock {
210
+ require .NoError (t , getWithTxn (key , hlc.Timestamp {}, lock .Exclusive , lock .Replicated , db , txn ))
211
+ } else {
212
+ require .NoError (t , putWithTxn (key , value , hlc.Timestamp {}, db , txn ))
213
+ }
214
+ return db , txn
215
+ }
216
+
217
+ // ops are the operations that support sending a ExpectExclusionSince
218
+ // timestamp.
219
+ ops := []struct {
220
+ name string
221
+ request func (key roachpb.Key , exclusionTS hlc.Timestamp , db storage.Engine , txn * roachpb.Transaction ) error
222
+ }{
223
+ {
224
+ name : "Get" ,
225
+ request : func (key roachpb.Key , exclusionTS hlc.Timestamp , db storage.Engine , txn * roachpb.Transaction ) error {
226
+ return getWithTxn (key , exclusionTS , lock .Exclusive , lock .Replicated , db , txn )
227
+ },
228
+ },
229
+ {
230
+ name : "Put" ,
231
+ request : func (key roachpb.Key , exclusionTS hlc.Timestamp , db storage.Engine , txn * roachpb.Transaction ) error {
232
+ return putWithTxn (key , value , exclusionTS , db , txn )
233
+ },
234
+ },
235
+ {
236
+ name : "Delete" ,
237
+ request : func (key roachpb.Key , exclusionTS hlc.Timestamp , db storage.Engine , txn * roachpb.Transaction ) error {
238
+ delResp := kvpb.DeleteResponse {}
239
+ _ , err := Delete (ctx , db , CommandArgs {
240
+ EvalCtx : evalCtx ,
241
+ Header : kvpb.Header {
242
+ Txn : txn ,
243
+ Timestamp : txn .ReadTimestamp ,
244
+ },
245
+ Args : & kvpb.DeleteRequest {
246
+ ExpectExclusionSince : exclusionTS ,
247
+ RequestHeader : kvpb.RequestHeader {Key : key },
248
+ },
249
+ }, & delResp )
250
+ return err
251
+ },
252
+ },
253
+ }
254
+
255
+ testCases := []testCase {
256
+ // If an intent write exists, it doesn't matter the timestamp it is at. We
257
+ // expect a lock conflict error and then wait on whoever violated our write
258
+ // exclusion.
259
+ {
260
+ writeType : intentWrite ,
261
+ exclusionTS : beforeExistingTS ,
262
+ expectLockConflictError : true ,
263
+ },
264
+ {
265
+ writeType : intentWrite ,
266
+ exclusionTS : equalExistingTS ,
267
+ expectLockConflictError : true ,
268
+ },
269
+ {
270
+ writeType : intentWrite ,
271
+ exclusionTS : afterExistingTS ,
272
+ expectLockConflictError : true ,
273
+ },
274
+
275
+ // Committed writes are where we expect to see a write exclusion violation
276
+ // error.
277
+ {
278
+ writeType : committedWrite ,
279
+ exclusionTS : beforeExistingTS ,
280
+ expectExclusionViolation : true ,
281
+ },
282
+ {
283
+ writeType : committedWrite ,
284
+ exclusionTS : equalExistingTS ,
285
+ expectExclusionViolation : true ,
286
+ },
287
+ {
288
+ writeType : committedWrite ,
289
+ exclusionTS : afterExistingTS ,
290
+ },
291
+
292
+ // For replicatedLocks, we get a lock conflict error in all case just like
293
+ // an intent write.
294
+ {
295
+ writeType : replicatedLock ,
296
+ exclusionTS : beforeExistingTS ,
297
+ expectLockConflictError : true ,
298
+ },
299
+ {
300
+ writeType : replicatedLock ,
301
+ exclusionTS : equalExistingTS ,
302
+ expectLockConflictError : true ,
303
+ },
304
+ {
305
+ writeType : replicatedLock ,
306
+ exclusionTS : afterExistingTS ,
307
+ expectLockConflictError : true ,
308
+ },
309
+ }
310
+
311
+ for _ , op := range ops {
312
+ for _ , tc := range testCases {
313
+ exclusionTSString := "unknown"
314
+ if tc .exclusionTS .Equal (equalExistingTS ) {
315
+ exclusionTSString = "equal"
316
+ } else if tc .exclusionTS .Equal (beforeExistingTS ) {
317
+ exclusionTSString = "before"
318
+ } else if tc .exclusionTS .Equal (afterExistingTS ) {
319
+ exclusionTSString = "after"
320
+ }
321
+
322
+ for _ , sameTxn := range []bool {true , false } {
323
+ // Committed writes don't have an associated transaction so the sameTxn
324
+ // variant doesn't make sense here.
325
+ if sameTxn && tc .writeType == committedWrite {
326
+ continue
327
+ }
328
+
329
+ name := fmt .Sprintf ("%s/%s/exclusion_ts=%s/sameTxn=%v" ,
330
+ op .name , tc .writeType , exclusionTSString , sameTxn )
331
+
332
+ t .Run (name , func (t * testing.T ) {
333
+ db , txn := setup (t , tc .writeType )
334
+ defer db .Close ()
335
+
336
+ if ! sameTxn {
337
+ txn1 := roachpb .MakeTransaction ("test" , nil , /* baseKey */
338
+ isolation .Serializable ,
339
+ roachpb .NormalUserPriority , clock .Now (), 0 , 0 , 0 , false /* omitInRangefeeds */ )
340
+ txn = & txn1
341
+ } else {
342
+ txn .Sequence ++
343
+ txn .BumpReadTimestamp (clock .Now ())
344
+ }
345
+
346
+ err := op .request (key , tc .exclusionTS , db , txn )
347
+ if sameTxn {
348
+ require .NoError (t , err , "expected no error for write in the same txn" )
349
+ } else if tc .expectExclusionViolation {
350
+ require .ErrorContains (t , err , "write exclusion on key" )
351
+ } else if tc .expectLockConflictError {
352
+ require .ErrorContains (t , err , "conflicting locks on" )
353
+ } else {
354
+ require .NoError (t , err )
355
+ }
356
+ })
357
+ }
358
+ }
359
+ }
360
+ }
0 commit comments