@@ -2,6 +2,8 @@ package db
22
33import (
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.
131139func (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
161164func (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