Skip to content

Commit 8cca32d

Browse files
craig[bot]pav-kv
andcommitted
Merge #155847
155847: keys: assert that schema is immutable r=arulajmani,iskettaneh a=pav-kv This PR adds a test which ensures that storage schema in `doc.go` does not change. If it does, the test fails and forces the author to think harder about the implications of the change. **Context / example of a concern**: in replica destruction code (`kvstorage.DestroyReplica` and variants), we should destroy, among other things, the “unreplicated” keys. With separated engines this is tricky because the raft and state machine keys are inconveniently interleaved. Instead of using various ranged deletions that cover the entire keyspace, we can manually clear each key (which we all know). But if we do so, there is a risk someone adds a new key to the unreplicated section and forgets to update the destruction code. This at best could lead to a small disk space leak, and at worst a more serious bug in replica lifecycle. **Another example**: the unreplicated space does not have ranged Pebble keys. Some code relies on this, e.g. `applySnapshot` does not clear ranged keys when rewriting the replica state (as an optimization, because there are none). It will have to start doing so if ranged keys are ever added to the unreplicated span. Related to #152845 Co-authored-by: Pavel Kalinnikov <pavel@cockroachlabs.com>
2 parents 96930c7 + 3448280 commit 8cca32d

File tree

3 files changed

+108
-8
lines changed

3 files changed

+108
-8
lines changed

