feat(tree): add persistent copy-on-write B-tree and snapshots
- commit
- f361506823ca149b1e727fa79ef3b9eb9296646e
- author
- brice · · 2026-10-10 01:45 UTC
- parent
- 4338ab4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +861 -0
A snapshot.go +59 -0
| @@ -0,0 +1,59 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package kv | |
| 4 | + | ||
| 5 | + | import "iter" | |
| 6 | + | ||
| 7 | + | // Item is the value stored under a key. | |
| 8 | + | type Item struct { | |
| 9 | + | Value []byte // owned by the snapshot: must not be modified | |
| 10 | + | Version uint64 // seq of the batch that last wrote the key | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | // Snapshot is an immutable, consistent view of the database at one seq. It is | |
| 14 | + | // safe for concurrent use and stays valid, unchanged, after later writes. | |
| 15 | + | // Keys yielded by iterators and Item.Value are owned by the snapshot and must | |
| 16 | + | // not be modified. | |
| 17 | + | type Snapshot struct { | |
| 18 | + | t tree | |
| 19 | + | seq uint64 | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func newSnapshot(t tree, seq uint64) *Snapshot { | |
| 23 | + | return &Snapshot{t: t, seq: seq} | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // Seq returns the seq of the last batch applied in this snapshot. | |
| 27 | + | func (s *Snapshot) Seq() uint64 { return s.seq } | |
| 28 | + | ||
| 29 | + | // Len returns the number of keys. | |
| 30 | + | func (s *Snapshot) Len() int { return s.t.len() } | |
| 31 | + | ||
| 32 | + | // Get returns the item stored under key. | |
| 33 | + | func (s *Snapshot) Get(key []byte) (Item, bool) { return s.t.get(key) } | |
| 34 | + | ||
| 35 | + | // Range iterates over the keys in [start, end) in ascending order. A nil end | |
| 36 | + | // (or an empty start) is unbounded. Breaking out of the loop stops the walk. | |
| 37 | + | func (s *Snapshot) Range(start, end []byte) iter.Seq2[[]byte, Item] { | |
| 38 | + | return s.t.ascend(start, end) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Prefix iterates over the keys that begin with p in ascending order. An | |
| 42 | + | // empty p matches every key. | |
| 43 | + | func (s *Snapshot) Prefix(p []byte) iter.Seq2[[]byte, Item] { | |
| 44 | + | return s.t.ascend(p, prefixEnd(p)) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // prefixEnd returns the smallest key greater than every key that begins with | |
| 48 | + | // p, or nil if there is none (p is empty or all 0xFF). | |
| 49 | + | func prefixEnd(p []byte) []byte { | |
| 50 | + | for i := len(p) - 1; i >= 0; i-- { | |
| 51 | + | if p[i] < 0xFF { | |
| 52 | + | end := make([]byte, i+1) | |
| 53 | + | copy(end, p) | |
| 54 | + | end[i]++ | |
| 55 | + | return end | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | return nil | |
| 59 | + | } | |
| @@ -0,0 +1,59 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package kv | |||
| 4 | + | ||||
| 5 | + | import "iter" | |||
| 6 | + | ||||
| 7 | + | // Item is the value stored under a key. | |||
| 8 | + | type Item struct { | |||
| 9 | + | Value []byte // owned by the snapshot: must not be modified | |||
| 10 | + | Version uint64 // seq of the batch that last wrote the key | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | // Snapshot is an immutable, consistent view of the database at one seq. It is | |||
| 14 | + | // safe for concurrent use and stays valid, unchanged, after later writes. | |||
| 15 | + | // Keys yielded by iterators and Item.Value are owned by the snapshot and must | |||
| 16 | + | // not be modified. | |||
| 17 | + | type Snapshot struct { | |||
| 18 | + | t tree | |||
| 19 | + | seq uint64 | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | func newSnapshot(t tree, seq uint64) *Snapshot { | |||
| 23 | + | return &Snapshot{t: t, seq: seq} | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // Seq returns the seq of the last batch applied in this snapshot. | |||
| 27 | + | func (s *Snapshot) Seq() uint64 { return s.seq } | |||
| 28 | + | ||||
| 29 | + | // Len returns the number of keys. | |||
| 30 | + | func (s *Snapshot) Len() int { return s.t.len() } | |||
| 31 | + | ||||
| 32 | + | // Get returns the item stored under key. | |||
| 33 | + | func (s *Snapshot) Get(key []byte) (Item, bool) { return s.t.get(key) } | |||
| 34 | + | ||||
| 35 | + | // Range iterates over the keys in [start, end) in ascending order. A nil end | |||
| 36 | + | // (or an empty start) is unbounded. Breaking out of the loop stops the walk. | |||
| 37 | + | func (s *Snapshot) Range(start, end []byte) iter.Seq2[[]byte, Item] { | |||
| 38 | + | return s.t.ascend(start, end) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // Prefix iterates over the keys that begin with p in ascending order. An | |||
| 42 | + | // empty p matches every key. | |||
| 43 | + | func (s *Snapshot) Prefix(p []byte) iter.Seq2[[]byte, Item] { | |||
| 44 | + | return s.t.ascend(p, prefixEnd(p)) | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // prefixEnd returns the smallest key greater than every key that begins with | |||
| 48 | + | // p, or nil if there is none (p is empty or all 0xFF). | |||
| 49 | + | func prefixEnd(p []byte) []byte { | |||
| 50 | + | for i := len(p) - 1; i >= 0; i-- { | |||
| 51 | + | if p[i] < 0xFF { | |||
| 52 | + | end := make([]byte, i+1) | |||
| 53 | + | copy(end, p) | |||
| 54 | + | end[i]++ | |||
| 55 | + | return end | |||
| 56 | + | } | |||
| 57 | + | } | |||
| 58 | + | return nil | |||
| 59 | + | } | |||
A tree.go +356 -0
| @@ -0,0 +1,356 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package kv | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "iter" | |
| 8 | + | "slices" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // The in-memory index is a persistent (copy-on-write) B-tree of order 32: | |
| 12 | + | // a node holds at most maxEntries entries and, except for the root, at least | |
| 13 | + | // minEntries. Entries live in internal nodes too. | |
| 14 | + | const ( | |
| 15 | + | degree = 32 | |
| 16 | + | maxEntries = 2*degree - 1 | |
| 17 | + | minEntries = degree - 1 | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // entry is a key with its value and the seq of the batch that last wrote it. | |
| 21 | + | // The tree never copies or modifies key and value: callers pass owned slices. | |
| 22 | + | type entry struct { | |
| 23 | + | key []byte | |
| 24 | + | value []byte | |
| 25 | + | version uint64 | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // owner identifies one builder epoch. It has a non-zero size so that every | |
| 29 | + | // new(owner) is a distinct pointer. | |
| 30 | + | type owner struct{ _ byte } | |
| 31 | + | ||
| 32 | + | // node is a B-tree node. A node without children is a leaf; otherwise it has | |
| 33 | + | // len(entries)+1 children. | |
| 34 | + | // | |
| 35 | + | // Invariant: a node is mutated only by the builder whose current owner token | |
| 36 | + | // equals node.own. freeze gives the builder a fresh token, so no live builder | |
| 37 | + | // holds the token of any node reachable from a frozen tree: published nodes | |
| 38 | + | // are never mutated, and a builder clones a node the first time it touches it. | |
| 39 | + | type node struct { | |
| 40 | + | own *owner | |
| 41 | + | entries []entry | |
| 42 | + | children []*node | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (n *node) leaf() bool { return len(n.children) == 0 } | |
| 46 | + | ||
| 47 | + | // find returns the index of the first entry whose key is >= key and whether | |
| 48 | + | // that entry's key equals key. | |
| 49 | + | func (n *node) find(key []byte) (int, bool) { | |
| 50 | + | return slices.BinarySearchFunc(n.entries, key, func(e entry, k []byte) int { | |
| 51 | + | return bytes.Compare(e.key, k) | |
| 52 | + | }) | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // lookup returns the entry stored under key, if any. | |
| 56 | + | func lookup(n *node, key []byte) (entry, bool) { | |
| 57 | + | for n != nil { | |
| 58 | + | i, found := n.find(key) | |
| 59 | + | if found { | |
| 60 | + | return n.entries[i], true | |
| 61 | + | } | |
| 62 | + | if n.leaf() { | |
| 63 | + | break | |
| 64 | + | } | |
| 65 | + | n = n.children[i] | |
| 66 | + | } | |
| 67 | + | return entry{}, false | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // tree is an immutable persistent B-tree; the zero value is empty. Its methods | |
| 71 | + | // never modify existing nodes, so a tree can be shared between goroutines. | |
| 72 | + | type tree struct { | |
| 73 | + | root *node | |
| 74 | + | n int | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func (t tree) len() int { return t.n } | |
| 78 | + | ||
| 79 | + | func (t tree) get(key []byte) (Item, bool) { | |
| 80 | + | e, ok := lookup(t.root, key) | |
| 81 | + | return Item{Value: e.value, Version: e.version}, ok | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // put returns a tree where key maps to value at version; t is untouched. | |
| 85 | + | func (t tree) put(key, value []byte, version uint64) tree { | |
| 86 | + | b := t.builder() | |
| 87 | + | b.put(key, value, version) | |
| 88 | + | return b.freeze() | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // delete returns a tree without key; t is untouched. | |
| 92 | + | func (t tree) delete(key []byte) tree { | |
| 93 | + | b := t.builder() | |
| 94 | + | b.delete(key) | |
| 95 | + | return b.freeze() | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // ascend yields the entries with start <= key < end in ascending order. An | |
| 99 | + | // empty start and a nil end are unbounded. It walks with an explicit stack and | |
| 100 | + | // does not allocate per yielded item. | |
| 101 | + | func (t tree) ascend(start, end []byte) iter.Seq2[[]byte, Item] { | |
| 102 | + | return func(yield func([]byte, Item) bool) { | |
| 103 | + | // A frame's i is the index of the next entry of n to yield; the | |
| 104 | + | // child left of that entry has already been visited or skipped. | |
| 105 | + | type frame struct { | |
| 106 | + | n *node | |
| 107 | + | i int | |
| 108 | + | } | |
| 109 | + | var buf [16]frame | |
| 110 | + | stack := buf[:0] | |
| 111 | + | ||
| 112 | + | // Seek to the first entry >= start. | |
| 113 | + | for n := t.root; n != nil; { | |
| 114 | + | i, found := n.find(start) | |
| 115 | + | stack = append(stack, frame{n, i}) | |
| 116 | + | if found || n.leaf() { | |
| 117 | + | break | |
| 118 | + | } | |
| 119 | + | n = n.children[i] | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | for len(stack) > 0 { | |
| 123 | + | f := &stack[len(stack)-1] | |
| 124 | + | if f.i >= len(f.n.entries) { | |
| 125 | + | stack = stack[:len(stack)-1] | |
| 126 | + | continue | |
| 127 | + | } | |
| 128 | + | n := f.n | |
| 129 | + | e := &n.entries[f.i] | |
| 130 | + | f.i++ | |
| 131 | + | if end != nil && bytes.Compare(e.key, end) >= 0 { | |
| 132 | + | return | |
| 133 | + | } | |
| 134 | + | if !yield(e.key, Item{Value: e.value, Version: e.version}) { | |
| 135 | + | return | |
| 136 | + | } | |
| 137 | + | // Descend to the leftmost leaf of the subtree right of e. | |
| 138 | + | for c := n; !c.leaf(); { | |
| 139 | + | c = c.children[f.i] | |
| 140 | + | stack = append(stack, frame{c, 0}) | |
| 141 | + | f = &stack[len(stack)-1] | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | // builder applies many mutations to a tree while copying each touched node at | |
| 148 | + | // most once. It is not safe for concurrent use. Trees returned by freeze are | |
| 149 | + | // immutable and independent of later builder operations. | |
| 150 | + | type builder struct { | |
| 151 | + | root *node | |
| 152 | + | n int | |
| 153 | + | own *owner | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // builder returns a builder starting from t; t is untouched. | |
| 157 | + | func (t tree) builder() *builder { | |
| 158 | + | return &builder{root: t.root, n: t.n, own: new(owner)} | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | // freeze returns the current state as an immutable tree. The builder remains | |
| 162 | + | // usable: it switches to a new owner token, so nodes of the returned tree are | |
| 163 | + | // cloned before any further mutation. | |
| 164 | + | func (b *builder) freeze() tree { | |
| 165 | + | b.own = new(owner) | |
| 166 | + | return tree{root: b.root, n: b.n} | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func (b *builder) len() int { return b.n } | |
| 170 | + | ||
| 171 | + | func (b *builder) get(key []byte) (Item, bool) { | |
| 172 | + | e, ok := lookup(b.root, key) | |
| 173 | + | return Item{Value: e.value, Version: e.version}, ok | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // mutable returns n if this builder owns it, else an owned shallow copy. | |
| 177 | + | func (b *builder) mutable(n *node) *node { | |
| 178 | + | if n.own == b.own { | |
| 179 | + | return n | |
| 180 | + | } | |
| 181 | + | c := &node{own: b.own, entries: slices.Clone(n.entries)} | |
| 182 | + | if len(n.children) > 0 { | |
| 183 | + | c.children = slices.Clone(n.children) | |
| 184 | + | } | |
| 185 | + | return c | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | // mutableChild makes n.children[i] owned and returns it. n must be owned. | |
| 189 | + | func (b *builder) mutableChild(n *node, i int) *node { | |
| 190 | + | c := b.mutable(n.children[i]) | |
| 191 | + | n.children[i] = c | |
| 192 | + | return c | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | // split cuts the owned node n at entry i: n keeps entries[:i], the returned | |
| 196 | + | // entry is entries[i], and the returned node holds the rest. | |
| 197 | + | func (b *builder) split(n *node, i int) (entry, *node) { | |
| 198 | + | mid := n.entries[i] | |
| 199 | + | r := &node{own: b.own, entries: slices.Clone(n.entries[i+1:])} | |
| 200 | + | clear(n.entries[i:]) | |
| 201 | + | n.entries = n.entries[:i] | |
| 202 | + | if !n.leaf() { | |
| 203 | + | r.children = slices.Clone(n.children[i+1:]) | |
| 204 | + | clear(n.children[i+1:]) | |
| 205 | + | n.children = n.children[:i+1] | |
| 206 | + | } | |
| 207 | + | return mid, r | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | // put sets key to value at version, replacing any existing entry. | |
| 211 | + | func (b *builder) put(key, value []byte, version uint64) { | |
| 212 | + | e := entry{key: key, value: value, version: version} | |
| 213 | + | if b.root == nil { | |
| 214 | + | b.root = &node{own: b.own, entries: []entry{e}} | |
| 215 | + | b.n = 1 | |
| 216 | + | return | |
| 217 | + | } | |
| 218 | + | b.root = b.mutable(b.root) | |
| 219 | + | if len(b.root.entries) >= maxEntries { | |
| 220 | + | mid, r := b.split(b.root, maxEntries/2) | |
| 221 | + | b.root = &node{ | |
| 222 | + | own: b.own, | |
| 223 | + | entries: []entry{mid}, | |
| 224 | + | children: []*node{b.root, r}, | |
| 225 | + | } | |
| 226 | + | } | |
| 227 | + | if !b.insert(b.root, e) { | |
| 228 | + | b.n++ | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | // insert adds e below the owned, non-full node n and reports whether it | |
| 233 | + | // replaced an existing entry. Full children are split on the way down. | |
| 234 | + | func (b *builder) insert(n *node, e entry) (replaced bool) { | |
| 235 | + | for { | |
| 236 | + | i, found := n.find(e.key) | |
| 237 | + | if found { | |
| 238 | + | n.entries[i] = e | |
| 239 | + | return true | |
| 240 | + | } | |
| 241 | + | if n.leaf() { | |
| 242 | + | n.entries = slices.Insert(n.entries, i, e) | |
| 243 | + | return false | |
| 244 | + | } | |
| 245 | + | c := b.mutableChild(n, i) | |
| 246 | + | if len(c.entries) >= maxEntries { | |
| 247 | + | mid, r := b.split(c, maxEntries/2) | |
| 248 | + | n.entries = slices.Insert(n.entries, i, mid) | |
| 249 | + | n.children = slices.Insert(n.children, i+1, r) | |
| 250 | + | switch cmp := bytes.Compare(e.key, mid.key); { | |
| 251 | + | case cmp == 0: | |
| 252 | + | n.entries[i] = e | |
| 253 | + | return true | |
| 254 | + | case cmp > 0: | |
| 255 | + | c = r | |
| 256 | + | } | |
| 257 | + | } | |
| 258 | + | n = c | |
| 259 | + | } | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | // delete removes key if present. | |
| 263 | + | func (b *builder) delete(key []byte) { | |
| 264 | + | if _, ok := lookup(b.root, key); !ok { | |
| 265 | + | return | |
| 266 | + | } | |
| 267 | + | b.root = b.mutable(b.root) | |
| 268 | + | b.remove(b.root, key, false) | |
| 269 | + | b.n-- | |
| 270 | + | switch { | |
| 271 | + | case len(b.root.entries) > 0: | |
| 272 | + | case b.root.leaf(): | |
| 273 | + | b.root = nil | |
| 274 | + | default: | |
| 275 | + | b.root = b.root.children[0] | |
| 276 | + | } | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | // remove deletes key (or, with max, the largest entry) from the subtree of the | |
| 280 | + | // owned node n and returns the removed entry. The key must exist. Every child | |
| 281 | + | // descended into is first guaranteed to hold more than minEntries entries. | |
| 282 | + | func (b *builder) remove(n *node, key []byte, max bool) entry { | |
| 283 | + | for { | |
| 284 | + | var i int | |
| 285 | + | var found bool | |
| 286 | + | switch { | |
| 287 | + | case max && n.leaf(): | |
| 288 | + | i, found = len(n.entries)-1, true | |
| 289 | + | case max: | |
| 290 | + | i = len(n.entries) | |
| 291 | + | default: | |
| 292 | + | i, found = n.find(key) | |
| 293 | + | } | |
| 294 | + | if n.leaf() { | |
| 295 | + | e := n.entries[i] | |
| 296 | + | n.entries = slices.Delete(n.entries, i, i+1) | |
| 297 | + | return e | |
| 298 | + | } | |
| 299 | + | if len(n.children[i].entries) <= minEntries { | |
| 300 | + | b.growChild(n, i) | |
| 301 | + | continue // the shape of n changed: search again | |
| 302 | + | } | |
| 303 | + | c := b.mutableChild(n, i) | |
| 304 | + | if found { | |
| 305 | + | // Replace the entry by its predecessor. | |
| 306 | + | e := n.entries[i] | |
| 307 | + | n.entries[i] = b.remove(c, nil, true) | |
| 308 | + | return e | |
| 309 | + | } | |
| 310 | + | n = c | |
| 311 | + | } | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | // growChild ensures n.children[i] has more than minEntries entries by | |
| 315 | + | // borrowing from a sibling or merging with one. n must be owned. | |
| 316 | + | func (b *builder) growChild(n *node, i int) { | |
| 317 | + | switch { | |
| 318 | + | case i > 0 && len(n.children[i-1].entries) > minEntries: | |
| 319 | + | c := b.mutableChild(n, i) | |
| 320 | + | l := b.mutableChild(n, i-1) | |
| 321 | + | last := len(l.entries) - 1 | |
| 322 | + | c.entries = slices.Insert(c.entries, 0, n.entries[i-1]) | |
| 323 | + | n.entries[i-1] = l.entries[last] | |
| 324 | + | l.entries[last] = entry{} | |
| 325 | + | l.entries = l.entries[:last] | |
| 326 | + | if !c.leaf() { | |
| 327 | + | lc := len(l.children) - 1 | |
| 328 | + | c.children = slices.Insert(c.children, 0, l.children[lc]) | |
| 329 | + | l.children[lc] = nil | |
| 330 | + | l.children = l.children[:lc] | |
| 331 | + | } | |
| 332 | + | case i < len(n.entries) && len(n.children[i+1].entries) > minEntries: | |
| 333 | + | c := b.mutableChild(n, i) | |
| 334 | + | r := b.mutableChild(n, i+1) | |
| 335 | + | c.entries = append(c.entries, n.entries[i]) | |
| 336 | + | n.entries[i] = r.entries[0] | |
| 337 | + | r.entries = slices.Delete(r.entries, 0, 1) | |
| 338 | + | if !c.leaf() { | |
| 339 | + | c.children = append(c.children, r.children[0]) | |
| 340 | + | r.children = slices.Delete(r.children, 0, 1) | |
| 341 | + | } | |
| 342 | + | default: | |
| 343 | + | // Merge children i and i+1 around entry i (children i-1 and i | |
| 344 | + | // around entry i-1 for the last child). | |
| 345 | + | if i == len(n.entries) { | |
| 346 | + | i-- | |
| 347 | + | } | |
| 348 | + | c := b.mutableChild(n, i) | |
| 349 | + | r := n.children[i+1] // only read | |
| 350 | + | c.entries = append(c.entries, n.entries[i]) | |
| 351 | + | c.entries = append(c.entries, r.entries...) | |
| 352 | + | c.children = append(c.children, r.children...) | |
| 353 | + | n.entries = slices.Delete(n.entries, i, i+1) | |
| 354 | + | n.children = slices.Delete(n.children, i+1, i+2) | |
| 355 | + | } | |
| 356 | + | } | |
| @@ -0,0 +1,356 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package kv | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "iter" | |||
| 8 | + | "slices" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // The in-memory index is a persistent (copy-on-write) B-tree of order 32: | |||
| 12 | + | // a node holds at most maxEntries entries and, except for the root, at least | |||
| 13 | + | // minEntries. Entries live in internal nodes too. | |||
| 14 | + | const ( | |||
| 15 | + | degree = 32 | |||
| 16 | + | maxEntries = 2*degree - 1 | |||
| 17 | + | minEntries = degree - 1 | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | // entry is a key with its value and the seq of the batch that last wrote it. | |||
| 21 | + | // The tree never copies or modifies key and value: callers pass owned slices. | |||
| 22 | + | type entry struct { | |||
| 23 | + | key []byte | |||
| 24 | + | value []byte | |||
| 25 | + | version uint64 | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // owner identifies one builder epoch. It has a non-zero size so that every | |||
| 29 | + | // new(owner) is a distinct pointer. | |||
| 30 | + | type owner struct{ _ byte } | |||
| 31 | + | ||||
| 32 | + | // node is a B-tree node. A node without children is a leaf; otherwise it has | |||
| 33 | + | // len(entries)+1 children. | |||
| 34 | + | // | |||
| 35 | + | // Invariant: a node is mutated only by the builder whose current owner token | |||
| 36 | + | // equals node.own. freeze gives the builder a fresh token, so no live builder | |||
| 37 | + | // holds the token of any node reachable from a frozen tree: published nodes | |||
| 38 | + | // are never mutated, and a builder clones a node the first time it touches it. | |||
| 39 | + | type node struct { | |||
| 40 | + | own *owner | |||
| 41 | + | entries []entry | |||
| 42 | + | children []*node | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (n *node) leaf() bool { return len(n.children) == 0 } | |||
| 46 | + | ||||
| 47 | + | // find returns the index of the first entry whose key is >= key and whether | |||
| 48 | + | // that entry's key equals key. | |||
| 49 | + | func (n *node) find(key []byte) (int, bool) { | |||
| 50 | + | return slices.BinarySearchFunc(n.entries, key, func(e entry, k []byte) int { | |||
| 51 | + | return bytes.Compare(e.key, k) | |||
| 52 | + | }) | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // lookup returns the entry stored under key, if any. | |||
| 56 | + | func lookup(n *node, key []byte) (entry, bool) { | |||
| 57 | + | for n != nil { | |||
| 58 | + | i, found := n.find(key) | |||
| 59 | + | if found { | |||
| 60 | + | return n.entries[i], true | |||
| 61 | + | } | |||
| 62 | + | if n.leaf() { | |||
| 63 | + | break | |||
| 64 | + | } | |||
| 65 | + | n = n.children[i] | |||
| 66 | + | } | |||
| 67 | + | return entry{}, false | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // tree is an immutable persistent B-tree; the zero value is empty. Its methods | |||
| 71 | + | // never modify existing nodes, so a tree can be shared between goroutines. | |||
| 72 | + | type tree struct { | |||
| 73 | + | root *node | |||
| 74 | + | n int | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | func (t tree) len() int { return t.n } | |||
| 78 | + | ||||
| 79 | + | func (t tree) get(key []byte) (Item, bool) { | |||
| 80 | + | e, ok := lookup(t.root, key) | |||
| 81 | + | return Item{Value: e.value, Version: e.version}, ok | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // put returns a tree where key maps to value at version; t is untouched. | |||
| 85 | + | func (t tree) put(key, value []byte, version uint64) tree { | |||
| 86 | + | b := t.builder() | |||
| 87 | + | b.put(key, value, version) | |||
| 88 | + | return b.freeze() | |||
| 89 | + | } | |||
| 90 | + | ||||
| 91 | + | // delete returns a tree without key; t is untouched. | |||
| 92 | + | func (t tree) delete(key []byte) tree { | |||
| 93 | + | b := t.builder() | |||
| 94 | + | b.delete(key) | |||
| 95 | + | return b.freeze() | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // ascend yields the entries with start <= key < end in ascending order. An | |||
| 99 | + | // empty start and a nil end are unbounded. It walks with an explicit stack and | |||
| 100 | + | // does not allocate per yielded item. | |||
| 101 | + | func (t tree) ascend(start, end []byte) iter.Seq2[[]byte, Item] { | |||
| 102 | + | return func(yield func([]byte, Item) bool) { | |||
| 103 | + | // A frame's i is the index of the next entry of n to yield; the | |||
| 104 | + | // child left of that entry has already been visited or skipped. | |||
| 105 | + | type frame struct { | |||
| 106 | + | n *node | |||
| 107 | + | i int | |||
| 108 | + | } | |||
| 109 | + | var buf [16]frame | |||
| 110 | + | stack := buf[:0] | |||
| 111 | + | ||||
| 112 | + | // Seek to the first entry >= start. | |||
| 113 | + | for n := t.root; n != nil; { | |||
| 114 | + | i, found := n.find(start) | |||
| 115 | + | stack = append(stack, frame{n, i}) | |||
| 116 | + | if found || n.leaf() { | |||
| 117 | + | break | |||
| 118 | + | } | |||
| 119 | + | n = n.children[i] | |||
| 120 | + | } | |||
| 121 | + | ||||
| 122 | + | for len(stack) > 0 { | |||
| 123 | + | f := &stack[len(stack)-1] | |||
| 124 | + | if f.i >= len(f.n.entries) { | |||
| 125 | + | stack = stack[:len(stack)-1] | |||
| 126 | + | continue | |||
| 127 | + | } | |||
| 128 | + | n := f.n | |||
| 129 | + | e := &n.entries[f.i] | |||
| 130 | + | f.i++ | |||
| 131 | + | if end != nil && bytes.Compare(e.key, end) >= 0 { | |||
| 132 | + | return | |||
| 133 | + | } | |||
| 134 | + | if !yield(e.key, Item{Value: e.value, Version: e.version}) { | |||
| 135 | + | return | |||
| 136 | + | } | |||
| 137 | + | // Descend to the leftmost leaf of the subtree right of e. | |||
| 138 | + | for c := n; !c.leaf(); { | |||
| 139 | + | c = c.children[f.i] | |||
| 140 | + | stack = append(stack, frame{c, 0}) | |||
| 141 | + | f = &stack[len(stack)-1] | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | // builder applies many mutations to a tree while copying each touched node at | |||
| 148 | + | // most once. It is not safe for concurrent use. Trees returned by freeze are | |||
| 149 | + | // immutable and independent of later builder operations. | |||
| 150 | + | type builder struct { | |||
| 151 | + | root *node | |||
| 152 | + | n int | |||
| 153 | + | own *owner | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | // builder returns a builder starting from t; t is untouched. | |||
| 157 | + | func (t tree) builder() *builder { | |||
| 158 | + | return &builder{root: t.root, n: t.n, own: new(owner)} | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | // freeze returns the current state as an immutable tree. The builder remains | |||
| 162 | + | // usable: it switches to a new owner token, so nodes of the returned tree are | |||
| 163 | + | // cloned before any further mutation. | |||
| 164 | + | func (b *builder) freeze() tree { | |||
| 165 | + | b.own = new(owner) | |||
| 166 | + | return tree{root: b.root, n: b.n} | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | func (b *builder) len() int { return b.n } | |||
| 170 | + | ||||
| 171 | + | func (b *builder) get(key []byte) (Item, bool) { | |||
| 172 | + | e, ok := lookup(b.root, key) | |||
| 173 | + | return Item{Value: e.value, Version: e.version}, ok | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | // mutable returns n if this builder owns it, else an owned shallow copy. | |||
| 177 | + | func (b *builder) mutable(n *node) *node { | |||
| 178 | + | if n.own == b.own { | |||
| 179 | + | return n | |||
| 180 | + | } | |||
| 181 | + | c := &node{own: b.own, entries: slices.Clone(n.entries)} | |||
| 182 | + | if len(n.children) > 0 { | |||
| 183 | + | c.children = slices.Clone(n.children) | |||
| 184 | + | } | |||
| 185 | + | return c | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | // mutableChild makes n.children[i] owned and returns it. n must be owned. | |||
| 189 | + | func (b *builder) mutableChild(n *node, i int) *node { | |||
| 190 | + | c := b.mutable(n.children[i]) | |||
| 191 | + | n.children[i] = c | |||
| 192 | + | return c | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | // split cuts the owned node n at entry i: n keeps entries[:i], the returned | |||
| 196 | + | // entry is entries[i], and the returned node holds the rest. | |||
| 197 | + | func (b *builder) split(n *node, i int) (entry, *node) { | |||
| 198 | + | mid := n.entries[i] | |||
| 199 | + | r := &node{own: b.own, entries: slices.Clone(n.entries[i+1:])} | |||
| 200 | + | clear(n.entries[i:]) | |||
| 201 | + | n.entries = n.entries[:i] | |||
| 202 | + | if !n.leaf() { | |||
| 203 | + | r.children = slices.Clone(n.children[i+1:]) | |||
| 204 | + | clear(n.children[i+1:]) | |||
| 205 | + | n.children = n.children[:i+1] | |||
| 206 | + | } | |||
| 207 | + | return mid, r | |||
| 208 | + | } | |||
| 209 | + | ||||
| 210 | + | // put sets key to value at version, replacing any existing entry. | |||
| 211 | + | func (b *builder) put(key, value []byte, version uint64) { | |||
| 212 | + | e := entry{key: key, value: value, version: version} | |||
| 213 | + | if b.root == nil { | |||
| 214 | + | b.root = &node{own: b.own, entries: []entry{e}} | |||
| 215 | + | b.n = 1 | |||
| 216 | + | return | |||
| 217 | + | } | |||
| 218 | + | b.root = b.mutable(b.root) | |||
| 219 | + | if len(b.root.entries) >= maxEntries { | |||
| 220 | + | mid, r := b.split(b.root, maxEntries/2) | |||
| 221 | + | b.root = &node{ | |||
| 222 | + | own: b.own, | |||
| 223 | + | entries: []entry{mid}, | |||
| 224 | + | children: []*node{b.root, r}, | |||
| 225 | + | } | |||
| 226 | + | } | |||
| 227 | + | if !b.insert(b.root, e) { | |||
| 228 | + | b.n++ | |||
| 229 | + | } | |||
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | // insert adds e below the owned, non-full node n and reports whether it | |||
| 233 | + | // replaced an existing entry. Full children are split on the way down. | |||
| 234 | + | func (b *builder) insert(n *node, e entry) (replaced bool) { | |||
| 235 | + | for { | |||
| 236 | + | i, found := n.find(e.key) | |||
| 237 | + | if found { | |||
| 238 | + | n.entries[i] = e | |||
| 239 | + | return true | |||
| 240 | + | } | |||
| 241 | + | if n.leaf() { | |||
| 242 | + | n.entries = slices.Insert(n.entries, i, e) | |||
| 243 | + | return false | |||
| 244 | + | } | |||
| 245 | + | c := b.mutableChild(n, i) | |||
| 246 | + | if len(c.entries) >= maxEntries { | |||
| 247 | + | mid, r := b.split(c, maxEntries/2) | |||
| 248 | + | n.entries = slices.Insert(n.entries, i, mid) | |||
| 249 | + | n.children = slices.Insert(n.children, i+1, r) | |||
| 250 | + | switch cmp := bytes.Compare(e.key, mid.key); { | |||
| 251 | + | case cmp == 0: | |||
| 252 | + | n.entries[i] = e | |||
| 253 | + | return true | |||
| 254 | + | case cmp > 0: | |||
| 255 | + | c = r | |||
| 256 | + | } | |||
| 257 | + | } | |||
| 258 | + | n = c | |||
| 259 | + | } | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | // delete removes key if present. | |||
| 263 | + | func (b *builder) delete(key []byte) { | |||
| 264 | + | if _, ok := lookup(b.root, key); !ok { | |||
| 265 | + | return | |||
| 266 | + | } | |||
| 267 | + | b.root = b.mutable(b.root) | |||
| 268 | + | b.remove(b.root, key, false) | |||
| 269 | + | b.n-- | |||
| 270 | + | switch { | |||
| 271 | + | case len(b.root.entries) > 0: | |||
| 272 | + | case b.root.leaf(): | |||
| 273 | + | b.root = nil | |||
| 274 | + | default: | |||
| 275 | + | b.root = b.root.children[0] | |||
| 276 | + | } | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | // remove deletes key (or, with max, the largest entry) from the subtree of the | |||
| 280 | + | // owned node n and returns the removed entry. The key must exist. Every child | |||
| 281 | + | // descended into is first guaranteed to hold more than minEntries entries. | |||
| 282 | + | func (b *builder) remove(n *node, key []byte, max bool) entry { | |||
| 283 | + | for { | |||
| 284 | + | var i int | |||
| 285 | + | var found bool | |||
| 286 | + | switch { | |||
| 287 | + | case max && n.leaf(): | |||
| 288 | + | i, found = len(n.entries)-1, true | |||
| 289 | + | case max: | |||
| 290 | + | i = len(n.entries) | |||
| 291 | + | default: | |||
| 292 | + | i, found = n.find(key) | |||
| 293 | + | } | |||
| 294 | + | if n.leaf() { | |||
| 295 | + | e := n.entries[i] | |||
| 296 | + | n.entries = slices.Delete(n.entries, i, i+1) | |||
| 297 | + | return e | |||
| 298 | + | } | |||
| 299 | + | if len(n.children[i].entries) <= minEntries { | |||
| 300 | + | b.growChild(n, i) | |||
| 301 | + | continue // the shape of n changed: search again | |||
| 302 | + | } | |||
| 303 | + | c := b.mutableChild(n, i) | |||
| 304 | + | if found { | |||
| 305 | + | // Replace the entry by its predecessor. | |||
| 306 | + | e := n.entries[i] | |||
| 307 | + | n.entries[i] = b.remove(c, nil, true) | |||
| 308 | + | return e | |||
| 309 | + | } | |||
| 310 | + | n = c | |||
| 311 | + | } | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | // growChild ensures n.children[i] has more than minEntries entries by | |||
| 315 | + | // borrowing from a sibling or merging with one. n must be owned. | |||
| 316 | + | func (b *builder) growChild(n *node, i int) { | |||
| 317 | + | switch { | |||
| 318 | + | case i > 0 && len(n.children[i-1].entries) > minEntries: | |||
| 319 | + | c := b.mutableChild(n, i) | |||
| 320 | + | l := b.mutableChild(n, i-1) | |||
| 321 | + | last := len(l.entries) - 1 | |||
| 322 | + | c.entries = slices.Insert(c.entries, 0, n.entries[i-1]) | |||
| 323 | + | n.entries[i-1] = l.entries[last] | |||
| 324 | + | l.entries[last] = entry{} | |||
| 325 | + | l.entries = l.entries[:last] | |||
| 326 | + | if !c.leaf() { | |||
| 327 | + | lc := len(l.children) - 1 | |||
| 328 | + | c.children = slices.Insert(c.children, 0, l.children[lc]) | |||
| 329 | + | l.children[lc] = nil | |||
| 330 | + | l.children = l.children[:lc] | |||
| 331 | + | } | |||
| 332 | + | case i < len(n.entries) && len(n.children[i+1].entries) > minEntries: | |||
| 333 | + | c := b.mutableChild(n, i) | |||
| 334 | + | r := b.mutableChild(n, i+1) | |||
| 335 | + | c.entries = append(c.entries, n.entries[i]) | |||
| 336 | + | n.entries[i] = r.entries[0] | |||
| 337 | + | r.entries = slices.Delete(r.entries, 0, 1) | |||
| 338 | + | if !c.leaf() { | |||
| 339 | + | c.children = append(c.children, r.children[0]) | |||
| 340 | + | r.children = slices.Delete(r.children, 0, 1) | |||
| 341 | + | } | |||
| 342 | + | default: | |||
| 343 | + | // Merge children i and i+1 around entry i (children i-1 and i | |||
| 344 | + | // around entry i-1 for the last child). | |||
| 345 | + | if i == len(n.entries) { | |||
| 346 | + | i-- | |||
| 347 | + | } | |||
| 348 | + | c := b.mutableChild(n, i) | |||
| 349 | + | r := n.children[i+1] // only read | |||
| 350 | + | c.entries = append(c.entries, n.entries[i]) | |||
| 351 | + | c.entries = append(c.entries, r.entries...) | |||
| 352 | + | c.children = append(c.children, r.children...) | |||
| 353 | + | n.entries = slices.Delete(n.entries, i, i+1) | |||
| 354 | + | n.children = slices.Delete(n.children, i+1, i+2) | |||
| 355 | + | } | |||
| 356 | + | } | |||
A tree_test.go +446 -0
| @@ -0,0 +1,446 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package kv | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "bytes" | |
| 7 | + | "fmt" | |
| 8 | + | "math/rand/v2" | |
| 9 | + | "slices" | |
| 10 | + | "testing" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // model is the reference implementation: a map plus a sort on demand. | |
| 14 | + | type model map[string]Item | |
| 15 | + | ||
| 16 | + | func (m model) clone() model { | |
| 17 | + | c := make(model, len(m)) | |
| 18 | + | for k, v := range m { | |
| 19 | + | c[k] = v | |
| 20 | + | } | |
| 21 | + | return c | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // sorted returns the keys of m in ascending order. | |
| 25 | + | func (m model) sorted() []string { | |
| 26 | + | ks := make([]string, 0, len(m)) | |
| 27 | + | for k := range m { | |
| 28 | + | ks = append(ks, k) | |
| 29 | + | } | |
| 30 | + | slices.Sort(ks) | |
| 31 | + | return ks | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // within returns the part of the sorted keys in [start, end); nil end = unbounded. | |
| 35 | + | func within(sorted []string, start, end []byte) []string { | |
| 36 | + | lo, _ := slices.BinarySearch(sorted, string(start)) | |
| 37 | + | hi := len(sorted) | |
| 38 | + | if end != nil { | |
| 39 | + | hi, _ = slices.BinarySearch(sorted, string(end)) | |
| 40 | + | } | |
| 41 | + | if hi < lo { | |
| 42 | + | return nil | |
| 43 | + | } | |
| 44 | + | return sorted[lo:hi] | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | type kvPair struct { | |
| 48 | + | key string | |
| 49 | + | item Item | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func collect(seq func(func([]byte, Item) bool)) []kvPair { | |
| 53 | + | var out []kvPair | |
| 54 | + | seq(func(k []byte, it Item) bool { | |
| 55 | + | out = append(out, kvPair{string(k), it}) | |
| 56 | + | return true | |
| 57 | + | }) | |
| 58 | + | return out | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func equalItem(a, b Item) bool { | |
| 62 | + | return a.Version == b.Version && bytes.Equal(a.Value, b.Value) | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // checkStructure verifies the B-tree invariants: ordering, fill bounds, | |
| 66 | + | // uniform leaf depth and the entry count. | |
| 67 | + | func checkStructure(t *testing.T, tr tree) { | |
| 68 | + | t.Helper() | |
| 69 | + | if tr.root == nil { | |
| 70 | + | if tr.n != 0 { | |
| 71 | + | t.Fatalf("empty root but n=%d", tr.n) | |
| 72 | + | } | |
| 73 | + | return | |
| 74 | + | } | |
| 75 | + | var count, leafDepth int | |
| 76 | + | var prev []byte | |
| 77 | + | var walk func(n *node, depth int, isRoot bool) | |
| 78 | + | walk = func(n *node, depth int, isRoot bool) { | |
| 79 | + | if len(n.entries) == 0 || len(n.entries) > maxEntries || (!isRoot && len(n.entries) < minEntries) { | |
| 80 | + | t.Fatalf("node with %d entries at depth %d", len(n.entries), depth) | |
| 81 | + | } | |
| 82 | + | if n.leaf() { | |
| 83 | + | if leafDepth == 0 { | |
| 84 | + | leafDepth = depth | |
| 85 | + | } else if leafDepth != depth { | |
| 86 | + | t.Fatalf("leaves at depths %d and %d", leafDepth, depth) | |
| 87 | + | } | |
| 88 | + | } else if len(n.children) != len(n.entries)+1 { | |
| 89 | + | t.Fatalf("%d entries but %d children", len(n.entries), len(n.children)) | |
| 90 | + | } | |
| 91 | + | for i, e := range n.entries { | |
| 92 | + | if !n.leaf() { | |
| 93 | + | walk(n.children[i], depth+1, false) | |
| 94 | + | } | |
| 95 | + | if prev != nil && bytes.Compare(prev, e.key) >= 0 { | |
| 96 | + | t.Fatalf("keys out of order: %x then %x", prev, e.key) | |
| 97 | + | } | |
| 98 | + | prev = e.key | |
| 99 | + | count++ | |
| 100 | + | } | |
| 101 | + | if !n.leaf() { | |
| 102 | + | walk(n.children[len(n.entries)], depth+1, false) | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | walk(tr.root, 1, true) | |
| 106 | + | if count != tr.n { | |
| 107 | + | t.Fatalf("tree has %d entries, n=%d", count, tr.n) | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | // checkAgainst compares the tree with the model through get, full scan, | |
| 112 | + | // random ranges and random prefixes. | |
| 113 | + | func checkAgainst(t *testing.T, rng *rand.Rand, tr tree, m model, genKey func() []byte) { | |
| 114 | + | t.Helper() | |
| 115 | + | checkStructure(t, tr) | |
| 116 | + | if tr.len() != len(m) { | |
| 117 | + | t.Fatalf("len = %d, want %d", tr.len(), len(m)) | |
| 118 | + | } | |
| 119 | + | s := newSnapshot(tr, 7) | |
| 120 | + | if s.Len() != len(m) || s.Seq() != 7 { | |
| 121 | + | t.Fatalf("snapshot Len/Seq = %d/%d", s.Len(), s.Seq()) | |
| 122 | + | } | |
| 123 | + | sorted := m.sorted() | |
| 124 | + | verify := func(what string, got []kvPair, start, end []byte) { | |
| 125 | + | t.Helper() | |
| 126 | + | want := within(sorted, start, end) | |
| 127 | + | if len(got) != len(want) { | |
| 128 | + | t.Fatalf("%s: got %d items, want %d", what, len(got), len(want)) | |
| 129 | + | } | |
| 130 | + | for i, k := range want { | |
| 131 | + | if got[i].key != k || !equalItem(got[i].item, m[k]) { | |
| 132 | + | t.Fatalf("%s: item %d = %x, want %x", what, i, got[i].key, k) | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | verify("full scan", collect(s.Range(nil, nil)), nil, nil) | |
| 137 | + | ||
| 138 | + | for range 20 { | |
| 139 | + | k := genKey() | |
| 140 | + | got, ok := s.Get(k) | |
| 141 | + | want, wok := m[string(k)] | |
| 142 | + | if ok != wok || (ok && !equalItem(got, want)) { | |
| 143 | + | t.Fatalf("Get(%x) = %v,%v want %v,%v", k, got, ok, want, wok) | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | for k, want := range m { | |
| 147 | + | if got, ok := s.Get([]byte(k)); !ok || !equalItem(got, want) { | |
| 148 | + | t.Fatalf("Get(%x) = %v,%v want %v", k, got, ok, want) | |
| 149 | + | } | |
| 150 | + | break // one arbitrary present key is enough per check | |
| 151 | + | } | |
| 152 | + | for range 10 { | |
| 153 | + | a, b := genKey(), genKey() | |
| 154 | + | if bytes.Compare(a, b) > 0 { | |
| 155 | + | a, b = b, a | |
| 156 | + | } | |
| 157 | + | verify("range", collect(s.Range(a, b)), a, b) | |
| 158 | + | verify("range open end", collect(s.Range(a, nil)), a, nil) | |
| 159 | + | verify("range open start", collect(s.Range(nil, b)), nil, b) | |
| 160 | + | } | |
| 161 | + | for range 10 { | |
| 162 | + | p := genKey() | |
| 163 | + | p = p[:rng.IntN(len(p)+1)] // may be empty | |
| 164 | + | var want []string | |
| 165 | + | for _, k := range sorted { | |
| 166 | + | if bytes.HasPrefix([]byte(k), p) { | |
| 167 | + | want = append(want, k) | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | got := collect(s.Prefix(p)) | |
| 171 | + | if len(got) != len(want) { | |
| 172 | + | t.Fatalf("prefix %x: got %d items, want %d", p, len(got), len(want)) | |
| 173 | + | } | |
| 174 | + | for i, k := range want { | |
| 175 | + | if got[i].key != k { | |
| 176 | + | t.Fatalf("prefix %x: item %d = %x, want %x", p, i, got[i].key, k) | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | // Early break stops the walk. | |
| 181 | + | seen := 0 | |
| 182 | + | for range s.Range(nil, nil) { | |
| 183 | + | seen++ | |
| 184 | + | if seen == 3 { | |
| 185 | + | break | |
| 186 | + | } | |
| 187 | + | } | |
| 188 | + | if seen != min(3, len(m)) { | |
| 189 | + | t.Fatalf("early break saw %d items", seen) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | type frozen struct { | |
| 194 | + | tr tree | |
| 195 | + | m model | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | func TestTreeModel(t *testing.T) { | |
| 199 | + | seeds, ops := 50, 10000 | |
| 200 | + | if testing.Short() { | |
| 201 | + | seeds, ops = 8, 2000 | |
| 202 | + | } | |
| 203 | + | alphabet := []byte{0, 1, 2, 'a', 'b', 'c', 0x7F, 0x80, 0xFD, 0xFE, 0xFF, 'x', 'y', 'z', 0x10, 0x20} | |
| 204 | + | for seed := range seeds { | |
| 205 | + | t.Run(fmt.Sprintf("seed=%d", seed), func(t *testing.T) { | |
| 206 | + | rng := rand.New(rand.NewPCG(uint64(seed), 0xe131)) | |
| 207 | + | alpha := alphabet[:2+seed%(len(alphabet)-1)] | |
| 208 | + | genKey := func() []byte { | |
| 209 | + | k := make([]byte, 1+rng.IntN(5)) | |
| 210 | + | for i := range k { | |
| 211 | + | k[i] = alpha[rng.IntN(len(alpha))] | |
| 212 | + | } | |
| 213 | + | return k | |
| 214 | + | } | |
| 215 | + | var ( | |
| 216 | + | tr tree | |
| 217 | + | m = model{} | |
| 218 | + | version uint64 | |
| 219 | + | history []frozen | |
| 220 | + | ) | |
| 221 | + | apply := func(del bool, k []byte, put func(k, v []byte, ver uint64), rm func(k []byte)) { | |
| 222 | + | version++ | |
| 223 | + | if del { | |
| 224 | + | rm(k) | |
| 225 | + | delete(m, string(k)) | |
| 226 | + | return | |
| 227 | + | } | |
| 228 | + | v := make([]byte, rng.IntN(8)) | |
| 229 | + | for i := range v { | |
| 230 | + | v[i] = byte(rng.IntN(256)) | |
| 231 | + | } | |
| 232 | + | put(k, v, version) | |
| 233 | + | m[string(k)] = Item{Value: v, Version: version} | |
| 234 | + | } | |
| 235 | + | for done, next := 0, 0; done < ops; { | |
| 236 | + | if rng.IntN(5) == 0 { | |
| 237 | + | // A group of ops through one builder. | |
| 238 | + | b := tr.builder() | |
| 239 | + | n := 1 + rng.IntN(40) | |
| 240 | + | for range n { | |
| 241 | + | k := genKey() | |
| 242 | + | apply(rng.IntN(3) == 0, k, b.put, b.delete) | |
| 243 | + | if got, ok := b.get(k); ok != (m[string(k)].Version != 0) || (ok && !equalItem(got, m[string(k)])) { | |
| 244 | + | t.Fatalf("seed %d: builder.get mismatch", seed) | |
| 245 | + | } | |
| 246 | + | } | |
| 247 | + | if b.len() != len(m) { | |
| 248 | + | t.Fatalf("seed %d: builder.len = %d, want %d", seed, b.len(), len(m)) | |
| 249 | + | } | |
| 250 | + | tr = b.freeze() | |
| 251 | + | done += n | |
| 252 | + | } else { | |
| 253 | + | k := genKey() | |
| 254 | + | apply(rng.IntN(3) == 0, k, func(k, v []byte, ver uint64) { tr = tr.put(k, v, ver) }, func(k []byte) { tr = tr.delete(k) }) | |
| 255 | + | done++ | |
| 256 | + | } | |
| 257 | + | if done >= next { | |
| 258 | + | next = done + 500 + rng.IntN(500) | |
| 259 | + | checkAgainst(t, rng, tr, m, genKey) | |
| 260 | + | history = append(history, frozen{tr, m.clone()}) | |
| 261 | + | } | |
| 262 | + | } | |
| 263 | + | checkAgainst(t, rng, tr, m, genKey) | |
| 264 | + | // Persistence: every older tree still equals its model. | |
| 265 | + | for _, h := range history { | |
| 266 | + | checkAgainst(t, rng, h.tr, h.m, genKey) | |
| 267 | + | } | |
| 268 | + | // Drain the tree in random order, with and without a builder. | |
| 269 | + | keys := m.sorted() | |
| 270 | + | rng.Shuffle(len(keys), func(i, j int) { keys[i], keys[j] = keys[j], keys[i] }) | |
| 271 | + | for i, k := range keys { | |
| 272 | + | if i%2 == 0 { | |
| 273 | + | tr = tr.delete([]byte(k)) | |
| 274 | + | } else { | |
| 275 | + | b := tr.builder() | |
| 276 | + | b.delete([]byte(k)) | |
| 277 | + | b.delete([]byte(k)) // absent: no-op | |
| 278 | + | tr = b.freeze() | |
| 279 | + | } | |
| 280 | + | delete(m, k) | |
| 281 | + | if i%97 == 0 { | |
| 282 | + | checkStructure(t, tr) | |
| 283 | + | } | |
| 284 | + | } | |
| 285 | + | checkAgainst(t, rng, tr, m, genKey) | |
| 286 | + | if tr.root != nil || tr.len() != 0 { | |
| 287 | + | t.Fatalf("seed %d: tree not empty after draining", seed) | |
| 288 | + | } | |
| 289 | + | // Draining must not have disturbed the older trees. | |
| 290 | + | for _, h := range history { | |
| 291 | + | checkAgainst(t, rng, h.tr, h.m, genKey) | |
| 292 | + | } | |
| 293 | + | }) | |
| 294 | + | } | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | // TestBuilderReuse checks that nodes of a frozen tree are never mutated by | |
| 298 | + | // later builder operations, and that nodes owned by the builder are reused. | |
| 299 | + | func TestBuilderReuse(t *testing.T) { | |
| 300 | + | var base tree | |
| 301 | + | for i := range 500 { | |
| 302 | + | base = base.put(fmt.Appendf(nil, "k%04d", i), []byte("v"), 1) | |
| 303 | + | } | |
| 304 | + | b := base.builder() | |
| 305 | + | b.put([]byte("k0100"), []byte("new"), 2) | |
| 306 | + | first := b.root | |
| 307 | + | b.put([]byte("k0101"), []byte("new"), 2) | |
| 308 | + | if b.root != first { | |
| 309 | + | t.Fatal("builder copied its own root twice") | |
| 310 | + | } | |
| 311 | + | frozenTree := b.freeze() | |
| 312 | + | b.put([]byte("k0102"), []byte("again"), 3) | |
| 313 | + | if b.root == frozenTree.root { | |
| 314 | + | t.Fatal("builder mutated the root of a frozen tree") | |
| 315 | + | } | |
| 316 | + | if it, _ := frozenTree.get([]byte("k0102")); string(it.Value) != "v" { | |
| 317 | + | t.Fatalf("frozen tree changed: %q", it.Value) | |
| 318 | + | } | |
| 319 | + | if it, _ := base.get([]byte("k0100")); string(it.Value) != "v" || it.Version != 1 { | |
| 320 | + | t.Fatalf("base tree changed: %v", it) | |
| 321 | + | } | |
| 322 | + | checkStructure(t, base) | |
| 323 | + | checkStructure(t, frozenTree) | |
| 324 | + | checkStructure(t, b.freeze()) | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | func TestPrefixEnd(t *testing.T) { | |
| 328 | + | tests := []struct{ in, want []byte }{ | |
| 329 | + | {nil, nil}, | |
| 330 | + | {[]byte{}, nil}, | |
| 331 | + | {[]byte("a"), []byte("b")}, | |
| 332 | + | {[]byte("ab"), []byte("ac")}, | |
| 333 | + | {[]byte{'a', 0xFF}, []byte("b")}, | |
| 334 | + | {[]byte{'a', 0xFF, 0xFF}, []byte("b")}, | |
| 335 | + | {[]byte{0xFF}, nil}, | |
| 336 | + | {[]byte{0xFF, 0xFF}, nil}, | |
| 337 | + | } | |
| 338 | + | for _, tt := range tests { | |
| 339 | + | if got := prefixEnd(tt.in); !bytes.Equal(got, tt.want) || (got == nil) != (tt.want == nil) { | |
| 340 | + | t.Errorf("prefixEnd(%x) = %x, want %x", tt.in, got, tt.want) | |
| 341 | + | } | |
| 342 | + | } | |
| 343 | + | } | |
| 344 | + | ||
| 345 | + | func TestPrefixAllFF(t *testing.T) { | |
| 346 | + | var tr tree | |
| 347 | + | for i, k := range [][]byte{{0xFE}, {0xFF}, {0xFF, 0x00}, {0xFF, 0xFF, 0xFF}} { | |
| 348 | + | tr = tr.put(k, nil, uint64(i+1)) | |
| 349 | + | } | |
| 350 | + | s := newSnapshot(tr, 4) | |
| 351 | + | if got := collect(s.Prefix([]byte{0xFF})); len(got) != 3 { | |
| 352 | + | t.Fatalf("prefix FF: %d items, want 3", len(got)) | |
| 353 | + | } | |
| 354 | + | if got := collect(s.Prefix(nil)); len(got) != 4 { | |
| 355 | + | t.Fatalf("empty prefix: %d items, want 4", len(got)) | |
| 356 | + | } | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | func benchTree(n int) (*Snapshot, [][]byte) { | |
| 360 | + | keys := make([][]byte, n) | |
| 361 | + | b := tree{}.builder() | |
| 362 | + | for i := range keys { | |
| 363 | + | keys[i] = fmt.Appendf(nil, "key/%08d", i) | |
| 364 | + | b.put(keys[i], []byte("value"), uint64(i+1)) | |
| 365 | + | } | |
| 366 | + | return newSnapshot(b.freeze(), uint64(n)), keys | |
| 367 | + | } | |
| 368 | + | ||
| 369 | + | func BenchmarkSnapshotGet(b *testing.B) { | |
| 370 | + | s, keys := benchTree(100_000) | |
| 371 | + | b.ReportAllocs() | |
| 372 | + | b.ResetTimer() | |
| 373 | + | for i := 0; b.Loop(); i++ { | |
| 374 | + | if _, ok := s.Get(keys[(i*7919)%len(keys)]); !ok { | |
| 375 | + | b.Fatal("missing key") | |
| 376 | + | } | |
| 377 | + | } | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | func BenchmarkRange(b *testing.B) { | |
| 381 | + | s, _ := benchTree(100_000) | |
| 382 | + | b.ReportAllocs() | |
| 383 | + | for b.Loop() { | |
| 384 | + | n := 0 | |
| 385 | + | for range s.Range(nil, nil) { | |
| 386 | + | n++ | |
| 387 | + | } | |
| 388 | + | if n != 100_000 { | |
| 389 | + | b.Fatalf("saw %d items", n) | |
| 390 | + | } | |
| 391 | + | } | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | func BenchmarkTreePut(b *testing.B) { | |
| 395 | + | _, keys := benchTree(100_000) | |
| 396 | + | b.ReportAllocs() | |
| 397 | + | var tr tree | |
| 398 | + | for i := 0; b.Loop(); i++ { | |
| 399 | + | tr = tr.put(keys[(i*7919)%len(keys)], nil, uint64(i)) | |
| 400 | + | } | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | func BenchmarkBuilderPut(b *testing.B) { | |
| 404 | + | _, keys := benchTree(100_000) | |
| 405 | + | b.ReportAllocs() | |
| 406 | + | bl := tree{}.builder() | |
| 407 | + | for i := 0; b.Loop(); i++ { | |
| 408 | + | bl.put(keys[(i*7919)%len(keys)], nil, uint64(i)) | |
| 409 | + | if i%64 == 63 { | |
| 410 | + | bl.freeze() | |
| 411 | + | } | |
| 412 | + | } | |
| 413 | + | } | |
| 414 | + | ||
| 415 | + | // TestTreeDeep builds a tree of several levels, then deletes ascending, | |
| 416 | + | // exercising merges and borrows on internal nodes. | |
| 417 | + | func TestTreeDeep(t *testing.T) { | |
| 418 | + | const n = 20000 | |
| 419 | + | var tr tree | |
| 420 | + | for i := range n { | |
| 421 | + | tr = tr.put(fmt.Appendf(nil, "%06d", i), nil, 1) | |
| 422 | + | } | |
| 423 | + | checkStructure(t, tr) | |
| 424 | + | depth := 0 | |
| 425 | + | for x := tr.root; x != nil; depth++ { | |
| 426 | + | if x.leaf() { | |
| 427 | + | depth++ | |
| 428 | + | break | |
| 429 | + | } | |
| 430 | + | x = x.children[0] | |
| 431 | + | } | |
| 432 | + | if depth < 3 { | |
| 433 | + | t.Fatalf("depth = %d, want >= 3", depth) | |
| 434 | + | } | |
| 435 | + | full := tr | |
| 436 | + | for i := range n { | |
| 437 | + | tr = tr.delete(fmt.Appendf(nil, "%06d", i)) | |
| 438 | + | if i%251 == 0 { | |
| 439 | + | checkStructure(t, tr) | |
| 440 | + | } | |
| 441 | + | } | |
| 442 | + | if tr.len() != 0 || tr.root != nil { | |
| 443 | + | t.Fatalf("tree not empty: len=%d", tr.len()) | |
| 444 | + | } | |
| 445 | + | checkStructure(t, full) | |
| 446 | + | } | |
| @@ -0,0 +1,446 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package kv | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "bytes" | |||
| 7 | + | "fmt" | |||
| 8 | + | "math/rand/v2" | |||
| 9 | + | "slices" | |||
| 10 | + | "testing" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // model is the reference implementation: a map plus a sort on demand. | |||
| 14 | + | type model map[string]Item | |||
| 15 | + | ||||
| 16 | + | func (m model) clone() model { | |||
| 17 | + | c := make(model, len(m)) | |||
| 18 | + | for k, v := range m { | |||
| 19 | + | c[k] = v | |||
| 20 | + | } | |||
| 21 | + | return c | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // sorted returns the keys of m in ascending order. | |||
| 25 | + | func (m model) sorted() []string { | |||
| 26 | + | ks := make([]string, 0, len(m)) | |||
| 27 | + | for k := range m { | |||
| 28 | + | ks = append(ks, k) | |||
| 29 | + | } | |||
| 30 | + | slices.Sort(ks) | |||
| 31 | + | return ks | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // within returns the part of the sorted keys in [start, end); nil end = unbounded. | |||
| 35 | + | func within(sorted []string, start, end []byte) []string { | |||
| 36 | + | lo, _ := slices.BinarySearch(sorted, string(start)) | |||
| 37 | + | hi := len(sorted) | |||
| 38 | + | if end != nil { | |||
| 39 | + | hi, _ = slices.BinarySearch(sorted, string(end)) | |||
| 40 | + | } | |||
| 41 | + | if hi < lo { | |||
| 42 | + | return nil | |||
| 43 | + | } | |||
| 44 | + | return sorted[lo:hi] | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | type kvPair struct { | |||
| 48 | + | key string | |||
| 49 | + | item Item | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func collect(seq func(func([]byte, Item) bool)) []kvPair { | |||
| 53 | + | var out []kvPair | |||
| 54 | + | seq(func(k []byte, it Item) bool { | |||
| 55 | + | out = append(out, kvPair{string(k), it}) | |||
| 56 | + | return true | |||
| 57 | + | }) | |||
| 58 | + | return out | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func equalItem(a, b Item) bool { | |||
| 62 | + | return a.Version == b.Version && bytes.Equal(a.Value, b.Value) | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | // checkStructure verifies the B-tree invariants: ordering, fill bounds, | |||
| 66 | + | // uniform leaf depth and the entry count. | |||
| 67 | + | func checkStructure(t *testing.T, tr tree) { | |||
| 68 | + | t.Helper() | |||
| 69 | + | if tr.root == nil { | |||
| 70 | + | if tr.n != 0 { | |||
| 71 | + | t.Fatalf("empty root but n=%d", tr.n) | |||
| 72 | + | } | |||
| 73 | + | return | |||
| 74 | + | } | |||
| 75 | + | var count, leafDepth int | |||
| 76 | + | var prev []byte | |||
| 77 | + | var walk func(n *node, depth int, isRoot bool) | |||
| 78 | + | walk = func(n *node, depth int, isRoot bool) { | |||
| 79 | + | if len(n.entries) == 0 || len(n.entries) > maxEntries || (!isRoot && len(n.entries) < minEntries) { | |||
| 80 | + | t.Fatalf("node with %d entries at depth %d", len(n.entries), depth) | |||
| 81 | + | } | |||
| 82 | + | if n.leaf() { | |||
| 83 | + | if leafDepth == 0 { | |||
| 84 | + | leafDepth = depth | |||
| 85 | + | } else if leafDepth != depth { | |||
| 86 | + | t.Fatalf("leaves at depths %d and %d", leafDepth, depth) | |||
| 87 | + | } | |||
| 88 | + | } else if len(n.children) != len(n.entries)+1 { | |||
| 89 | + | t.Fatalf("%d entries but %d children", len(n.entries), len(n.children)) | |||
| 90 | + | } | |||
| 91 | + | for i, e := range n.entries { | |||
| 92 | + | if !n.leaf() { | |||
| 93 | + | walk(n.children[i], depth+1, false) | |||
| 94 | + | } | |||
| 95 | + | if prev != nil && bytes.Compare(prev, e.key) >= 0 { | |||
| 96 | + | t.Fatalf("keys out of order: %x then %x", prev, e.key) | |||
| 97 | + | } | |||
| 98 | + | prev = e.key | |||
| 99 | + | count++ | |||
| 100 | + | } | |||
| 101 | + | if !n.leaf() { | |||
| 102 | + | walk(n.children[len(n.entries)], depth+1, false) | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | walk(tr.root, 1, true) | |||
| 106 | + | if count != tr.n { | |||
| 107 | + | t.Fatalf("tree has %d entries, n=%d", count, tr.n) | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | // checkAgainst compares the tree with the model through get, full scan, | |||
| 112 | + | // random ranges and random prefixes. | |||
| 113 | + | func checkAgainst(t *testing.T, rng *rand.Rand, tr tree, m model, genKey func() []byte) { | |||
| 114 | + | t.Helper() | |||
| 115 | + | checkStructure(t, tr) | |||
| 116 | + | if tr.len() != len(m) { | |||
| 117 | + | t.Fatalf("len = %d, want %d", tr.len(), len(m)) | |||
| 118 | + | } | |||
| 119 | + | s := newSnapshot(tr, 7) | |||
| 120 | + | if s.Len() != len(m) || s.Seq() != 7 { | |||
| 121 | + | t.Fatalf("snapshot Len/Seq = %d/%d", s.Len(), s.Seq()) | |||
| 122 | + | } | |||
| 123 | + | sorted := m.sorted() | |||
| 124 | + | verify := func(what string, got []kvPair, start, end []byte) { | |||
| 125 | + | t.Helper() | |||
| 126 | + | want := within(sorted, start, end) | |||
| 127 | + | if len(got) != len(want) { | |||
| 128 | + | t.Fatalf("%s: got %d items, want %d", what, len(got), len(want)) | |||
| 129 | + | } | |||
| 130 | + | for i, k := range want { | |||
| 131 | + | if got[i].key != k || !equalItem(got[i].item, m[k]) { | |||
| 132 | + | t.Fatalf("%s: item %d = %x, want %x", what, i, got[i].key, k) | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | verify("full scan", collect(s.Range(nil, nil)), nil, nil) | |||
| 137 | + | ||||
| 138 | + | for range 20 { | |||
| 139 | + | k := genKey() | |||
| 140 | + | got, ok := s.Get(k) | |||
| 141 | + | want, wok := m[string(k)] | |||
| 142 | + | if ok != wok || (ok && !equalItem(got, want)) { | |||
| 143 | + | t.Fatalf("Get(%x) = %v,%v want %v,%v", k, got, ok, want, wok) | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | for k, want := range m { | |||
| 147 | + | if got, ok := s.Get([]byte(k)); !ok || !equalItem(got, want) { | |||
| 148 | + | t.Fatalf("Get(%x) = %v,%v want %v", k, got, ok, want) | |||
| 149 | + | } | |||
| 150 | + | break // one arbitrary present key is enough per check | |||
| 151 | + | } | |||
| 152 | + | for range 10 { | |||
| 153 | + | a, b := genKey(), genKey() | |||
| 154 | + | if bytes.Compare(a, b) > 0 { | |||
| 155 | + | a, b = b, a | |||
| 156 | + | } | |||
| 157 | + | verify("range", collect(s.Range(a, b)), a, b) | |||
| 158 | + | verify("range open end", collect(s.Range(a, nil)), a, nil) | |||
| 159 | + | verify("range open start", collect(s.Range(nil, b)), nil, b) | |||
| 160 | + | } | |||
| 161 | + | for range 10 { | |||
| 162 | + | p := genKey() | |||
| 163 | + | p = p[:rng.IntN(len(p)+1)] // may be empty | |||
| 164 | + | var want []string | |||
| 165 | + | for _, k := range sorted { | |||
| 166 | + | if bytes.HasPrefix([]byte(k), p) { | |||
| 167 | + | want = append(want, k) | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | got := collect(s.Prefix(p)) | |||
| 171 | + | if len(got) != len(want) { | |||
| 172 | + | t.Fatalf("prefix %x: got %d items, want %d", p, len(got), len(want)) | |||
| 173 | + | } | |||
| 174 | + | for i, k := range want { | |||
| 175 | + | if got[i].key != k { | |||
| 176 | + | t.Fatalf("prefix %x: item %d = %x, want %x", p, i, got[i].key, k) | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | } | |||
| 180 | + | // Early break stops the walk. | |||
| 181 | + | seen := 0 | |||
| 182 | + | for range s.Range(nil, nil) { | |||
| 183 | + | seen++ | |||
| 184 | + | if seen == 3 { | |||
| 185 | + | break | |||
| 186 | + | } | |||
| 187 | + | } | |||
| 188 | + | if seen != min(3, len(m)) { | |||
| 189 | + | t.Fatalf("early break saw %d items", seen) | |||
| 190 | + | } | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | type frozen struct { | |||
| 194 | + | tr tree | |||
| 195 | + | m model | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | func TestTreeModel(t *testing.T) { | |||
| 199 | + | seeds, ops := 50, 10000 | |||
| 200 | + | if testing.Short() { | |||
| 201 | + | seeds, ops = 8, 2000 | |||
| 202 | + | } | |||
| 203 | + | alphabet := []byte{0, 1, 2, 'a', 'b', 'c', 0x7F, 0x80, 0xFD, 0xFE, 0xFF, 'x', 'y', 'z', 0x10, 0x20} | |||
| 204 | + | for seed := range seeds { | |||
| 205 | + | t.Run(fmt.Sprintf("seed=%d", seed), func(t *testing.T) { | |||
| 206 | + | rng := rand.New(rand.NewPCG(uint64(seed), 0xe131)) | |||
| 207 | + | alpha := alphabet[:2+seed%(len(alphabet)-1)] | |||
| 208 | + | genKey := func() []byte { | |||
| 209 | + | k := make([]byte, 1+rng.IntN(5)) | |||
| 210 | + | for i := range k { | |||
| 211 | + | k[i] = alpha[rng.IntN(len(alpha))] | |||
| 212 | + | } | |||
| 213 | + | return k | |||
| 214 | + | } | |||
| 215 | + | var ( | |||
| 216 | + | tr tree | |||
| 217 | + | m = model{} | |||
| 218 | + | version uint64 | |||
| 219 | + | history []frozen | |||
| 220 | + | ) | |||
| 221 | + | apply := func(del bool, k []byte, put func(k, v []byte, ver uint64), rm func(k []byte)) { | |||
| 222 | + | version++ | |||
| 223 | + | if del { | |||
| 224 | + | rm(k) | |||
| 225 | + | delete(m, string(k)) | |||
| 226 | + | return | |||
| 227 | + | } | |||
| 228 | + | v := make([]byte, rng.IntN(8)) | |||
| 229 | + | for i := range v { | |||
| 230 | + | v[i] = byte(rng.IntN(256)) | |||
| 231 | + | } | |||
| 232 | + | put(k, v, version) | |||
| 233 | + | m[string(k)] = Item{Value: v, Version: version} | |||
| 234 | + | } | |||
| 235 | + | for done, next := 0, 0; done < ops; { | |||
| 236 | + | if rng.IntN(5) == 0 { | |||
| 237 | + | // A group of ops through one builder. | |||
| 238 | + | b := tr.builder() | |||
| 239 | + | n := 1 + rng.IntN(40) | |||
| 240 | + | for range n { | |||
| 241 | + | k := genKey() | |||
| 242 | + | apply(rng.IntN(3) == 0, k, b.put, b.delete) | |||
| 243 | + | if got, ok := b.get(k); ok != (m[string(k)].Version != 0) || (ok && !equalItem(got, m[string(k)])) { | |||
| 244 | + | t.Fatalf("seed %d: builder.get mismatch", seed) | |||
| 245 | + | } | |||
| 246 | + | } | |||
| 247 | + | if b.len() != len(m) { | |||
| 248 | + | t.Fatalf("seed %d: builder.len = %d, want %d", seed, b.len(), len(m)) | |||
| 249 | + | } | |||
| 250 | + | tr = b.freeze() | |||
| 251 | + | done += n | |||
| 252 | + | } else { | |||
| 253 | + | k := genKey() | |||
| 254 | + | apply(rng.IntN(3) == 0, k, func(k, v []byte, ver uint64) { tr = tr.put(k, v, ver) }, func(k []byte) { tr = tr.delete(k) }) | |||
| 255 | + | done++ | |||
| 256 | + | } | |||
| 257 | + | if done >= next { | |||
| 258 | + | next = done + 500 + rng.IntN(500) | |||
| 259 | + | checkAgainst(t, rng, tr, m, genKey) | |||
| 260 | + | history = append(history, frozen{tr, m.clone()}) | |||
| 261 | + | } | |||
| 262 | + | } | |||
| 263 | + | checkAgainst(t, rng, tr, m, genKey) | |||
| 264 | + | // Persistence: every older tree still equals its model. | |||
| 265 | + | for _, h := range history { | |||
| 266 | + | checkAgainst(t, rng, h.tr, h.m, genKey) | |||
| 267 | + | } | |||
| 268 | + | // Drain the tree in random order, with and without a builder. | |||
| 269 | + | keys := m.sorted() | |||
| 270 | + | rng.Shuffle(len(keys), func(i, j int) { keys[i], keys[j] = keys[j], keys[i] }) | |||
| 271 | + | for i, k := range keys { | |||
| 272 | + | if i%2 == 0 { | |||
| 273 | + | tr = tr.delete([]byte(k)) | |||
| 274 | + | } else { | |||
| 275 | + | b := tr.builder() | |||
| 276 | + | b.delete([]byte(k)) | |||
| 277 | + | b.delete([]byte(k)) // absent: no-op | |||
| 278 | + | tr = b.freeze() | |||
| 279 | + | } | |||
| 280 | + | delete(m, k) | |||
| 281 | + | if i%97 == 0 { | |||
| 282 | + | checkStructure(t, tr) | |||
| 283 | + | } | |||
| 284 | + | } | |||
| 285 | + | checkAgainst(t, rng, tr, m, genKey) | |||
| 286 | + | if tr.root != nil || tr.len() != 0 { | |||
| 287 | + | t.Fatalf("seed %d: tree not empty after draining", seed) | |||
| 288 | + | } | |||
| 289 | + | // Draining must not have disturbed the older trees. | |||
| 290 | + | for _, h := range history { | |||
| 291 | + | checkAgainst(t, rng, h.tr, h.m, genKey) | |||
| 292 | + | } | |||
| 293 | + | }) | |||
| 294 | + | } | |||
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | // TestBuilderReuse checks that nodes of a frozen tree are never mutated by | |||
| 298 | + | // later builder operations, and that nodes owned by the builder are reused. | |||
| 299 | + | func TestBuilderReuse(t *testing.T) { | |||
| 300 | + | var base tree | |||
| 301 | + | for i := range 500 { | |||
| 302 | + | base = base.put(fmt.Appendf(nil, "k%04d", i), []byte("v"), 1) | |||
| 303 | + | } | |||
| 304 | + | b := base.builder() | |||
| 305 | + | b.put([]byte("k0100"), []byte("new"), 2) | |||
| 306 | + | first := b.root | |||
| 307 | + | b.put([]byte("k0101"), []byte("new"), 2) | |||
| 308 | + | if b.root != first { | |||
| 309 | + | t.Fatal("builder copied its own root twice") | |||
| 310 | + | } | |||
| 311 | + | frozenTree := b.freeze() | |||
| 312 | + | b.put([]byte("k0102"), []byte("again"), 3) | |||
| 313 | + | if b.root == frozenTree.root { | |||
| 314 | + | t.Fatal("builder mutated the root of a frozen tree") | |||
| 315 | + | } | |||
| 316 | + | if it, _ := frozenTree.get([]byte("k0102")); string(it.Value) != "v" { | |||
| 317 | + | t.Fatalf("frozen tree changed: %q", it.Value) | |||
| 318 | + | } | |||
| 319 | + | if it, _ := base.get([]byte("k0100")); string(it.Value) != "v" || it.Version != 1 { | |||
| 320 | + | t.Fatalf("base tree changed: %v", it) | |||
| 321 | + | } | |||
| 322 | + | checkStructure(t, base) | |||
| 323 | + | checkStructure(t, frozenTree) | |||
| 324 | + | checkStructure(t, b.freeze()) | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | func TestPrefixEnd(t *testing.T) { | |||
| 328 | + | tests := []struct{ in, want []byte }{ | |||
| 329 | + | {nil, nil}, | |||
| 330 | + | {[]byte{}, nil}, | |||
| 331 | + | {[]byte("a"), []byte("b")}, | |||
| 332 | + | {[]byte("ab"), []byte("ac")}, | |||
| 333 | + | {[]byte{'a', 0xFF}, []byte("b")}, | |||
| 334 | + | {[]byte{'a', 0xFF, 0xFF}, []byte("b")}, | |||
| 335 | + | {[]byte{0xFF}, nil}, | |||
| 336 | + | {[]byte{0xFF, 0xFF}, nil}, | |||
| 337 | + | } | |||
| 338 | + | for _, tt := range tests { | |||
| 339 | + | if got := prefixEnd(tt.in); !bytes.Equal(got, tt.want) || (got == nil) != (tt.want == nil) { | |||
| 340 | + | t.Errorf("prefixEnd(%x) = %x, want %x", tt.in, got, tt.want) | |||
| 341 | + | } | |||
| 342 | + | } | |||
| 343 | + | } | |||
| 344 | + | ||||
| 345 | + | func TestPrefixAllFF(t *testing.T) { | |||
| 346 | + | var tr tree | |||
| 347 | + | for i, k := range [][]byte{{0xFE}, {0xFF}, {0xFF, 0x00}, {0xFF, 0xFF, 0xFF}} { | |||
| 348 | + | tr = tr.put(k, nil, uint64(i+1)) | |||
| 349 | + | } | |||
| 350 | + | s := newSnapshot(tr, 4) | |||
| 351 | + | if got := collect(s.Prefix([]byte{0xFF})); len(got) != 3 { | |||
| 352 | + | t.Fatalf("prefix FF: %d items, want 3", len(got)) | |||
| 353 | + | } | |||
| 354 | + | if got := collect(s.Prefix(nil)); len(got) != 4 { | |||
| 355 | + | t.Fatalf("empty prefix: %d items, want 4", len(got)) | |||
| 356 | + | } | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | func benchTree(n int) (*Snapshot, [][]byte) { | |||
| 360 | + | keys := make([][]byte, n) | |||
| 361 | + | b := tree{}.builder() | |||
| 362 | + | for i := range keys { | |||
| 363 | + | keys[i] = fmt.Appendf(nil, "key/%08d", i) | |||
| 364 | + | b.put(keys[i], []byte("value"), uint64(i+1)) | |||
| 365 | + | } | |||
| 366 | + | return newSnapshot(b.freeze(), uint64(n)), keys | |||
| 367 | + | } | |||
| 368 | + | ||||
| 369 | + | func BenchmarkSnapshotGet(b *testing.B) { | |||
| 370 | + | s, keys := benchTree(100_000) | |||
| 371 | + | b.ReportAllocs() | |||
| 372 | + | b.ResetTimer() | |||
| 373 | + | for i := 0; b.Loop(); i++ { | |||
| 374 | + | if _, ok := s.Get(keys[(i*7919)%len(keys)]); !ok { | |||
| 375 | + | b.Fatal("missing key") | |||
| 376 | + | } | |||
| 377 | + | } | |||
| 378 | + | } | |||
| 379 | + | ||||
| 380 | + | func BenchmarkRange(b *testing.B) { | |||
| 381 | + | s, _ := benchTree(100_000) | |||
| 382 | + | b.ReportAllocs() | |||
| 383 | + | for b.Loop() { | |||
| 384 | + | n := 0 | |||
| 385 | + | for range s.Range(nil, nil) { | |||
| 386 | + | n++ | |||
| 387 | + | } | |||
| 388 | + | if n != 100_000 { | |||
| 389 | + | b.Fatalf("saw %d items", n) | |||
| 390 | + | } | |||
| 391 | + | } | |||
| 392 | + | } | |||
| 393 | + | ||||
| 394 | + | func BenchmarkTreePut(b *testing.B) { | |||
| 395 | + | _, keys := benchTree(100_000) | |||
| 396 | + | b.ReportAllocs() | |||
| 397 | + | var tr tree | |||
| 398 | + | for i := 0; b.Loop(); i++ { | |||
| 399 | + | tr = tr.put(keys[(i*7919)%len(keys)], nil, uint64(i)) | |||
| 400 | + | } | |||
| 401 | + | } | |||
| 402 | + | ||||
| 403 | + | func BenchmarkBuilderPut(b *testing.B) { | |||
| 404 | + | _, keys := benchTree(100_000) | |||
| 405 | + | b.ReportAllocs() | |||
| 406 | + | bl := tree{}.builder() | |||
| 407 | + | for i := 0; b.Loop(); i++ { | |||
| 408 | + | bl.put(keys[(i*7919)%len(keys)], nil, uint64(i)) | |||
| 409 | + | if i%64 == 63 { | |||
| 410 | + | bl.freeze() | |||
| 411 | + | } | |||
| 412 | + | } | |||
| 413 | + | } | |||
| 414 | + | ||||
| 415 | + | // TestTreeDeep builds a tree of several levels, then deletes ascending, | |||
| 416 | + | // exercising merges and borrows on internal nodes. | |||
| 417 | + | func TestTreeDeep(t *testing.T) { | |||
| 418 | + | const n = 20000 | |||
| 419 | + | var tr tree | |||
| 420 | + | for i := range n { | |||
| 421 | + | tr = tr.put(fmt.Appendf(nil, "%06d", i), nil, 1) | |||
| 422 | + | } | |||
| 423 | + | checkStructure(t, tr) | |||
| 424 | + | depth := 0 | |||
| 425 | + | for x := tr.root; x != nil; depth++ { | |||
| 426 | + | if x.leaf() { | |||
| 427 | + | depth++ | |||
| 428 | + | break | |||
| 429 | + | } | |||
| 430 | + | x = x.children[0] | |||
| 431 | + | } | |||
| 432 | + | if depth < 3 { | |||
| 433 | + | t.Fatalf("depth = %d, want >= 3", depth) | |||
| 434 | + | } | |||
| 435 | + | full := tr | |||
| 436 | + | for i := range n { | |||
| 437 | + | tr = tr.delete(fmt.Appendf(nil, "%06d", i)) | |||
| 438 | + | if i%251 == 0 { | |||
| 439 | + | checkStructure(t, tr) | |||
| 440 | + | } | |||
| 441 | + | } | |||
| 442 | + | if tr.len() != 0 || tr.root != nil { | |||
| 443 | + | t.Fatalf("tree not empty: len=%d", tr.len()) | |||
| 444 | + | } | |||
| 445 | + | checkStructure(t, full) | |||
| 446 | + | } | |||