Skip to content

Commit c7344ec

Browse files
committed
part: Store header pointer instead of uintptr in nodeMutated
If we store uintptr then there's a chance that a pointer there is garbage collected and reallocated in which case the assumption that a node present in nodeMutated has had its watch channel replaced and marked for closing no longer holds. This lead to the validation failing with a watch channel retained on a node which was mutated (e.g. leaf or children changed). Signed-off-by: Jussi Maki <jussi.maki@isovalent.com>
1 parent ca1fc27 commit c7344ec

File tree

3 files changed

+36
-16
lines changed

3 files changed

+36
-16
lines changed

part/cache.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,56 @@ import (
99

1010
const nodeMutatedSize = 256 // must be power-of-two
1111

12-
type nodeMutated struct {
13-
ptrs [nodeMutatedSize]uintptr
12+
// nodeMutated is a probabilistic check for seeing if a node has
13+
// been cloned within a transaction and thus can be modified in-place
14+
// since it has not been seen outside. This significantly speeds up
15+
// writes within a single write transaction as inner nodes no longer
16+
// need to be cloned on every change, effectively making the immutable
17+
// radix tree perform as if it's a mutable one.
18+
//
19+
// Earlier versions of StateDB just used a map[*header[T]]struct{}, but
20+
// that was fairly costly and experiments showed that it's enough to most
21+
// of the time avoid the clone to perform well.
22+
//
23+
// The value for [nodeMutatedSize] is a balance between making Txn()
24+
// not too costly (due to e.g. clear()) and between giving a high likelyhood
25+
// that we mutate nodes in-place.
26+
type nodeMutated[T any] struct {
27+
ptrs [nodeMutatedSize]*header[T]
1428
used bool
1529
}
1630

17-
func nodeMutatedSet[T any](nm *nodeMutated, ptr *header[T]) {
31+
func (nm *nodeMutated[T]) set(n *header[T]) {
1832
if nm == nil {
1933
return
2034
}
21-
ptrInt := uintptr(unsafe.Pointer(ptr))
22-
nm.ptrs[slot(ptrInt)] = ptrInt
35+
ptrInt := uintptr(unsafe.Pointer(n))
36+
nm.ptrs[slot(ptrInt)] = n
2337
nm.used = true
2438
}
2539

26-
func nodeMutatedExists[T any](nm *nodeMutated, ptr *header[T]) bool {
40+
func (nm *nodeMutated[T]) exists(n *header[T]) bool {
2741
if nm == nil {
2842
return false
2943
}
30-
ptrInt := uintptr(unsafe.Pointer(ptr))
31-
return nm.ptrs[slot(ptrInt)] == ptrInt
44+
ptrInt := uintptr(unsafe.Pointer(n))
45+
return nm.ptrs[slot(ptrInt)] == n
3246
}
3347

48+
// slot returns the index in the [ptrs] array for a given pointer.
49+
// The Go spec allows objects to be moved so it may be that the same
50+
// instance of an object is assigned to a different memory location in
51+
// which case we'd no longer report that node as being in the cache.
52+
// This is fine though as we do compare the actual *header[T] pointers
53+
// and this is probabilistic anyway as this is a fixed size cache.
3454
func slot(p uintptr) int {
3555
p >>= 4 // ignore low order bits
3656
// use some relevant bits from the pointer
3757
slot := uint8(p) ^ uint8(p>>8) ^ uint8(p>>16)
3858
return int(slot & (nodeMutatedSize - 1))
3959
}
4060

41-
func (nm *nodeMutated) clear() {
61+
func (nm *nodeMutated[T]) clear() {
4262
if nm == nil {
4363
return
4464
}

part/tree.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func newTxn[T any](o options) *Txn[T] {
5151
watches: make(map[chan struct{}]struct{}),
5252
}
5353
if !o.noCache() {
54-
txn.mutated = &nodeMutated{}
54+
txn.mutated = &nodeMutated[T]{}
5555
txn.deleteParentsCache = make([]deleteParent[T], 0, 32)
5656
}
5757
txn.opts = o

part/txn.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type Txn[T any] struct {
2626
// that we can keep mutating without cloning them again.
2727
// It is cleared if the transaction is cloned or iterated
2828
// upon.
29-
mutated *nodeMutated
29+
mutated *nodeMutated[T]
3030

3131
// watches contains the channels of cloned nodes that should be closed
3232
// when transaction is committed.
@@ -213,14 +213,14 @@ func (txn *Txn[T]) PrintTree() {
213213
}
214214

215215
func (txn *Txn[T]) cloneNode(n *header[T]) *header[T] {
216-
if nodeMutatedExists(txn.mutated, n) {
216+
if txn.mutated.exists(n) {
217217
return n
218218
}
219219
if n.watch != nil {
220220
txn.watches[n.watch] = struct{}{}
221221
}
222222
n = n.clone(!txn.opts.rootOnlyWatch())
223-
nodeMutatedSet(txn.mutated, n)
223+
txn.mutated.set(n)
224224
return n
225225
}
226226

@@ -265,7 +265,7 @@ func (txn *Txn[T]) modify(root *header[T], key []byte, mod func(T) T) (oldValue
265265
txn.watches[this.watch] = struct{}{}
266266
}
267267
this = this.promote()
268-
nodeMutatedSet(txn.mutated, this)
268+
txn.mutated.set(this)
269269
} else {
270270
// Node is big enough, clone it so we can mutate it
271271
this = txn.cloneNode(this)
@@ -371,7 +371,7 @@ func (txn *Txn[T]) modify(root *header[T], key []byte, mod func(T) T) (oldValue
371371
newNode.setSize(2)
372372
}
373373
*thisp = newNode.self()
374-
nodeMutatedSet(txn.mutated, newNode.self())
374+
txn.mutated.set(newNode.self())
375375

376376
return
377377
}
@@ -602,7 +602,7 @@ func (txn *Txn[T]) removeChild(parent *header[T], index int) (newParent *header[
602602
if parent.watch != nil {
603603
txn.watches[parent.watch] = struct{}{}
604604
}
605-
nodeMutatedSet(txn.mutated, newParent)
605+
txn.mutated.set(newParent)
606606
return newParent
607607
}
608608

0 commit comments

Comments
 (0)