pkg/keys/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ go_test(
2929
name = "keys_test",
3030
size = "small",
3131
srcs = [
32+
"doc_test.go",
3233
"keys_test.go",
3334
"printer_test.go",
3435
"sql_test.go",

pkg/keys/doc.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ package keys
146146

147147
// NB: The sorting order of the symbols below map to the physical layout.
148148
// Preserve group-wise ordering when adding new constants.
149-
var _ = [...]interface{}{
149+
//
150+
// See TestSchemaIsStable for additional considerations when changing schema.
151+
var schema = [...]interface{}{
150152
MinKey,
151153

152154
// There are five types of local key data enumerated below: replicated
@@ -176,19 +178,21 @@ var _ = [...]interface{}{
176178
// range as a whole. Though they are replicated, they are unaddressable.
177179
// Typical examples are MVCC stats and the abort span. They all share
178180
// `LocalRangeIDPrefix` and `LocalRangeIDReplicatedInfix`.
179-
AbortSpanKey, // "abc-"
180-
RangeGCThresholdKey, // "lgc-"
181-
RangeAppliedStateKey, // "rask"
182-
RangeForceFlushKey, // "rffk"
183-
RangeLeaseKey, // "rll-"
184-
RangePriorReadSummaryKey, // "rprs"
181+
LocalRangeIDReplicatedInfix, // "r"
182+
AbortSpanKey, // "abc-"
183+
RangeGCThresholdKey, // "lgc-"
184+
RangeAppliedStateKey, // "rask"
185+
RangeForceFlushKey, // "rffk"
186+
RangeLeaseKey, // "rll-"
187+
RangePriorReadSummaryKey, // "rprs"
185188
ReplicatedSharedLocksTransactionLatchingKey, // "rsl-"
186-
RangeVersionKey, // "rver"
189+
RangeVersionKey, // "rver"
187190

188191
// 2. Unreplicated range-ID local keys: These contain metadata that
189192
// pertain to just one replica of a range. They are unreplicated and
190193
// unaddressable. The typical example is the Raft log. They all share
191194
// `LocalRangeIDPrefix` and `localRangeIDUnreplicatedInfix`.
195+
localRangeIDUnreplicatedInfix, // "u"
192196
RangeTombstoneKey, // "rftb"
193197
RaftHardStateKey, // "rfth"
194198
RaftLogKey, // "rftl"
@@ -200,6 +204,7 @@ var _ = [...]interface{}{
200204
// as a whole. They are replicated and addressable. Typical examples are
201205
// the range descriptor and transaction records. They all share
202206
// `LocalRangePrefix`.
207+
LocalRangePrefix, // "k"
203208
RangeProbeKey, // "prbe"
204209
QueueLastProcessedKey, // "qlpt"
205210
RangeDescriptorKey, // "rdsc"
@@ -208,6 +213,7 @@ var _ = [...]interface{}{
208213
// 4. Store local keys: These contain metadata about an individual store.
209214
// They are unreplicated and unaddressable. The typical example is the
210215
// store 'ident' record. They all share `localStorePrefix`.
216+
LocalStorePrefix, // "s"
211217
DeprecatedStoreClusterVersionKey, // "cver"
212218
StoreGossipKey, // "goss"
213219
StoreHLCUpperBoundKey, // "hlcu"
@@ -229,6 +235,7 @@ var _ = [...]interface{}{
229235
// Single key locks use a byte, LockTableSingleKeyInfix, that follows
230236
// the LocalRangeLockTablePrefix. This is to keep the single-key locks
231237
// separate from (future) range locks.
238+
LocalRangeLockTablePrefix, // "z"
232239
LockTableSingleKey,
233240

234241
// The global keyspace includes the meta{1,2}, system, system tenant SQL

pkg/keys/doc_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package keys
7+
8+
import (
9+
"testing"
10+
11+
"github.com/cockroachdb/cockroach/pkg/roachpb"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// TestSchemaIsStable asserts that the storage schema in doc.go does not change.
16+
// If it does (e.g. there is a new key), this test fails and nudges the author
17+
// to think through the implications.
18+
//
19+
// When changing the schema, it is a good idea to read the comments in doc.go
20+
// around the modified sections, and understand the lifecycle of the relevant
21+
// keys. For example, look at the code where the related keys are read, written,
22+
// modified, and removed.
23+
//
24+
// Note that it can be tricky: e.g. some code might be clearing a whole span of
25+
// keys (like unreplicated RangeID-local space), without explicitly calling out
26+
// individual keys, so the author should be aware of this to understand where
27+
// the lifetime of their new key ends.
28+
//
29+
// Conversely, some key spans can be cleared key-by-key, for efficiency or
30+
// simplicity reasons. New keys might need to be explicitly cleared in those
31+
// places, so that there is no storage space leakage. The author should not
32+
// assume that keys are cleared auto-magically in the right places. There are
33+
// "gaps" in the keyspace that don't have proper management because the code
34+
// assumes they are always empty.
35+
//
36+
// Another important aspect to consider is version compatibility, i.e. how
37+
// adjacent CRDB versions handle the added or removed keys. It might be
38+
// necessary to complete a migration to make a transition safe.
39+
//
40+
// Once this due diligence is done, it is ok to trivially fix this test.
41+
//
42+
// TODO(pav-kv): make a proper "structured" schema instead of a flat list of
43+
// things, and make the assertions stronger (currently they only count "lines"
44+
// in the schema).
45+
func TestSchemaIsStable(t *testing.T) {
46+
pos := make(map[string]struct {
47+
first int
48+
last int
49+
})
50+
add := func(k string, v int) {
51+
p, found := pos[k]
52+
if found {
53+
p.last++
54+
require.Equal(t, p.last, v)
55+
} else {
56+
p.first = v
57+
p.last = v
58+
}
59+
pos[k] = p
60+
}
61+
for i, ref := range schema {
62+
switch ref := ref.(type) {
63+
case []byte:
64+
add(string(ref), i)
65+
case roachpb.Key:
66+
add(string(ref), i)
67+
}
68+
}
69+
70+
section := func(from, to string, items int) {
71+
t.Helper()
72+
fromPos, ok := pos[from]
73+
require.True(t, ok)
74+
toPos, ok := pos[to]
75+
require.True(t, ok)
76+
require.Greater(t, toPos.first, fromPos.last)
77+
require.Equal(t, items, toPos.first-fromPos.last-1)
78+
}
79+
80+
// The replicated RangeID-local keys section.
81+
// WARNING: if this line fails, don't just fix it, read the test comment.
82+
section(string(LocalRangeIDReplicatedInfix), string(localRangeIDUnreplicatedInfix), 8)
83+
// The unreplicated RangeID-local keys section.
84+
// WARNING: if this line fails, don't just fix it, read the test comment.
85+
section(string(localRangeIDUnreplicatedInfix), string(LocalRangePrefix), 6)
86+
// The replicated range-local keys section.
87+
// WARNING: if this line fails, don't just fix it, read the test comment.
88+
section(string(LocalRangePrefix), string(LocalStorePrefix), 4)
89+
// The store-local keys section.
90+
// WARNING: if this line fails, don't just fix it, read the test comment.
91+
section(string(LocalStorePrefix), string(LocalRangeLockTablePrefix), 12)
92+
}

0 commit comments

Comments
 (0)