Skip to content

Commit fe42162

Browse files
authored
Merge pull request #328 from SUNET/masv/improve/faster_registry_lookup
Faster lookup in registry.
2 parents 21df8ed + 2f8b8dd commit fe42162

File tree

1 file changed

+83
-50
lines changed

1 file changed

+83
-50
lines changed

internal/registry/db/methods_token_status_list.go

Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package db
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"math/rand/v2"
68
"vc/pkg/logger"
79

@@ -86,7 +88,14 @@ func (c *TokenStatusListColl) createIndex(ctx context.Context) error {
8688
},
8789
Options: options.Index().SetName("index_uniq").SetUnique(true),
8890
}
89-
_, err := c.Coll.Indexes().CreateMany(ctx, []mongo.IndexModel{indexUniq})
91+
indexDecoyLookup := mongo.IndexModel{
92+
Keys: bson.D{
93+
bson.E{Key: "section", Value: 1},
94+
bson.E{Key: "decoy", Value: 1},
95+
},
96+
Options: options.Index().SetName("decoy_lookup"),
97+
}
98+
_, err := c.Coll.Indexes().CreateMany(ctx, []mongo.IndexModel{indexUniq, indexDecoyLookup})
9099
if err != nil {
91100
return err
92101
}
@@ -127,16 +136,10 @@ func (c *TokenStatusListColl) FindOne(ctx context.Context, section, index int64)
127136
}
128137

129138
// CreateNewSection creates a new section with decoy entries.
130-
// If sectionSize is 0 or negative, it defaults to 500,000.
131139
func (c *TokenStatusListColl) CreateNewSection(ctx context.Context, section int64, sectionSize int64) error {
132140
ctx, span := c.Service.tracer.Start(ctx, "db:token_status_list:createNewSection")
133141
defer span.End()
134142

135-
// Default to 500,000 if not set
136-
if sectionSize <= 0 {
137-
sectionSize = 500000
138-
}
139-
140143
docs := []*TokenStatusListDoc{}
141144
for i := int64(0); i < sectionSize; i++ {
142145
docs = append(docs, &TokenStatusListDoc{
@@ -161,25 +164,41 @@ func (c *TokenStatusListColl) CreateNewSection(ctx context.Context, section int6
161164
func (c *TokenStatusListColl) getRandomDecoys(ctx context.Context, section int64) ([]TokenStatusListDoc, error) {
162165
ctx, span := c.Service.tracer.Start(ctx, "db:token_status_list:getRandomDecoyIndexes")
163166
defer span.End()
164-
match := bson.D{{Key: "section", Value: section}, {Key: "decoy", Value: true}}
165-
sample := bson.M{"size": 10}
166167

167-
pipeline := mongo.Pipeline{
168-
{{"$match", match}},
169-
{{"$sample", sample}},
170-
}
168+
sectionSize := c.Service.cfg.Registry.TokenStatusLists.SectionSize
171169

172-
cursor, err := c.Coll.Aggregate(ctx, pipeline)
173-
if err != nil {
174-
span.SetStatus(codes.Error, err.Error())
175-
c.log.Error(err, "cant get decoy indexes")
176-
return nil, err
170+
// Generate random indices and look them up directly via the {index, section} unique index.
171+
// This is O(n) index seeks instead of O(sectionSize) collection scan from $match + $sample.
172+
const want = 10
173+
const maxAttempts = 50
174+
var docs []TokenStatusListDoc
175+
seen := make(map[int64]bool, maxAttempts)
176+
177+
for attempt := 0; len(docs) < want && attempt < maxAttempts; attempt++ {
178+
idx := rand.Int64N(sectionSize)
179+
if seen[idx] {
180+
continue
181+
}
182+
seen[idx] = true
183+
184+
var doc TokenStatusListDoc
185+
err := c.Coll.FindOne(ctx, bson.M{"index": idx, "section": section}).Decode(&doc)
186+
if err != nil {
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)
192+
}
193+
if !doc.Decoy {
194+
continue // skip already-allocated entries
195+
}
196+
docs = append(docs, doc)
177197
}
178198

179-
var docs []TokenStatusListDoc
180-
if err = cursor.All(ctx, &docs); err != nil {
199+
if len(docs) == 0 {
200+
err := fmt.Errorf("no decoy entries found in section %d", section)
181201
span.SetStatus(codes.Error, err.Error())
182-
c.log.Error(err, "cant decode decoy indexes")
183202
return nil, err
184203
}
185204

@@ -191,45 +210,59 @@ func (c *TokenStatusListColl) Add(ctx context.Context, section int64, status uin
191210
ctx, span := c.Service.tracer.Start(ctx, "db:token_status_list:add")
192211
defer span.End()
193212

194-
decoys, err := c.getRandomDecoys(ctx, section)
195-
if err != nil {
196-
return 0, err
197-
}
198-
199-
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+
}
200219

201-
doc := &TokenStatusListDoc{
202-
Index: decoys[rand.IntN(len(decoys))].Index,
203-
Status: status,
204-
Decoy: false,
205-
Section: section,
206-
}
220+
c.log.Debug("add", "decoys", decoys)
207221

208-
_, err = c.Coll.UpdateOne(ctx, bson.M{"index": doc.Index, "section": doc.Section}, bson.M{"$set": doc})
209-
if err != nil {
210-
span.SetStatus(codes.Error, err.Error())
211-
c.log.Error(err, "cant add status")
212-
return 0, err
213-
}
214-
215-
// Update random decoys to add noise
216-
for _, decoy := range decoys {
217-
if decoy.Index == doc.Index {
218-
continue
222+
doc := &TokenStatusListDoc{
223+
Index: decoys[rand.IntN(len(decoys))].Index,
224+
Status: status,
225+
Decoy: false,
226+
Section: section,
219227
}
220228

221-
filter := bson.M{"index": decoy.Index, "section": section}
222-
updateDoc := bson.M{"$set": bson.M{"status": rand.IntN(maxRandomLimit)}}
223-
224-
_, err := c.Coll.UpdateOne(ctx, filter, updateDoc)
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})
225234
if err != nil {
226235
span.SetStatus(codes.Error, err.Error())
227-
c.log.Error(err, "cant update status")
236+
c.log.Error(err, "cant allocate status entry")
228237
return 0, err
229238
}
239+
240+
if result.MatchedCount == 0 {
241+
c.log.Info("decoy already claimed, retrying", "index", doc.Index, "retry", retry)
242+
continue
243+
}
244+
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
230263
}
231264

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

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

0 commit comments

Comments
 (0)