Skip to content

Commit f551b59

Browse files
committed
problemspans: add data structure
Package problemspans provides functionality for tracking and managing key spans that have been identified as problematic. It allows users to add spans with associated expiration times, check if a given key range overlaps any active (non-expired) spans, and remove spans when issues are resolved.
1 parent 38199b5 commit f551b59

File tree

9 files changed

+853
-0
lines changed

9 files changed

+853
-0
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/cockroachdb/pebble
33
require (
44
github.com/DataDog/zstd v1.5.7
55
github.com/HdrHistogram/hdrhistogram-go v1.1.2
6+
github.com/RaduBerinde/axisds v0.0.0-20250405232732-ecb85bedf677
67
github.com/cespare/xxhash/v2 v2.2.0
78
github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b
89
github.com/cockroachdb/datadriven v1.0.3-0.20250407164829-2945557346d5
@@ -38,6 +39,7 @@ require (
3839
github.com/getsentry/sentry-go v0.27.0 // indirect
3940
github.com/gogo/protobuf v1.3.2 // indirect
4041
github.com/golang/protobuf v1.5.3 // indirect
42+
github.com/google/btree v1.1.3 // indirect
4143
github.com/inconshreveable/mousetrap v1.0.0 // indirect
4244
github.com/kr/text v0.2.0 // indirect
4345
github.com/mattn/go-runewidth v0.0.9 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190129172621-c8b1d7a94ddf
1010
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
1111
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
1212
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
13+
github.com/RaduBerinde/axisds v0.0.0-20250405232732-ecb85bedf677 h1:NofOMIO/Z3301wDc9gIM/M0/+YjvOYSvUe7+30bkxkA=
14+
github.com/RaduBerinde/axisds v0.0.0-20250405232732-ecb85bedf677/go.mod h1:YO26VdZg1RVVjEhjmeEJE/4bLnD2mowj5eDiZQSLtlE=
1315
github.com/aclements/go-gg v0.0.0-20170118225347-6dbb4e4fefb0/go.mod h1:55qNq4vcpkIuHowELi5C8e+1yUHtoLoOUR9QU5j7Tes=
1416
github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g=
1517
github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY=
@@ -104,6 +106,8 @@ github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhS
104106
github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A=
105107
github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
106108
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
109+
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
110+
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
107111
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
108112
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
109113
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=

internal/problemspans/by_level.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2+
// of this source code is governed by a BSD-style license that can be found in
3+
// the LICENSE file.
4+
5+
package problemspans
6+
7+
import (
8+
"fmt"
9+
"strings"
10+
"sync"
11+
"sync/atomic"
12+
"time"
13+
14+
"github.com/cockroachdb/crlib/crstrings"
15+
"github.com/cockroachdb/crlib/crtime"
16+
"github.com/cockroachdb/pebble/internal/base"
17+
)
18+
19+
// ByLevel maintains a set of spans (separated by LSM level) with expiration
20+
// times and allows checking for overlap against active (non-expired) spans.
21+
//
22+
// When the spans added to the set are not overlapping, all operations are
23+
// logarithmic.
24+
//
25+
// ByLevel is safe for concurrent use.
26+
type ByLevel struct {
27+
empty atomic.Bool
28+
mu sync.Mutex
29+
levels []Set
30+
}
31+
32+
// Init must be called before using the ByLevel.
33+
func (bl *ByLevel) Init(numLevels int, cmp base.Compare) {
34+
bl.empty.Store(false)
35+
bl.levels = make([]Set, numLevels)
36+
for i := range bl.levels {
37+
bl.levels[i].Init(cmp)
38+
}
39+
}
40+
41+
// InitForTesting is used by tests which mock the time source.
42+
func (bl *ByLevel) InitForTesting(numLevels int, cmp base.Compare, nowFn func() crtime.Mono) {
43+
bl.empty.Store(false)
44+
bl.levels = make([]Set, numLevels)
45+
for i := range bl.levels {
46+
bl.levels[i].init(cmp, nowFn)
47+
}
48+
}
49+
50+
// IsEmpty returns true if there are no problem spans (the "normal" case). It
51+
// can be used in fast paths to avoid checking for specific overlaps.
52+
func (bl *ByLevel) IsEmpty() bool {
53+
if bl.empty.Load() {
54+
// Fast path.
55+
return true
56+
}
57+
bl.mu.Lock()
58+
defer bl.mu.Unlock()
59+
for i := range bl.levels {
60+
if !bl.levels[i].IsEmpty() {
61+
return false
62+
}
63+
}
64+
bl.empty.Store(true)
65+
return true
66+
}
67+
68+
// Add a span on a specific level. The span automatically expires after the
69+
// given duration.
70+
func (bl *ByLevel) Add(level int, bounds base.UserKeyBounds, expiration time.Duration) {
71+
bl.mu.Lock()
72+
defer bl.mu.Unlock()
73+
bl.empty.Store(false)
74+
bl.levels[level].Add(bounds, expiration)
75+
}
76+
77+
// Overlaps returns true if any active (non-expired) span on the given level
78+
// overlaps the given bounds.
79+
func (bl *ByLevel) Overlaps(level int, bounds base.UserKeyBounds) bool {
80+
if bl.empty.Load() {
81+
// Fast path.
82+
return false
83+
}
84+
bl.mu.Lock()
85+
defer bl.mu.Unlock()
86+
return bl.levels[level].Overlaps(bounds)
87+
}
88+
89+
// Excise a span from all levels. Any overlapping active (non-expired) spans are
90+
// split or trimmed accordingly.
91+
func (bl *ByLevel) Excise(bounds base.UserKeyBounds) {
92+
bl.mu.Lock()
93+
defer bl.mu.Unlock()
94+
for i := range bl.levels {
95+
bl.levels[i].Excise(bounds)
96+
}
97+
}
98+
99+
// String prints all active (non-expired) span fragments.
100+
func (bl *ByLevel) String() string {
101+
bl.mu.Lock()
102+
defer bl.mu.Unlock()
103+
var buf strings.Builder
104+
105+
for i := range bl.levels {
106+
if !bl.levels[i].IsEmpty() {
107+
fmt.Fprintf(&buf, "L%d:\n", i)
108+
for _, line := range crstrings.Lines(bl.levels[i].String()) {
109+
fmt.Fprintf(&buf, " %s\n", line)
110+
}
111+
}
112+
}
113+
if buf.Len() == 0 {
114+
return "<empty>"
115+
}
116+
return buf.String()
117+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2+
// of this source code is governed by a BSD-style license that can be found in
3+
// the LICENSE file.
4+
5+
package problemspans
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"strings"
11+
"testing"
12+
"time"
13+
14+
"github.com/cockroachdb/crlib/crstrings"
15+
"github.com/cockroachdb/crlib/crtime"
16+
"github.com/cockroachdb/datadriven"
17+
"github.com/cockroachdb/pebble/internal/base"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
func TestByLevel(t *testing.T) {
22+
const numLevels = 7
23+
now := crtime.Mono(0)
24+
nowFn := func() crtime.Mono { return now }
25+
var byLevel ByLevel
26+
byLevel.InitForTesting(numLevels, base.DefaultComparer.Compare, nowFn)
27+
28+
datadriven.RunTest(t, "testdata/by_level", func(t *testing.T, td *datadriven.TestData) string {
29+
var nowStr string
30+
if td.MaybeScanArgs(t, "now", &nowStr) {
31+
var nowVal int
32+
if n, err := fmt.Sscanf(nowStr, "%ds", &nowVal); err != nil || n != 1 {
33+
td.Fatalf(t, "error parsing now %q: %v", nowStr, err)
34+
}
35+
v := crtime.Mono(time.Duration(nowVal) * time.Second)
36+
if v < now {
37+
td.Fatalf(t, "now cannot go backwards")
38+
}
39+
now = v
40+
}
41+
42+
var out bytes.Buffer
43+
switch td.Cmd {
44+
45+
case "add":
46+
for _, line := range crstrings.Lines(td.Input) {
47+
parts := strings.SplitN(line, " ", 2)
48+
require.Equalf(t, 2, len(parts), "%s", line)
49+
var level int
50+
_, err := fmt.Sscanf(parts[0], "L%d", &level)
51+
require.NoError(t, err)
52+
bounds, expiration := parseSetLine(parts[1], true /* withTime */)
53+
// The test uses absolute expiration times.
54+
byLevel.Add(level, bounds, expiration.Sub(now))
55+
}
56+
57+
case "excise":
58+
for _, line := range crstrings.Lines(td.Input) {
59+
bounds, _ := parseSetLine(line, false /* withTime */)
60+
byLevel.Excise(bounds)
61+
}
62+
63+
case "overlap":
64+
for _, line := range crstrings.Lines(td.Input) {
65+
parts := strings.SplitN(line, " ", 2)
66+
require.Equal(t, 2, len(parts))
67+
var level int
68+
_, err := fmt.Sscanf(parts[0], "L%d", &level)
69+
require.NoError(t, err)
70+
bounds, _ := parseSetLine(parts[1], false /* withTime */)
71+
res := "overlap"
72+
if !byLevel.Overlaps(level, bounds) {
73+
res = "no overlap"
74+
}
75+
fmt.Fprintf(&out, "%s: %s\n", bounds, res)
76+
}
77+
78+
case "is-empty":
79+
if byLevel.IsEmpty() {
80+
out.WriteString("empty\n")
81+
} else {
82+
out.WriteString("not empty\n")
83+
}
84+
default:
85+
td.Fatalf(t, "unknown command %q", td.Cmd)
86+
}
87+
out.WriteString("ByLevel:\n")
88+
for _, l := range crstrings.Lines(byLevel.String()) {
89+
fmt.Fprintf(&out, " %s\n", l)
90+
}
91+
return out.String()
92+
})
93+
}

internal/problemspans/doc.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2+
// of this source code is governed by a BSD-style license that can be found in
3+
// the LICENSE file.
4+
5+
// Package problemspans provides functionality for tracking and managing key
6+
// spans that have been identified as problematic. It allows users to add spans
7+
// with associated expiration times, check if a given key range overlaps any
8+
// active (non-expired) spans, and remove spans when issues are resolved.
9+
//
10+
// This package is designed for efficiently tracking key ranges that may need
11+
// special handling.
12+
//
13+
// Key Features:
14+
//
15+
// - **Span Registration:**
16+
// Add spans with specified expiration times so that they automatically
17+
// become inactive after a set duration.
18+
//
19+
// - **Overlap Detection:**
20+
// Quickly check if a key range overlaps with any active problematic spans.
21+
//
22+
// - **Span Excise:**
23+
// Remove or adjust spans to reflect changes as issues are resolved.
24+
//
25+
// - **Level-Based Organization:**
26+
// The package offers a structure to organize and manage problematic spans
27+
// per level, with built-in support for concurrent operations.
28+
package problemspans

internal/problemspans/set.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2025 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2+
// of this source code is governed by a BSD-style license that can be found in
3+
// the LICENSE file.
4+
5+
package problemspans
6+
7+
import (
8+
"fmt"
9+
"strings"
10+
"time"
11+
12+
"github.com/RaduBerinde/axisds"
13+
"github.com/RaduBerinde/axisds/regiontree"
14+
"github.com/cockroachdb/crlib/crtime"
15+
"github.com/cockroachdb/pebble/internal/base"
16+
)
17+
18+
// Set maintains a set of spans with expiration times and allows checking for
19+
// overlap against non-expired spans.
20+
//
21+
// When the spans added to the set are not overlapping, all operations are
22+
// logarithmic.
23+
//
24+
// Set is not safe for concurrent use.
25+
type Set struct {
26+
cmp base.Compare
27+
nowFn func() crtime.Mono
28+
29+
now crtime.Mono
30+
31+
// We use a region tree with key boundaries and the expirationTime as a
32+
// property.
33+
rt regiontree.T[axisds.Endpoint[[]byte], expirationTime]
34+
}
35+
36+
// expirationTime of a problem span. 0 means that there is no problem span in a
37+
// region. Expiration times <= Set.now are equivalent to 0.
38+
type expirationTime crtime.Mono
39+
40+
// Init must be called before a Set can be used.
41+
func (s *Set) Init(cmp base.Compare) {
42+
s.init(cmp, crtime.NowMono)
43+
}
44+
45+
func (s *Set) init(cmp base.Compare, nowFn func() crtime.Mono) {
46+
*s = Set{}
47+
s.cmp = cmp
48+
s.nowFn = nowFn
49+
// The region tree supports a property equality function that "evolves" over
50+
// time, in that some properties that used to not be equal become equal. In
51+
// our case expired properties become equal to 0.
52+
//
53+
// Note that the region tree automatically removes boundaries between two
54+
// regions that have expired, even during enumeration.
55+
propEqFn := func(a, b expirationTime) bool {
56+
return a == b ||
57+
crtime.Mono(a) <= s.now && crtime.Mono(b) <= s.now // Both are expired or 0.
58+
}
59+
endpointCmp := axisds.EndpointCompareFn(axisds.CompareFn[[]byte](cmp))
60+
s.rt = regiontree.Make(endpointCmp, propEqFn)
61+
}
62+
63+
func boundsToEndpoints(bounds base.UserKeyBounds) (start, end axisds.Endpoint[[]byte]) {
64+
start = axisds.MakeStartEndpoint(bounds.Start, axisds.Inclusive)
65+
end = axisds.MakeEndEndpoint(bounds.End.Key, axisds.InclusiveIf(bounds.End.Kind == base.Inclusive))
66+
return start, end
67+
}
68+
69+
// Add a span to the set. The span automatically expires after the given duration.
70+
func (s *Set) Add(bounds base.UserKeyBounds, expiration time.Duration) {
71+
s.now = s.nowFn()
72+
expTime := expirationTime(s.now + crtime.Mono(expiration))
73+
start, end := boundsToEndpoints(bounds)
74+
s.rt.Update(start, end, func(p expirationTime) expirationTime {
75+
return max(p, expTime)
76+
})
77+
}
78+
79+
// Overlaps returns true if the bounds overlap with a non-expired span.
80+
func (s *Set) Overlaps(bounds base.UserKeyBounds) bool {
81+
s.now = s.nowFn()
82+
start, end := boundsToEndpoints(bounds)
83+
return s.rt.AnyWithGC(start, end, func(exp expirationTime) bool {
84+
return crtime.Mono(exp) > s.now
85+
})
86+
}
87+
88+
// Excise removes a span fragment from all spans in the set. Any overlapping
89+
// non-expired spans are cut accordingly.
90+
func (s *Set) Excise(bounds base.UserKeyBounds) {
91+
s.now = s.nowFn()
92+
start, end := boundsToEndpoints(bounds)
93+
s.rt.Update(start, end, func(p expirationTime) expirationTime {
94+
return 0
95+
})
96+
}
97+
98+
// IsEmpty returns true if the set contains no non-expired spans.
99+
func (s *Set) IsEmpty() bool {
100+
s.now = s.nowFn()
101+
return s.rt.IsEmpty()
102+
}
103+
104+
// String prints all active (non-expired) span fragments.
105+
func (s *Set) String() string {
106+
var buf strings.Builder
107+
s.now = s.nowFn()
108+
s.rt.EnumerateAll(func(start, end axisds.Endpoint[[]byte], prop expirationTime) bool {
109+
fmt.Fprintf(&buf, "%s expires in: %s\n", keyEndpointIntervalFormatter(start, end), time.Duration(prop)-time.Duration(s.now))
110+
return true
111+
})
112+
if buf.Len() == 0 {
113+
return "<empty>"
114+
}
115+
return buf.String()
116+
}
117+
118+
var keyBoundaryFormatter axisds.BoundaryFormatter[[]byte] = func(b []byte) string {
119+
return string(b)
120+
}
121+
122+
var keyEndpointIntervalFormatter = axisds.MakeEndpointIntervalFormatter(keyBoundaryFormatter)

0 commit comments

Comments
 (0)