Skip to content

Commit 5b4fc1e

Browse files
tmennatnaga
authored andcommitted
chore: conflict and ingest instrumentation
1 parent 411b0f1 commit 5b4fc1e

15 files changed

Lines changed: 1250 additions & 30 deletions

internal/datastore/readkinds.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2026 Democratized Data Foundation
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package datastore
12+
13+
import (
14+
"strings"
15+
"sync/atomic"
16+
)
17+
18+
// ReadKind names one class of key a transaction's read set can hold.
19+
//
20+
// The underlying store reports a commit conflict without naming the contended key, so the
21+
// set of classes a transaction read is the only lead available for working out what it
22+
// contended with. The classes are bit flags so a whole transaction's set fits in one word
23+
// and recording a read costs an atomic OR with no allocation.
24+
type ReadKind uint32
25+
26+
const (
27+
// ReadDoc is a document field value or its priority marker, under the datastore.
28+
ReadDoc ReadKind = 1 << iota
29+
// ReadIndex is a secondary index entry, under the datastore. Set by the index write
30+
// path, which is the only place that knows an encoded datastore key is an index entry.
31+
ReadIndex
32+
// ReadUniqueIndex is the existence check a unique index performs before writing its
33+
// entry. Set by the unique index write path for the same reason as ReadIndex.
34+
ReadUniqueIndex
35+
// ReadHead is a headstore key: a document's or collection's current DAG heads.
36+
ReadHead
37+
// ReadBlock is a blockstore key: an IPLD block or its to-merge marker.
38+
ReadBlock
39+
// ReadSystem is a systemstore key that is not a sequence.
40+
ReadSystem
41+
// ReadSequence is a systemstore /seq/ key. Short ID allocation commits the sequence in
42+
// its own transaction, so a merge never carries one in its read set and this stays zero
43+
// on that path. It is kept for callers that do read a sequence inline.
44+
ReadSequence
45+
// ReadPeer is a peerstore key.
46+
ReadPeer
47+
// ReadEnc is an encryption keystore key.
48+
ReadEnc
49+
// ReadRangeIterator means the transaction opened an iterator bounded by start and end
50+
// rather than by prefix. Badger's prefix option is unset for those, so its bounds
51+
// check reads the first key past the range into the read set.
52+
ReadRangeIterator
53+
// ReadPrefixIterator means the transaction opened a prefix-bounded iterator, which
54+
// cannot read past its range.
55+
ReadPrefixIterator
56+
)
57+
58+
// readKindNames is ordered to match the bit order above.
59+
var readKindNames = [...]string{
60+
"doc",
61+
"index",
62+
"uniqueIndex",
63+
"head",
64+
"block",
65+
"system",
66+
"sequence",
67+
"peer",
68+
"enc",
69+
"rangeIter",
70+
"prefixIter",
71+
}
72+
73+
// ReadKindCount is the number of distinct kinds, for callers keeping a counter per kind.
74+
const ReadKindCount = len(readKindNames)
75+
76+
// ReadKindName returns the name of the kind held in bit i, or "" if i is out of range.
77+
func ReadKindName(i int) string {
78+
if i < 0 || i >= len(readKindNames) {
79+
return ""
80+
}
81+
return readKindNames[i]
82+
}
83+
84+
// String renders the set as a pipe-separated list in bit order, e.g. "doc|head|rangeIter".
85+
// An empty set renders as "none".
86+
func (k ReadKind) String() string {
87+
if k == 0 {
88+
return "none"
89+
}
90+
var b strings.Builder
91+
for i, name := range readKindNames {
92+
if k&(1<<uint(i)) == 0 {
93+
continue
94+
}
95+
if b.Len() > 0 {
96+
b.WriteByte('|')
97+
}
98+
b.WriteString(name)
99+
}
100+
return b.String()
101+
}
102+
103+
// ReadKinds accumulates the kinds of key a single transaction has read.
104+
//
105+
// The zero value is ready to use. It is safe for concurrent use, which matters because a
106+
// transaction may be read from more than one goroutine even though a merge is not.
107+
type ReadKinds struct {
108+
bits atomic.Uint32
109+
}
110+
111+
// Mark records that a key of the given kind entered the read set. Safe on a nil receiver
112+
// so call sites do not have to check whether the transaction records kinds.
113+
func (r *ReadKinds) Mark(kind ReadKind) {
114+
if r == nil {
115+
return
116+
}
117+
r.bits.Or(uint32(kind))
118+
}
119+
120+
// Kinds returns the set recorded so far.
121+
func (r *ReadKinds) Kinds() ReadKind {
122+
if r == nil {
123+
return 0
124+
}
125+
return ReadKind(r.bits.Load())
126+
}
127+
128+
// readKindsCarrier is implemented by transactions that record read kinds. It is optional:
129+
// shims and mocks that do not record them are simply not asked.
130+
type readKindsCarrier interface {
131+
ReadKinds() *ReadKinds
132+
}
133+
134+
// ReadKindsOf returns the recorder for txn, or nil if txn does not record read kinds.
135+
// A nil result is usable: every method on ReadKinds tolerates it.
136+
//
137+
// Used by the few call sites that know a key's class from its Go type rather than its
138+
// encoded bytes: an index entry and a document value are both datastore keys and are
139+
// indistinguishable once encoded.
140+
func ReadKindsOf(txn any) *ReadKinds {
141+
if c, ok := txn.(readKindsCarrier); ok {
142+
return c.ReadKinds()
143+
}
144+
return nil
145+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2026 Democratized Data Foundation
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package datastore
12+
13+
import (
14+
"context"
15+
"testing"
16+
17+
"github.com/stretchr/testify/require"
18+
19+
"github.com/sourcenetwork/corekv"
20+
"github.com/sourcenetwork/corekv/memory"
21+
"github.com/sourcenetwork/immutable"
22+
23+
"github.com/sourcenetwork/defradb/internal/db/lock"
24+
"github.com/sourcenetwork/defradb/internal/keys"
25+
)
26+
27+
func TestKindForKey(t *testing.T) {
28+
tests := []struct {
29+
name string
30+
key string
31+
want ReadKind
32+
}{
33+
{"datastore", "d/1/v/2/3", ReadDoc},
34+
{"headstore", "h/1/2", ReadHead},
35+
{"blockstore", "bsomecid", ReadBlock},
36+
{"systemstore", "s/collection/name/Foo", ReadSystem},
37+
{"sequence", "s/seq/doc", ReadSequence},
38+
{"peerstore", "p/replicator/1", ReadPeer},
39+
{"encstore", "esomecid", ReadEnc},
40+
{"unknown", "?/1", 0},
41+
{"empty", "", 0},
42+
}
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
require.Equal(t, tt.want, kindForKey([]byte(tt.key)))
46+
})
47+
}
48+
}
49+
50+
func TestReadKindString(t *testing.T) {
51+
require.Equal(t, "none", ReadKind(0).String())
52+
require.Equal(t, "doc", ReadDoc.String())
53+
require.Equal(t, "doc|head|rangeIter", (ReadDoc | ReadHead | ReadRangeIterator).String())
54+
}
55+
56+
// The recorder has to see the store discriminator that namespace.Wrap prepends, so these
57+
// go through a real transaction rather than calling the recorder directly.
58+
func TestTxnRecordsReadKinds(t *testing.T) {
59+
ctx := context.Background()
60+
txn := NewTxnFrom(memory.NewDatastore(ctx), lock.NewLockSet(), 0, false, immutable.None[int]())
61+
defer txn.Discard()
62+
ctx = CtxSetTxn(ctx, txn)
63+
64+
require.Equal(t, ReadKind(0), txn.ReadKinds().Kinds())
65+
66+
key := keys.DataStoreKey{CollectionShortID: 1, InstanceType: keys.ValueKey, DocShortID: 2, FieldID: "3"}
67+
_, err := txn.Datastore().Get(ctx, key)
68+
require.ErrorIs(t, err, corekv.ErrNotFound)
69+
require.Equal(t, ReadDoc, txn.ReadKinds().Kinds())
70+
71+
_, err = txn.Headstore().Get(ctx, []byte("/1/2"))
72+
require.ErrorIs(t, err, corekv.ErrNotFound)
73+
require.Equal(t, ReadDoc|ReadHead, txn.ReadKinds().Kinds())
74+
}
75+
76+
// A start/end iterator is the shape whose bounds check reads one key past the requested
77+
// range. Distinguishing it from a prefix iterator is the point of the two iterator bits.
78+
func TestTxnRecordsIteratorShape(t *testing.T) {
79+
ctx := context.Background()
80+
81+
prefix := keys.DataStoreKey{CollectionShortID: 1, InstanceType: keys.ValueKey, DocShortID: 2}
82+
83+
rangeTxn := NewTxnFrom(memory.NewDatastore(ctx), lock.NewLockSet(), 0, false, immutable.None[int]())
84+
defer rangeTxn.Discard()
85+
iter, err := rangeTxn.Datastore().Iterator(
86+
CtxSetTxn(ctx, rangeTxn),
87+
IterOptions{Start: prefix, End: prefix.PrefixEnd()},
88+
)
89+
require.NoError(t, err)
90+
require.NoError(t, iter.Close())
91+
require.Equal(t, ReadDoc|ReadRangeIterator, rangeTxn.ReadKinds().Kinds())
92+
93+
prefixTxn := NewTxnFrom(memory.NewDatastore(ctx), lock.NewLockSet(), 0, false, immutable.None[int]())
94+
defer prefixTxn.Discard()
95+
iter, err = prefixTxn.Datastore().Iterator(CtxSetTxn(ctx, prefixTxn), IterOptions{Prefix: prefix})
96+
require.NoError(t, err)
97+
require.NoError(t, iter.Close())
98+
require.Equal(t, ReadDoc|ReadPrefixIterator, prefixTxn.ReadKinds().Kinds())
99+
}

