7
7
8
8
"github.com/ipfs/go-cid"
9
9
ds "github.com/ipfs/go-datastore"
10
+ "github.com/ipfs/go-datastore/namespace"
10
11
"github.com/libp2p/go-libp2p-kad-dht/provider/internal/keyspace"
11
12
mh "github.com/multiformats/go-multihash"
12
13
)
@@ -26,17 +27,35 @@ type resetOp struct {
26
27
}
27
28
28
29
// ResettableKeyStore is a KeyStore implementation that supports atomic reset
29
- // operations. It maintains two alternate bases in the underlying datastore and
30
- // can swap between them to provide atomic replacement of all stored keys.
30
+ // operations using a dual-datastore architecture. It maintains two separate
31
+ // datastores (primary and alternate) where only one is active at any time,
32
+ // enabling atomic replacement of all stored keys without interrupting
33
+ // concurrent operations.
31
34
//
32
- // The reset operation allows replacing all stored multihashes with a new set
33
- // without interrupting concurrent read/write operations. During a reset, new
34
- // writes are duplicated to both the current and alternate storage bases to
35
- // maintain consistency.
35
+ // Architecture:
36
+ // - Primary datastore: Currently active storage for all read/write operations
37
+ // - Alternate datastore: Standby storage used during reset operations
38
+ // - The datastores use "/0" and "/1" namespace suffixes and can be swapped
39
+ //
40
+ // Reset Operation Flow:
41
+ // 1. New keys from reset are written to the alternate (inactive) datastore
42
+ // 2. Concurrent Put operations are automatically duplicated to both datastores
43
+ // to maintain consistency during the transition
44
+ // 3. Once all reset keys are written, the datastores are atomically swapped
45
+ // 4. The old datastore (now alternate) is cleaned up
46
+ //
47
+ // Thread Safety:
48
+ // - All operations are processed sequentially by a single worker goroutine
49
+ // - Reset operations are non-blocking for concurrent reads and writes
50
+ // - Only one reset operation can be active at a time
51
+ //
52
+ // The reset operation allows complete replacement of stored multihashes
53
+ // without data loss or service interruption, making it suitable for
54
+ // scenarios requiring periodic full dataset updates.
36
55
type ResettableKeyStore struct {
37
56
keyStore
38
57
39
- altBase ds.Key
58
+ altDs ds.Batching
40
59
resetInProgress bool
41
60
resetSync chan []mh.Multihash // passes keys from worker to reset go routine
42
61
resetOps chan resetOp // reset operations that must be run in main go routine
@@ -59,15 +78,14 @@ func NewResettableKeyStore(d ds.Batching, opts ...KeyStoreOption) (*ResettableKe
59
78
60
79
rks := & ResettableKeyStore {
61
80
keyStore : keyStore {
62
- ds : d ,
63
- base : ds .NewKey (cfg .base ).ChildString ("0" ),
81
+ ds : namespace .Wrap (d , ds .NewKey (cfg .base + "/0" )),
64
82
prefixBits : cfg .prefixBits ,
65
83
batchSize : cfg .batchSize ,
66
84
requests : make (chan operation ),
67
85
close : make (chan struct {}),
68
86
done : make (chan struct {}),
69
87
},
70
- altBase : ds .NewKey (cfg .base ). ChildString ( "1" ),
88
+ altDs : namespace . Wrap ( d , ds .NewKey (cfg .base + "/1" ) ),
71
89
resetOps : make (chan resetOp ),
72
90
resetSync : make (chan []mh.Multihash , 128 ), // buffered to avoid blocking
73
91
}
@@ -105,7 +123,7 @@ func (s *ResettableKeyStore) worker() {
105
123
op .response <- operationResponse {err : err }
106
124
107
125
case opEmpty :
108
- err := empty (op .ctx , s .ds , s .base , s . batchSize )
126
+ err := empty (op .ctx , s .ds , s .batchSize )
109
127
op .response <- operationResponse {err : err }
110
128
111
129
case opSize :
@@ -127,20 +145,21 @@ func (s *ResettableKeyStore) worker() {
127
145
// handling during reset operations.
128
146
func (s * ResettableKeyStore ) put (ctx context.Context , keys []mh.Multihash ) ([]mh.Multihash , error ) {
129
147
if s .resetInProgress {
130
- // Reset is in progress, write to alternate base in addition to current base
148
+ // Reset is in progress, write to alternate datastore in addition to
149
+ // current datastore
131
150
s .resetSync <- keys
132
151
}
133
152
return s .keyStore .put (ctx , keys )
134
153
}
135
154
136
- // altPut writes the given multihashes to the alternate base in the datastore.
155
+ // altPut writes the given multihashes to the alternate datastore.
137
156
func (s * ResettableKeyStore ) altPut (ctx context.Context , keys []mh.Multihash ) error {
138
- b , err := s .ds .Batch (ctx )
157
+ b , err := s .altDs .Batch (ctx )
139
158
if err != nil {
140
159
return err
141
160
}
142
161
for _ , h := range keys {
143
- dsk := dsKey (keyspace .MhToBit256 (h ), s .prefixBits , s . altBase )
162
+ dsk := dsKey (keyspace .MhToBit256 (h ), s .prefixBits )
144
163
if err := b .Put (ctx , dsk , h ); err != nil {
145
164
return err
146
165
}
@@ -155,7 +174,7 @@ func (s *ResettableKeyStore) handleResetOp(op resetOp) {
155
174
op .response <- ErrResetInProgress
156
175
return
157
176
}
158
- if err := empty (context .Background (), s .ds , s . altBase , s .batchSize ); err != nil {
177
+ if err := empty (context .Background (), s .altDs , s .batchSize ); err != nil {
159
178
op .response <- err
160
179
return
161
180
}
@@ -166,10 +185,10 @@ func (s *ResettableKeyStore) handleResetOp(op resetOp) {
166
185
167
186
// Cleanup operation
168
187
if op .success {
169
- // Swap the keystore prefix bases .
170
- oldBase := s .base
171
- s .base = s .altBase
172
- s .altBase = oldBase
188
+ // Swap the active datastore .
189
+ oldDs := s .ds
190
+ s .ds = s .altDs
191
+ s .altDs = oldDs
173
192
}
174
193
// Drain resetSync
175
194
drain:
@@ -180,9 +199,9 @@ drain:
180
199
break drain
181
200
}
182
201
}
183
- // Empty the unused base prefix
202
+ // Empty the unused datastore.
184
203
s .resetInProgress = false
185
- op .response <- empty (context .Background (), s .ds , s . altBase , s .batchSize )
204
+ op .response <- empty (context .Background (), s .altDs , s .batchSize )
186
205
}
187
206
188
207
// ResetCids atomically replaces all stored keys with the CIDs received from
@@ -231,7 +250,7 @@ func (s *ResettableKeyStore) ResetCids(ctx context.Context, keysChan <-chan cid.
231
250
case <- s .done :
232
251
// Safe not to go through the worker since we are done, and we need to
233
252
// cleanup
234
- empty (context .Background (), s .ds , s . altBase , s .batchSize )
253
+ empty (context .Background (), s .altDs , s .batchSize )
235
254
}
236
255
}()
237
256
@@ -260,7 +279,7 @@ func (s *ResettableKeyStore) ResetCids(ctx context.Context, keysChan <-chan cid.
260
279
return nil
261
280
}
262
281
263
- // Read all the keys from the channel and put them in batch at the alternate base
282
+ // Read all the keys from the channel and write them to the altDs
264
283
loop:
265
284
for {
266
285
select {
0 commit comments