Skip to content

Commit 2f8b8dd

Browse files
committed
Guard with decoy true to prevent writes on existing real status.
1 parent 5a6c1f4 commit 2f8b8dd

File tree

1 file changed

+53
-43
lines changed

1 file changed

+53
-43
lines changed

internal/registry/db/methods_token_status_list.go

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package db
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"math/rand/v2"
78
"vc/pkg/logger"
@@ -135,16 +136,10 @@ func (c *TokenStatusListColl) FindOne(ctx context.Context, section, index int64)
135136
}
136137

137138
// CreateNewSection creates a new section with decoy entries.
138-
// If sectionSize is 0 or negative, it defaults to 500,000.
139139
func (c *TokenStatusListColl) CreateNewSection(ctx context.Context, section int64, sectionSize int64) error {
140140
ctx, span := c.Service.tracer.Start(ctx, "db:token_status_list:createNewSection")
141141
defer span.End()
142142

143-
// Default to 500,000 if not set
144-
if sectionSize <= 0 {
145-
sectionSize = 500000
146-
}
147-
148143
docs := []*TokenStatusListDoc{}
149144
for i := int64(0); i < sectionSize; i++ {
150145
docs = append(docs, &TokenStatusListDoc{
@@ -171,9 +166,6 @@ func (c *TokenStatusListColl) getRandomDecoys(ctx context.Context, section int64
171166
defer span.End()
172167

173168
sectionSize := c.Service.cfg.Registry.TokenStatusLists.SectionSize
174-
if sectionSize <= 0 {
175-
sectionSize = 1000000
176-
}
177169

178170
// Generate random indices and look them up directly via the {index, section} unique index.
179171
// This is O(n) index seeks instead of O(sectionSize) collection scan from $match + $sample.
@@ -192,7 +184,11 @@ func (c *TokenStatusListColl) getRandomDecoys(ctx context.Context, section int64
192184
var doc TokenStatusListDoc
193185
err := c.Coll.FindOne(ctx, bson.M{"index": idx, "section": section}).Decode(&doc)
194186
if err != nil {
195-
continue
187+
if errors.Is(err, mongo.ErrNoDocuments) {
188+
continue
189+
}
190+
span.SetStatus(codes.Error, err.Error())
191+
return nil, fmt.Errorf("lookup decoy index %d in section %d: %w", idx, section, err)
196192
}
197193
if !doc.Decoy {
198194
continue // skip already-allocated entries
@@ -201,7 +197,9 @@ func (c *TokenStatusListColl) getRandomDecoys(ctx context.Context, section int64
201197
}
202198

203199
if len(docs) == 0 {
204-
return nil, fmt.Errorf("no decoy entries found in section %d", section)
200+
err := fmt.Errorf("no decoy entries found in section %d", section)
201+
span.SetStatus(codes.Error, err.Error())
202+
return nil, err
205203
}
206204

207205
return docs, nil
@@ -212,47 +210,59 @@ func (c *TokenStatusListColl) Add(ctx context.Context, section int64, status uin
212210
ctx, span := c.Service.tracer.Start(ctx, "db:token_status_list:add")
213211
defer span.End()
214212

215-
decoys, err := c.getRandomDecoys(ctx, section)
216-
if err != nil {
217-
return 0, err
218-
}
219-
220-
c.log.Debug("add", "decoys", decoys)
213+
const maxRetries = 3
214+
for retry := 0; retry < maxRetries; retry++ {
215+
decoys, err := c.getRandomDecoys(ctx, section)
216+
if err != nil {
217+
return 0, err
218+
}
221219

222-
doc := &TokenStatusListDoc{
223-
Index: decoys[rand.IntN(len(decoys))].Index,
224-
Status: status,
225-
Decoy: false,
226-
Section: section,
227-
}
220+
c.log.Debug("add", "decoys", decoys)
228221

229-
// Batch all updates (real entry + noise decoys) into a single BulkWrite
230-
// to avoid sequential round-trips to MongoDB.
231-
models := make([]mongo.WriteModel, 0, len(decoys))
222+
doc := &TokenStatusListDoc{
223+
Index: decoys[rand.IntN(len(decoys))].Index,
224+
Status: status,
225+
Decoy: false,
226+
Section: section,
227+
}
232228

233-
// The real status entry
234-
models = append(models, mongo.NewUpdateOneModel().
235-
SetFilter(bson.M{"index": doc.Index, "section": doc.Section}).
236-
SetUpdate(bson.M{"$set": doc}))
229+
// Atomically claim the decoy slot by requiring decoy: true in the filter.
230+
// If another caller already claimed this index, MatchedCount will be 0.
231+
result, err := c.Coll.UpdateOne(ctx,
232+
bson.M{"index": doc.Index, "section": doc.Section, "decoy": true},
233+
bson.M{"$set": doc})
234+
if err != nil {
235+
span.SetStatus(codes.Error, err.Error())
236+
c.log.Error(err, "cant allocate status entry")
237+
return 0, err
238+
}
237239

238-
// Noise updates for the remaining decoys
239-
for _, decoy := range decoys {
240-
if decoy.Index == doc.Index {
240+
if result.MatchedCount == 0 {
241+
c.log.Info("decoy already claimed, retrying", "index", doc.Index, "retry", retry)
241242
continue
242243
}
243-
models = append(models, mongo.NewUpdateOneModel().
244-
SetFilter(bson.M{"index": decoy.Index, "section": section}).
245-
SetUpdate(bson.M{"$set": bson.M{"status": rand.IntN(maxRandomLimit)}}))
246-
}
247244

248-
_, err = c.Coll.BulkWrite(ctx, models)
249-
if err != nil {
250-
span.SetStatus(codes.Error, err.Error())
251-
c.log.Error(err, "cant add status with noise updates")
252-
return 0, err
245+
// Noise updates for the remaining decoys — guard with decoy: true
246+
// so we don't overwrite already-allocated real entries.
247+
if len(decoys) > 1 {
248+
models := make([]mongo.WriteModel, 0, len(decoys)-1)
249+
for _, decoy := range decoys {
250+
if decoy.Index == doc.Index {
251+
continue
252+
}
253+
models = append(models, mongo.NewUpdateOneModel().
254+
SetFilter(bson.M{"index": decoy.Index, "section": section, "decoy": true}).
255+
SetUpdate(bson.M{"$set": bson.M{"status": rand.IntN(maxRandomLimit)}}))
256+
}
257+
if _, err = c.Coll.BulkWrite(ctx, models); err != nil {
258+
c.log.Error(err, "noise updates failed (real entry already allocated)", "index", doc.Index)
259+
}
260+
}
261+
262+
return doc.Index, nil
253263
}
254264

255-
return doc.Index, nil
265+
return 0, fmt.Errorf("failed to allocate status entry after %d retries in section %d", maxRetries, section)
256266
}
257267

258268
// GetAllStatusesForSection retrieves all status entries for a given section, ordered by index.

0 commit comments

Comments
 (0)