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