internal/datastore/readrecorder.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2026 Democratized Data Foundation
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package datastore
12+
13+
import (
14+
"bytes"
15+
"context"
16+
17+
"github.com/sourcenetwork/corekv"
18+
)
19+
20+
// sequencePrefix is the systemstore prefix under which all sequences live.
21+
var sequencePrefix = []byte("/seq/")
22+
23+
// readRecorder sits between a transaction's namespaced stores and the raw transaction,
24+
// classifying every key the transaction reads by the store it belongs to.
25+
//
26+
// Writes pass through unclassified: the underlying store's conflict detection compares a
27+
// transaction's reads against other transactions' writes, so only reads decide whether a
28+
// commit conflicts.
29+
//
30+
// The recorder must not be handed to corekv as a context transaction. corekv casts the
31+
// context value to its own concrete transaction type, and a wrapper fails that cast.
32+
type readRecorder struct {
33+
store corekv.ReaderWriter
34+
kinds *ReadKinds
35+
}
36+
37+
var _ corekv.ReaderWriter = (*readRecorder)(nil)
38+
39+
// kindForKey classifies a rootstore key by its leading store discriminator byte, which
40+
// namespace.Wrap has already prepended by the time the key reaches here.
41+
func kindForKey(key []byte) ReadKind {
42+
if len(key) == 0 {
43+
return 0
44+
}
45+
switch key[0] {
46+
case dataStoreKey:
47+
// Index entries are also datastore keys and encode identically to document keys.
48+
// The index write path marks those itself; everything else here is a document.
49+
return ReadDoc
50+
case headStoreKey:
51+
return ReadHead
52+
case blockStoreKey:
53+
return ReadBlock
54+
case systemStoreKey:
55+
if bytes.HasPrefix(key[1:], sequencePrefix) {
56+
return ReadSequence
57+
}
58+
return ReadSystem
59+
case peerStoreKey:
60+
return ReadPeer
61+
case encStoreKey:
62+
return ReadEnc
63+
default:
64+
return 0
65+
}
66+
}
67+
68+
func (r *readRecorder) Get(ctx context.Context, key []byte) ([]byte, error) {
69+
r.kinds.Mark(kindForKey(key))
70+
return r.store.Get(ctx, key)
71+
}
72+
73+
func (r *readRecorder) Has(ctx context.Context, key []byte) (bool, error) {
74+
r.kinds.Mark(kindForKey(key))
75+
return r.store.Has(ctx, key)
76+
}
77+
78+
// Iterator records the shape of the iterator as well as what it scans. A start/end
79+
// iterator is the shape whose bounds check reads one key past the requested range, so
80+
// separating the two shapes is what makes that over-read visible.
81+
func (r *readRecorder) Iterator(ctx context.Context, opts corekv.IterOptions) (corekv.Iterator, error) {
82+
switch {
83+
case opts.Prefix != nil:
84+
r.kinds.Mark(ReadPrefixIterator | kindForKey(opts.Prefix))
85+
case opts.Start != nil:
86+
r.kinds.Mark(ReadRangeIterator | kindForKey(opts.Start))
87+
case opts.End != nil:
88+
r.kinds.Mark(ReadRangeIterator | kindForKey(opts.End))
89+
}
90+
return r.store.Iterator(ctx, opts)
91+
}
92+
93+
func (r *readRecorder) Set(ctx context.Context, key, value []byte) error {
94+
return r.store.Set(ctx, key, value)
95+
}
96+
97+
func (r *readRecorder) Delete(ctx context.Context, key []byte) error {
98+
return r.store.Delete(ctx, key)
99+
}

