Skip to content

Commit 0a2a0aa

Browse files
committed
kv,sql: explicitly set IsReverse on the Header
When processing batches that touch multiple ranges, the DistSender needs to know whether to iterate across those ranges in the forward or reverse manner (i.e. ASC or DESC for range keys). Currently, it only uses the reverse direction if it finds at least one ReverseScan request in the batch, and it uses the forward direction otherwise. This goes against the needs of SQL which might issue point Gets when performing a SQL revscan operation, and currently in such a scenario we would hit an error (instead of returning the results in incorrect order). This commit fixes the issue by introducing `IsReverse` boolean on the batch header to explicitly indicate the direction for range iteration. It seems reasonable that the caller should be explicit about this, and we also add a validation that the boolean is set correctly (meaning that it should be `false` when only forward range requests are present and `true` when only reverse range requests are present). In order to simplify the story a bit, the DistSender will no longer allow batches that have both forward and reverse range requests. Previously, this limitation only applied to the batches with limits, but now it's extended to be unconditional. SQL never issues such batches, so the limitation seems acceptable. This limitation required some updates to the existing tests, including KVNemesis to not generate batches that are now disallowed. See also 619f395 for some related context. Given that the new header field is only examined on the KV client, the change can be backported with no mixed-version concerns. Release note (bug fix): Previously, CockroachDB could hit an error `ERROR: span with results after resume span...` when evaluating some queries with ORDER BY ... DESC in an edge case. The bug has been present since about v22.1 and is now fixed.
1 parent 2eb84bc commit 0a2a0aa

File tree

19 files changed

+165
-102
lines changed

19 files changed

+165
-102
lines changed

