Skip to content

Commit 16d7a28

Browse files
committed
spanset: Add Overlaps() and PartiallyOverlaps helper functions
Analogous to how we have a spanset helper contains() that understands the special span representation: [x-eps,x). This commit adds a couple helpers to detect overlapping spans. Moreover, this commit adds tests to verify the behaviour of these helper functions.
1 parent 02b8bae commit 16d7a28

File tree

2 files changed

+218
-14
lines changed

2 files changed

+218
-14
lines changed

pkg/kv/kvserver/spanset/spanset.go

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ type Span struct {
7676
Timestamp hlc.Timestamp
7777
}
7878

79+
// TrickySpan represents a span that supports a special encoding where a nil
80+
// start key with a non-nil end key represents the point span:
81+
// [EndKey.Prev(), EndKey).
82+
type TrickySpan roachpb.Span
83+
7984
// SpanSet tracks the set of key spans touched by a command, broken into MVCC
8085
// and non-MVCC accesses. The set is divided into subsets for access type
8186
// (read-only or read/write) and key scope (local or global; used to facilitate
@@ -88,7 +93,7 @@ type SpanSet struct {
8893
// shouldn't be accessed (forbidden). This allows for complex pattern matching
8994
// like forbidding specific keys across all range IDs without enumerating them
9095
// explicitly.
91-
forbiddenSpansMatchers []func(roachpb.Span) error
96+
forbiddenSpansMatchers []func(TrickySpan) error
9297
allowUndeclared bool
9398
allowForbidden bool
9499
}
@@ -213,7 +218,7 @@ func (s *SpanSet) AddMVCC(access SpanAccess, span roachpb.Span, timestamp hlc.Ti
213218

214219
// AddForbiddenMatcher adds a forbidden span matcher. The matcher is a function
215220
// that is called for each span access to check if it should be forbidden.
216-
func (s *SpanSet) AddForbiddenMatcher(matcher func(roachpb.Span) error) {
221+
func (s *SpanSet) AddForbiddenMatcher(matcher func(TrickySpan) error) {
217222
s.forbiddenSpansMatchers = append(s.forbiddenSpansMatchers, matcher)
218223
}
219224

@@ -353,7 +358,7 @@ func (s *SpanSet) checkAllowed(
353358
if !s.allowForbidden {
354359
// Check if the span is forbidden.
355360
for _, matcher := range s.forbiddenSpansMatchers {
356-
if err := matcher(span); err != nil {
361+
if err := matcher(TrickySpan(span)); err != nil {
357362
return errors.Errorf("cannot %s span %s: matches forbidden pattern",
358363
access, span)
359364
}
@@ -370,7 +375,7 @@ func (s *SpanSet) checkAllowed(
370375

371376
for ac := access; ac < NumSpanAccess; ac++ {
372377
for _, cur := range s.spans[ac][scope] {
373-
if contains(cur.Span, span) && check(ac, cur) {
378+
if Contains(cur.Span, TrickySpan(span)) && check(ac, cur) {
374379
return nil
375380
}
376381
}
@@ -382,24 +387,63 @@ func (s *SpanSet) checkAllowed(
382387
return nil
383388
}
384389

385-
// contains returns whether s1 contains s2. Unlike Span.Contains, this function
386-
// supports spans with a nil start key and a non-nil end key (e.g. "[nil, c)").
387-
// In this form, s2.Key (inclusive) is considered to be the previous key to
388-
// s2.EndKey (exclusive).
389-
func contains(s1, s2 roachpb.Span) bool {
390-
if s2.Key != nil {
391-
// The common case.
392-
return s1.Contains(s2)
390+
// Contains returns whether s1 contains s2, where s2 can be a TrickySpan.
391+
func Contains(s1 roachpb.Span, s2 TrickySpan) bool {
392+
s2Span := roachpb.Span(s2)
393+
394+
if s2Span.Key != nil {
395+
// The common case: s2 is a regular span with a non-nil start key.
396+
return s1.Contains(s2Span)
393397
}
394398

399+
// s2 is a TrickySpan with nil Key and non-nil EndKey.
395400
// The following is equivalent to:
396401
// s1.Contains(roachpb.Span{Key: s2.EndKey.Prev()})
397402

398403
if s1.EndKey == nil {
399-
return s1.Key.IsPrev(s2.EndKey)
404+
return s1.Key.IsPrev(s2Span.EndKey)
405+
}
406+
407+
return s1.Key.Compare(s2Span.EndKey) < 0 && s1.EndKey.Compare(s2Span.EndKey) >= 0
408+
}
409+
410+
// Overlaps returns whether s1 overlaps s2, where s2 can be a TrickySpan.
411+
func Overlaps(s1 roachpb.Span, s2 TrickySpan) bool {
412+
s2Span := roachpb.Span(s2)
413+
414+
// The common case: both spans have non-nil start keys.
415+
if s2Span.Key != nil {
416+
return s1.Overlaps(s2Span)
417+
}
418+
419+
// s2 is a TrickySpan with nil Key and non-nil EndKey.
420+
// The following is equivalent to:
421+
// s1.Overlaps(roachpb.Span{Key: s2.EndKey.Prev()})
422+
423+
if s1.EndKey == nil {
424+
// s1 is a point span, overlaps with s2 iff s1.Key is the prev of s2.EndKey
425+
return s1.Key.IsPrev(s2Span.EndKey)
400426
}
401427

402-
return s1.Key.Compare(s2.EndKey) < 0 && s1.EndKey.Compare(s2.EndKey) >= 0
428+
// s1 is [s1.Key, s1.EndKey), s2 is [s2.EndKey.Prev(), s2.EndKey)
429+
// They overlap iff s2.EndKey.Prev() is in [s1.Key, s1.EndKey).
430+
return s1.Key.Compare(s2Span.EndKey) < 0 && s1.EndKey.Compare(s2Span.EndKey) >= 0
431+
}
432+
433+
// PartiallyOverlaps returns whether s1 partially overlaps s2,
434+
// where s2 can be a TrickySpan. If true, it means that s1 and s2 overlap, but
435+
// neither s1 nor s2 contain one another.
436+
func PartiallyOverlaps(s1 roachpb.Span, s2 TrickySpan) bool {
437+
s2Span := roachpb.Span(s2)
438+
439+
// The common case: both spans have non-nil start keys.
440+
if s2Span.Key != nil {
441+
return s1.Overlaps(s2Span) && !s1.Contains(s2Span) && !s2Span.Contains(s1)
442+
}
443+
444+
// At this point, we know that s2 is a TrickySpan, and it represents a special
445+
// point span. Point spans can never partially overlap with any span.
446+
return false
403447
}
404448

405449
// Validate returns an error if any spans that have been added to the set

pkg/kv/kvserver/spanset/spanset_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package spanset
77

88
import (
99
"reflect"
10+
"strings"
1011
"testing"
1112

1213
"github.com/cockroachdb/cockroach/pkg/keys"
@@ -394,3 +395,162 @@ func TestSpanSetWriteImpliesRead(t *testing.T) {
394395
t.Errorf("expected to be allowed to read rwSpan, error: %+v", err)
395396
}
396397
}
398+
399+
// makeSpanHelper accepts strings like: "a-d", and returns a span with
400+
// startKey = a, and endKey = d. It also accepts `X` which represents nil. For
401+
// example, "X-d" returns a span with a nil startKey, and endKey = d.
402+
func makeSpanHelper(t *testing.T, s string) roachpb.Span {
403+
parts := strings.Split(s, "-")
404+
require.Len(t, parts, 2)
405+
406+
var start roachpb.Key
407+
var end roachpb.Key
408+
409+
if parts[0] != "X" {
410+
start = roachpb.Key(parts[0])
411+
}
412+
413+
if parts[1] != "X" {
414+
end = roachpb.Key(parts[1])
415+
}
416+
417+
return roachpb.Span{
418+
Key: start,
419+
EndKey: end,
420+
}
421+
}
422+
423+
// Test that Contains correctly determines if s1 contains s2, including
424+
// support for spans with nil start/end keys.
425+
func TestContains(t *testing.T) {
426+
defer leaktest.AfterTest(t)()
427+
428+
testCases := []struct {
429+
name string
430+
s1 roachpb.Span
431+
s2 roachpb.Span
432+
expected bool
433+
}{
434+
// s1 equals s2.
435+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "a-c"), expected: true},
436+
// s1 contains s2.
437+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "b-c"), expected: true},
438+
// s1 contains point s2.
439+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "a-X"), expected: true},
440+
// Point s1 contains point s2.
441+
{s1: makeSpanHelper(t, "a-X"), s2: makeSpanHelper(t, "a-X"), expected: true},
442+
// s1 contains point s2 with nil startKey.
443+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "X-d"), expected: true},
444+
// Point s1 contains point s2 with nil startKey.
445+
{s1: makeSpanHelper(t, "a-X"), s2: roachpb.Span{EndKey: roachpb.Key("a").Next()}, expected: true},
446+
// s1 does not contain s2.
447+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "d-f"), expected: false},
448+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "b-d"), expected: false},
449+
{s1: makeSpanHelper(t, "b-c"), s2: makeSpanHelper(t, "a-d"), expected: false},
450+
// s1 does not contain s2 point.
451+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "d-X"), expected: false},
452+
// Point s1 does not contain s2.
453+
{s1: makeSpanHelper(t, "a-X"), s2: makeSpanHelper(t, "b-d"), expected: false},
454+
// Point s1 does not contain point s2.
455+
{s1: makeSpanHelper(t, "a-X"), s2: makeSpanHelper(t, "b-X"), expected: false},
456+
// s1 does not contain point s2 with nil startKey.
457+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "X-e"), expected: false},
458+
// Point s1 does not contain point s2 with nil startKey.
459+
{s1: makeSpanHelper(t, "a-X"), s2: makeSpanHelper(t, "X-b"), expected: false},
460+
}
461+
462+
for _, tc := range testCases {
463+
t.Run(tc.name, func(t *testing.T) {
464+
require.Equal(t, tc.expected, Contains(tc.s1, TrickySpan(tc.s2)))
465+
})
466+
}
467+
}
468+
469+
// Test that Overlaps correctly determines if s1 overlaps s2, including
470+
// support for spans with nil start/end keys.
471+
func TestOverlaps(t *testing.T) {
472+
defer leaktest.AfterTest(t)()
473+
474+
testCases := []struct {
475+
name string
476+
s1 roachpb.Span
477+
s2 roachpb.Span
478+
expected bool
479+
}{
480+
// s1 equals s2.
481+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "a-c"), expected: true},
482+
// s1 overlaps s2.
483+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "b-d"), expected: true},
484+
// s1 contains s2.
485+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "b-c"), expected: true},
486+
// s2 contains s1.
487+
{s1: makeSpanHelper(t, "b-c"), s2: makeSpanHelper(t, "a-d"), expected: true},
488+
// s1 overlaps point s2.
489+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "a-X"), expected: true},
490+
// s1 overlaps point s2 with nil startKey.
491+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "X-d"), expected: true},
492+
// Point s1 overlaps point s2 with nil startKey.
493+
{s1: makeSpanHelper(t, "a-X"), s2: roachpb.Span{EndKey: roachpb.Key("a").Next()}, expected: true},
494+
// Point s1 overlaps point s2.
495+
{s1: makeSpanHelper(t, "a-X"), s2: makeSpanHelper(t, "a-X"), expected: true},
496+
// s1 doesn't overlap s2.
497+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "d-f"), expected: false},
498+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "c-d"), expected: false},
499+
// s1 doesn't overlap point s2.
500+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "c-X"), expected: false},
501+
// s1 doesn't overlap point s2 with nil startKey.
502+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "X-a"), expected: false},
503+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "X-d"), expected: false},
504+
}
505+
506+
for _, tc := range testCases {
507+
t.Run("", func(t *testing.T) {
508+
require.Equal(t, tc.expected, Overlaps(tc.s1, TrickySpan(tc.s2)))
509+
})
510+
}
511+
}
512+
513+
// Test that PartiallyOverlaps correctly determines if s1 partially overlaps s2,
514+
// including support for spans with nil start/end keys.
515+
func TestPartiallyOverlaps(t *testing.T) {
516+
defer leaktest.AfterTest(t)()
517+
518+
testCases := []struct {
519+
name string
520+
s1 roachpb.Span
521+
s2 roachpb.Span
522+
expected bool
523+
}{
524+
// s1 equals s2.
525+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "a-c"), expected: false},
526+
// s1 partially overlaps s2.
527+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "b-d"), expected: true},
528+
{s1: makeSpanHelper(t, "b-d"), s2: makeSpanHelper(t, "a-c"), expected: true},
529+
// s1 contains s2.
530+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "b-c"), expected: false},
531+
// s2 contains s1.
532+
{s1: makeSpanHelper(t, "b-c"), s2: makeSpanHelper(t, "a-d"), expected: false},
533+
// s1 contains point s2.
534+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "a-X"), expected: false},
535+
// s1 contains point s2 with nil startKey.
536+
{s1: makeSpanHelper(t, "a-d"), s2: makeSpanHelper(t, "X-d"), expected: false},
537+
// Point s1 contains point s2 with nil startKey.
538+
{s1: makeSpanHelper(t, "a-X"), s2: roachpb.Span{EndKey: roachpb.Key("a").Next()}, expected: false},
539+
// Point s1 contains point s2.
540+
{s1: makeSpanHelper(t, "a-X"), s2: makeSpanHelper(t, "a-X"), expected: false},
541+
// s1 doesn't overlap s2.
542+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "d-f"), expected: false},
543+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "c-d"), expected: false},
544+
// s1 doesn't overlap point s2.
545+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "c-X"), expected: false},
546+
// s1 doesn't overlap point s2 with nil startKey.
547+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "X-a"), expected: false},
548+
{s1: makeSpanHelper(t, "a-c"), s2: makeSpanHelper(t, "X-d"), expected: false},
549+
}
550+
551+
for _, tc := range testCases {
552+
t.Run("", func(t *testing.T) {
553+
require.Equal(t, tc.expected, PartiallyOverlaps(tc.s1, TrickySpan(tc.s2)))
554+
})
555+
}
556+
}

0 commit comments

Comments
 (0)