internal/datastore/txn.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ type BasicTxn struct {
9090
id uint64
9191
ts time.Time // timestamp
9292

93+
// readKinds records which classes of key this transaction's read set holds, so a
94+
// commit conflict can be attributed to a class of key rather than reported bare.
95+
readKinds ReadKinds
96+
9397
successFns []func()
9498
errorFns []func()
9599
discardFns []func()
@@ -110,14 +114,26 @@ func NewTxnFrom(
110114
chunkSize immutable.Option[int],
111115
) *BasicTxn {
112116
rootTxn := rootstore.NewTxn(readonly)
113-
multistore := NewMultistore(rootTxn, lockSet, chunkSize)
114117

115-
return &BasicTxn{
116-
Multistore: multistore,
118+
txn := &BasicTxn{
117119
underlyingTxn: rootTxn,
118120
id: id,
119121
ts: time.Now(),
120122
}
123+
// The child stores read through the recorder; underlyingTxn stays unwrapped because
124+
// corekv casts it to its own concrete type when it is fetched from a context.
125+
txn.Multistore = NewMultistore(
126+
&readRecorder{store: rootTxn, kinds: &txn.readKinds},
127+
lockSet,
128+
chunkSize,
129+
)
130+
131+
return txn
132+
}
133+
134+
// ReadKinds returns the classes of key this transaction has read so far.
135+
func (t *BasicTxn) ReadKinds() *ReadKinds {
136+
return &t.readKinds
121137
}
122138

123139
// The raw underlying txn.

0 commit comments

Comments
 (0)