pkg/kv/batch.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ func (b *Batch) AddRawRequest(reqs ...kvpb.Request) {
365365
*kvpb.IncrementRequest,
366366
*kvpb.DeleteRequest:
367367
numRows = 1
368+
case *kvpb.ReverseScanRequest:
369+
b.Header.IsReverse = true
368370
}
369371
b.appendReqs(args)
370372
b.initResult(1 /* calls */, numRows, raw, nil)
@@ -735,6 +737,7 @@ func (b *Batch) scan(
735737
str kvpb.KeyLockingStrengthType,
736738
dur kvpb.KeyLockingDurabilityType,
737739
) {
740+
b.Header.IsReverse = isReverse
738741
begin, err := marshalKey(s)
739742
if err != nil {
740743
b.initResult(0, 0, notRaw, err)

pkg/kv/db.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ func (db *DB) scan(
499499
if maxRows > 0 {
500500
b.Header.MaxSpanRequestKeys = maxRows
501501
}
502+
b.Header.IsReverse = isReverse
502503
b.scan(begin, end, isReverse, str, dur)
503504
r, err := getOneResult(db.Run(ctx, b), b)
504505
return r.Rows, err

pkg/kv/kvclient/kvcoord/dist_sender.go

Lines changed: 55 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,35 +1079,46 @@ func (ds *DistSender) initAndVerifyBatch(ctx context.Context, ba *kvpb.BatchRequ
10791079
return kvpb.NewErrorf("empty batch")
10801080
}
10811081

1082-
if ba.MaxSpanRequestKeys != 0 || ba.TargetBytes != 0 {
1083-
// Verify that the batch contains only specific requests. Verify that a
1084-
// batch with a ReverseScan only contains ReverseScan range requests.
1085-
var foundForward, foundReverse bool
1086-
for _, req := range ba.Requests {
1087-
inner := req.GetInner()
1088-
switch inner.(type) {
1089-
case *kvpb.ScanRequest, *kvpb.ResolveIntentRangeRequest,
1090-
*kvpb.DeleteRangeRequest, *kvpb.RevertRangeRequest,
1091-
*kvpb.ExportRequest, *kvpb.QueryLocksRequest, *kvpb.IsSpanEmptyRequest:
1092-
// Accepted forward range requests.
1093-
foundForward = true
1082+
// Verify that forward and reverse range requests are never in the same
1083+
// batch. Also verify that the batch with limits contains only specific
1084+
// requests.
1085+
var foundForward, foundReverse bool
1086+
var disallowedReq string
1087+
for _, req := range ba.Requests {
1088+
inner := req.GetInner()
1089+
switch inner.(type) {
1090+
case *kvpb.ScanRequest, *kvpb.ResolveIntentRangeRequest,
1091+
*kvpb.DeleteRangeRequest, *kvpb.RevertRangeRequest,
1092+
*kvpb.ExportRequest, *kvpb.QueryLocksRequest, *kvpb.IsSpanEmptyRequest:
1093+
// Accepted forward range requests.
1094+
foundForward = true
10941095

1095-
case *kvpb.ReverseScanRequest:
1096-
// Accepted reverse range requests.
1097-
foundReverse = true
1096+
case *kvpb.ReverseScanRequest:
1097+
// Accepted reverse range requests.
1098+
foundReverse = true
10981099

1099-
case *kvpb.QueryIntentRequest, *kvpb.EndTxnRequest,
1100-
*kvpb.GetRequest, *kvpb.ResolveIntentRequest, *kvpb.DeleteRequest, *kvpb.PutRequest:
1101-
// Accepted point requests that can be in batches with limit.
1100+
case *kvpb.QueryIntentRequest, *kvpb.EndTxnRequest,
1101+
*kvpb.GetRequest, *kvpb.ResolveIntentRequest, *kvpb.DeleteRequest, *kvpb.PutRequest:
1102+
// Accepted point requests that can be in batches with limit. No
1103+
// need to set disallowedReq.
11021104

1103-
default:
1104-
return kvpb.NewErrorf("batch with limit contains %s request", inner.Method())
1105-
}
1106-
}
1107-
if foundForward && foundReverse {
1108-
return kvpb.NewErrorf("batch with limit contains both forward and reverse scans")
1105+
default:
1106+
disallowedReq = inner.Method().String()
11091107
}
11101108
}
1109+
if foundForward && foundReverse {
1110+
return kvpb.NewErrorf("batch contains both forward and reverse requests")
1111+
}
1112+
if (ba.MaxSpanRequestKeys != 0 || ba.TargetBytes != 0) && disallowedReq != "" {
1113+
return kvpb.NewErrorf("batch with limit contains %s request", disallowedReq)
1114+
}
1115+
// Also verify that IsReverse is set accordingly on the batch header.
1116+
if foundForward && ba.Header.IsReverse {
1117+
return kvpb.NewErrorf("batch contains forward requests but IsReverse is set")
1118+
}
1119+
if foundReverse && !ba.Header.IsReverse {
1120+
return kvpb.NewErrorf("batch contains reverse requests but IsReverse is not set")
1121+
}
11111122

11121123
switch ba.WaitPolicy {
11131124
case lock.WaitPolicy_Block, lock.WaitPolicy_Error:
@@ -1268,7 +1279,6 @@ func (ds *DistSender) Send(
12681279
if err != nil {
12691280
return nil, kvpb.NewError(err)
12701281
}
1271-
isReverse := ba.IsReverse()
12721282

12731283
// Determine whether this part of the BatchRequest contains a committing
12741284
// EndTxn request.
@@ -1282,9 +1292,9 @@ func (ds *DistSender) Send(
12821292
var rpl *kvpb.BatchResponse
12831293
var pErr *kvpb.Error
12841294
if withParallelCommit {
1285-
rpl, pErr = ds.divideAndSendParallelCommit(ctx, ba, rs, isReverse, 0 /* batchIdx */)
1295+
rpl, pErr = ds.divideAndSendParallelCommit(ctx, ba, rs, 0 /* batchIdx */)
12861296
} else {
1287-
rpl, pErr = ds.divideAndSendBatchToRanges(ctx, ba, rs, isReverse, withCommit, 0 /* batchIdx */)
1297+
rpl, pErr = ds.divideAndSendBatchToRanges(ctx, ba, rs, withCommit, 0 /* batchIdx */)
12881298
}
12891299

12901300
if pErr == errNo1PCTxn {
@@ -1475,7 +1485,7 @@ type response struct {
14751485
// method is never invoked recursively, but it is exposed to maintain symmetry
14761486
// with divideAndSendBatchToRanges.
14771487
func (ds *DistSender) divideAndSendParallelCommit(
1478-
ctx context.Context, ba *kvpb.BatchRequest, rs roachpb.RSpan, isReverse bool, batchIdx int,
1488+
ctx context.Context, ba *kvpb.BatchRequest, rs roachpb.RSpan, batchIdx int,
14791489
) (br *kvpb.BatchResponse, pErr *kvpb.Error) {
14801490
// Search backwards, looking for the first pre-commit QueryIntent.
14811491
swapIdx := -1
@@ -1491,7 +1501,7 @@ func (ds *DistSender) divideAndSendParallelCommit(
14911501
if swapIdx == -1 {
14921502
// No pre-commit QueryIntents. Nothing to split.
14931503
log.VEvent(ctx, 3, "no pre-commit QueryIntents found, sending batch as-is")
1494-
return ds.divideAndSendBatchToRanges(ctx, ba, rs, isReverse, true /* withCommit */, batchIdx)
1504+
return ds.divideAndSendBatchToRanges(ctx, ba, rs, true /* withCommit */, batchIdx)
14951505
}
14961506

14971507
// Swap the EndTxn request and the first pre-commit QueryIntent. This
@@ -1518,7 +1528,8 @@ func (ds *DistSender) divideAndSendParallelCommit(
15181528
if err != nil {
15191529
return br, kvpb.NewError(err)
15201530
}
1521-
qiIsReverse := false // QueryIntentRequests do not carry the isReverse flag
1531+
// No need to process QueryIntentRequests in the reverse order.
1532+
qiBa.IsReverse = false
15221533
qiBatchIdx := batchIdx + 1
15231534
qiResponseCh := make(chan response, 1)
15241535

@@ -1544,7 +1555,7 @@ func (ds *DistSender) divideAndSendParallelCommit(
15441555

15451556
// Send the batch with withCommit=true since it will be inflight
15461557
// concurrently with the EndTxn batch below.
1547-
reply, pErr := ds.divideAndSendBatchToRanges(ctx, qiBa, qiRS, qiIsReverse, true /* withCommit */, qiBatchIdx)
1558+
reply, pErr := ds.divideAndSendBatchToRanges(ctx, qiBa, qiRS, true /* withCommit */, qiBatchIdx)
15481559
qiResponseCh <- response{reply: reply, positions: positions, pErr: pErr}
15491560
}
15501561

@@ -1576,10 +1587,7 @@ func (ds *DistSender) divideAndSendParallelCommit(
15761587
if err != nil {
15771588
return nil, kvpb.NewError(err)
15781589
}
1579-
// Note that we don't need to recompute isReverse for the updated batch
1580-
// since we only separated out QueryIntentRequests which don't carry the
1581-
// isReverse flag.
1582-
br, pErr = ds.divideAndSendBatchToRanges(ctx, ba, rs, isReverse, true /* withCommit */, batchIdx)
1590+
br, pErr = ds.divideAndSendBatchToRanges(ctx, ba, rs, true /* withCommit */, batchIdx)
15831591

15841592
// Wait for the QueryIntent-only batch to complete and stitch
15851593
// the responses together.
@@ -1748,12 +1756,6 @@ func mergeErrors(pErr1, pErr2 *kvpb.Error) *kvpb.Error {
17481756
// is trimmed against each range which is part of the span and sent
17491757
// either serially or in parallel, if possible.
17501758
//
1751-
// isReverse indicates the direction that the provided span should be
1752-
// iterated over while sending requests. It is passed in by callers
1753-
// instead of being recomputed based on the requests in the batch to
1754-
// prevent the iteration direction from switching midway through a
1755-
// batch, in cases where partial batches recurse into this function.
1756-
//
17571759
// withCommit indicates that the batch contains a transaction commit
17581760
// or that a transaction commit is being run concurrently with this
17591761
// batch. Either way, if this is true then sendToReplicas will need
@@ -1763,12 +1765,7 @@ func mergeErrors(pErr1, pErr2 *kvpb.Error) *kvpb.Error {
17631765
// being processed by this method. It's specified as non-zero when
17641766
// this method is invoked recursively.
17651767
func (ds *DistSender) divideAndSendBatchToRanges(
1766-
ctx context.Context,
1767-
ba *kvpb.BatchRequest,
1768-
rs roachpb.RSpan,
1769-
isReverse bool,
1770-
withCommit bool,
1771-
batchIdx int,
1768+
ctx context.Context, ba *kvpb.BatchRequest, rs roachpb.RSpan, withCommit bool, batchIdx int,
17721769
) (br *kvpb.BatchResponse, pErr *kvpb.Error) {
17731770
// Clone the BatchRequest's transaction so that future mutations to the
17741771
// proto don't affect the proto in this batch.
@@ -1778,7 +1775,7 @@ func (ds *DistSender) divideAndSendBatchToRanges(
17781775
// Get initial seek key depending on direction of iteration.
17791776
var scanDir ScanDirection
17801777
var seekKey roachpb.RKey
1781-
if !isReverse {
1778+
if !ba.IsReverse {
17821779
scanDir = Ascending
17831780
seekKey = rs.Key
17841781
} else {
@@ -1793,7 +1790,7 @@ func (ds *DistSender) divideAndSendBatchToRanges(
17931790
// Take the fast path if this batch fits within a single range.
17941791
if !ri.NeedAnother(rs) {
17951792
resp := ds.sendPartialBatch(
1796-
ctx, ba, rs, isReverse, withCommit, batchIdx, ri.Token(),
1793+
ctx, ba, rs, withCommit, batchIdx, ri.Token(),
17971794
)
17981795
// resp.positions remains nil since the original batch is fully
17991796
// contained within a single range.
@@ -1915,7 +1912,7 @@ func (ds *DistSender) divideAndSendBatchToRanges(
19151912
}
19161913

19171914
if pErr == nil && couldHaveSkippedResponses {
1918-
fillSkippedResponses(ba, br, seekKey, resumeReason, isReverse)
1915+
fillSkippedResponses(ba, br, seekKey, resumeReason)
19191916
}
19201917
}()
19211918

@@ -1994,7 +1991,7 @@ func (ds *DistSender) divideAndSendBatchToRanges(
19941991
// If we can reserve one of the limited goroutines available for parallel
19951992
// batch RPCs, send asynchronously.
19961993
if canParallelize && !lastRange && !ds.disableParallelBatches {
1997-
if ds.sendPartialBatchAsync(ctx, curRangeBatch, curRangeRS, isReverse, withCommit, batchIdx, ri.Token(), responseCh, positions) {
1994+
if ds.sendPartialBatchAsync(ctx, curRangeBatch, curRangeRS, withCommit, batchIdx, ri.Token(), responseCh, positions) {
19981995
asyncSent = true
19991996
} else {
20001997
asyncThrottled = true
@@ -2009,7 +2006,7 @@ func (ds *DistSender) divideAndSendBatchToRanges(
20092006
}()
20102007
}
20112008
return ds.sendPartialBatch(
2012-
ctx, curRangeBatch, curRangeRS, isReverse, withCommit, batchIdx, ri.Token(),
2009+
ctx, curRangeBatch, curRangeRS, withCommit, batchIdx, ri.Token(),
20132010
)
20142011
}()
20152012
resp.positions = positions
@@ -2089,7 +2086,6 @@ func (ds *DistSender) sendPartialBatchAsync(
20892086
ctx context.Context,
20902087
ba *kvpb.BatchRequest,
20912088
rs roachpb.RSpan,
2092-
isReverse bool,
20932089
withCommit bool,
20942090
batchIdx int,
20952091
routing rangecache.EvictionToken,
@@ -2111,7 +2107,7 @@ func (ds *DistSender) sendPartialBatchAsync(
21112107
ds.metrics.AsyncSentCount.Inc(1)
21122108
ds.metrics.AsyncInProgress.Inc(1)
21132109
defer ds.metrics.AsyncInProgress.Dec(1)
2114-
resp := ds.sendPartialBatch(ctx, ba, rs, isReverse, withCommit, batchIdx, routing)
2110+
resp := ds.sendPartialBatch(ctx, ba, rs, withCommit, batchIdx, routing)
21152111
resp.positions = positions
21162112
responseCh <- resp
21172113
}(ctx)
@@ -2175,7 +2171,6 @@ func (ds *DistSender) sendPartialBatch(
21752171
ctx context.Context,
21762172
ba *kvpb.BatchRequest,
21772173
rs roachpb.RSpan,
2178-
isReverse bool,
21792174
withCommit bool,
21802175
batchIdx int,
21812176
routingTok rangecache.EvictionToken,
@@ -2203,7 +2198,7 @@ func (ds *DistSender) sendPartialBatch(
22032198

22042199
if !routingTok.Valid() {
22052200
var descKey roachpb.RKey
2206-
if isReverse {
2201+
if ba.IsReverse {
22072202
descKey = rs.EndKey
22082203
} else {
22092204
descKey = rs.Key
@@ -2219,7 +2214,7 @@ func (ds *DistSender) sendPartialBatch(
22192214
// replica, while detecting hazardous cases where the follower does
22202215
// not have the latest information and the current descriptor did
22212216
// not result in a successful send.
2222-
routingTok, err = ds.getRoutingInfo(ctx, descKey, prevTok, isReverse)
2217+
routingTok, err = ds.getRoutingInfo(ctx, descKey, prevTok, ba.IsReverse)
22232218
if err != nil {
22242219
log.VErrEventf(ctx, 1, "range descriptor re-lookup failed: %s", err)
22252220
// We set pErr if we encountered an error getting the descriptor in
@@ -2242,7 +2237,7 @@ func (ds *DistSender) sendPartialBatch(
22422237
}
22432238
if !intersection.Equal(rs) {
22442239
log.Eventf(ctx, "range shrunk; sub-dividing the request")
2245-
reply, pErr = ds.divideAndSendBatchToRanges(ctx, ba, rs, isReverse, withCommit, batchIdx)
2240+
reply, pErr = ds.divideAndSendBatchToRanges(ctx, ba, rs, withCommit, batchIdx)
22462241
return response{reply: reply, pErr: pErr}
22472242
}
22482243
}
@@ -2344,7 +2339,7 @@ func (ds *DistSender) sendPartialBatch(
23442339
// batch here would give a potentially larger response slice
23452340
// with unknown mapping to our truncated reply).
23462341
log.VEventf(ctx, 1, "likely split; will resend. Got new descriptors: %s", tErr.Ranges)
2347-
reply, pErr = ds.divideAndSendBatchToRanges(ctx, ba, rs, isReverse, withCommit, batchIdx)
2342+
reply, pErr = ds.divideAndSendBatchToRanges(ctx, ba, rs, withCommit, batchIdx)
23482343
return response{reply: reply, pErr: pErr}
23492344
}
23502345
break
@@ -2393,7 +2388,6 @@ func fillSkippedResponses(
23932388
br *kvpb.BatchResponse,
23942389
nextKey roachpb.RKey,
23952390
resumeReason kvpb.ResumeReason,
2396-
isReverse bool,
23972391
) {
23982392
// Some requests might have no response at all if we used a batch-wide
23992393
// limit; simply create trivial responses for those. Note that any type
@@ -2423,7 +2417,7 @@ func fillSkippedResponses(
24232417
for i, resp := range br.Responses {
24242418
req := ba.Requests[i].GetInner()
24252419
hdr := resp.GetInner().Header()
2426-
maybeSetResumeSpan(req, &hdr, nextKey, isReverse)
2420+
maybeSetResumeSpan(req, &hdr, nextKey, ba.IsReverse)
24272421
if hdr.ResumeSpan != nil {
24282422
hdr.ResumeReason = resumeReason
24292423
}

pkg/kv/kvclient/kvcoord/dist_sender_server_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -809,11 +809,10 @@ func TestMultiRequestBatchWithFwdAndReverseRequests(t *testing.T) {
809809
t.Fatal(err)
810810
}
811811
b := &kv.Batch{}
812-
b.Header.MaxSpanRequestKeys = 100
813812
b.Scan("a", "b")
814813
b.ReverseScan("a", "b")
815814
if err := db.Run(ctx, b); !testutils.IsError(
816-
err, "batch with limit contains both forward and reverse scans",
815+
err, "batch contains both forward and reverse requests",
817816
) {
818817
t.Fatal(err)
819818
}

pkg/kv/kvclient/kvstreamer/streamer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,7 @@ func (w *workerCoordinator) performRequestAsync(
13981398
ba.Header.TargetBytes = targetBytes
13991399
ba.Header.AllowEmpty = !headOfLine
14001400
ba.Header.WholeRowsOfSize = w.s.maxKeysPerRow
1401+
ba.Header.IsReverse = w.s.reverse
14011402
// TODO(yuzefovich): consider setting MaxSpanRequestKeys whenever
14021403
// applicable (#67885).
14031404
ba.AdmissionHeader = w.requestAdmissionHeader

pkg/kv/kvnemesis/applier_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,16 @@ func TestApplier(t *testing.T) {
252252
"txn-si-err", step(closureTxn(ClosureTxnType_Commit, isolation.Snapshot, delRange(k2, k4, 1))),
253253
},
254254
{
255-
"batch-mixed", step(batch(put(k2, 2), get(k1), del(k2, 1), del(k3, 1), scan(k1, k3), reverseScanForUpdate(k1, k5))),
255+
"batch-mixed-fwd", step(batch(put(k2, 2), get(k1), del(k2, 1), del(k3, 1), scan(k1, k3))),
256256
},
257257
{
258-
"batch-mixed-err", step(batch(put(k2, 2), getForUpdate(k1), scanForUpdate(k1, k3), reverseScan(k1, k3))),
258+
"batch-mixed-rev", step(batch(put(k2, 2), get(k1), del(k2, 1), del(k3, 1), reverseScanForUpdate(k1, k5))),
259+
},
260+
{
261+
"batch-mixed-err-fwd", step(batch(put(k2, 2), getForUpdate(k1), scanForUpdate(k1, k3))),
262+
},
263+
{
264+
"batch-mixed-err-rev", step(batch(put(k2, 2), getForUpdate(k1), reverseScan(k1, k3))),
259265
},
260266
{
261267
"txn-ssi-commit-mixed", step(closureTxn(ClosureTxnType_Commit, isolation.Serializable, put(k5, 5), batch(put(k6, 6), delRange(k3, k5, 1)))),

pkg/kv/kvnemesis/generator.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1517,8 +1517,28 @@ func makeRandBatch(c *ClientOperationConfig) opGenFunc {
15171517
g.registerClientOps(&allowed, c)
15181518
numOps := rng.Intn(4)
15191519
ops := make([]Operation, numOps)
1520-
for i := range ops {
1520+
var addedForwardScan, addedReverseScan bool
1521+
for i := 0; i < numOps; i++ {
15211522
ops[i] = g.selectOp(rng, allowed)
1523+
if ops[i].Scan != nil {
1524+
if !ops[i].Scan.Reverse {
1525+
if addedReverseScan {
1526+
// We cannot include the forward scan into the batch
1527+
// that already contains the reverse scan.
1528+
i--
1529+
continue
1530+
}
1531+
addedForwardScan = true
1532+
} else {
1533+
if addedForwardScan {
1534+
// We cannot include the reverse scan into the batch
1535+
// that already contains the forward scan.
1536+
i--
1537+
continue
1538+
}
1539+
addedReverseScan = true
1540+
}
1541+
}
15221542
}
15231543
return batch(ops...)
15241544
}

pkg/kv/kvnemesis/testdata/TestApplier/batch-mixed-err

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)