github.com/cilium/statedb@v0.3.2/graveyard.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package statedb 5 6 import ( 7 "context" 8 "maps" 9 "slices" 10 "time" 11 12 "golang.org/x/time/rate" 13 ) 14 15 const ( 16 // defaultGCRateLimitInterval is the default minimum interval between garbage collections. 17 defaultGCRateLimitInterval = time.Second 18 ) 19 20 func graveyardWorker(db *DB, ctx context.Context, gcRateLimitInterval time.Duration) { 21 limiter := rate.NewLimiter(rate.Every(gcRateLimitInterval), 1) 22 defer close(db.gcExited) 23 24 for { 25 select { 26 case <-ctx.Done(): 27 return 28 case <-db.gcTrigger: 29 } 30 31 // Throttle garbage collection. 32 if err := limiter.Wait(ctx); err != nil { 33 return 34 } 35 36 cleaningTimes := make(map[string]time.Duration) 37 38 type deadObjectRevisionKey = []byte 39 toBeDeleted := map[TableMeta][]deadObjectRevisionKey{} 40 41 // Do a lockless read transaction to find potential dead objects. 42 txn := db.ReadTxn().getTxn() 43 for _, table := range txn.root { 44 tableName := table.meta.Name() 45 start := time.Now() 46 47 // Find the low watermark 48 lowWatermark := table.revision 49 dtIter := table.deleteTrackers.Iterator() 50 for _, dt, ok := dtIter.Next(); ok; _, dt, ok = dtIter.Next() { 51 rev := dt.getRevision() 52 if rev < lowWatermark { 53 lowWatermark = rev 54 } 55 } 56 57 db.metrics.GraveyardLowWatermark( 58 tableName, 59 lowWatermark, 60 ) 61 62 // Find objects to be deleted by iterating over the graveyard revision index up 63 // to the low watermark. 64 indexTree := txn.mustIndexReadTxn(table.meta, GraveyardRevisionIndexPos) 65 66 objIter := indexTree.Iterator() 67 for key, obj, ok := objIter.Next(); ok; key, obj, ok = objIter.Next() { 68 if obj.revision > lowWatermark { 69 break 70 } 71 toBeDeleted[table.meta] = append(toBeDeleted[table.meta], key) 72 } 73 cleaningTimes[tableName] = time.Since(start) 74 } 75 76 if len(toBeDeleted) == 0 { 77 for tableName, stat := range cleaningTimes { 78 db.metrics.GraveyardCleaningDuration( 79 tableName, 80 stat, 81 ) 82 } 83 continue 84 } 85 86 // Dead objects found, do a write transaction against all tables with dead objects in them. 87 tablesToModify := slices.Collect(maps.Keys(toBeDeleted)) 88 txn = db.WriteTxn(tablesToModify[0], tablesToModify[1:]...).getTxn() 89 for meta, deadObjs := range toBeDeleted { 90 tableName := meta.Name() 91 start := time.Now() 92 for _, key := range deadObjs { 93 oldObj, existed := txn.mustIndexWriteTxn(meta, GraveyardRevisionIndexPos).Delete(key) 94 if existed { 95 // The dead object still existed (and wasn't replaced by a create->delete), 96 // delete it from the primary index. 97 key = meta.primary().fromObject(oldObj).First() 98 txn.mustIndexWriteTxn(meta, GraveyardIndexPos).Delete(key) 99 } 100 } 101 cleaningTimes[tableName] = time.Since(start) 102 } 103 txn.Commit() 104 105 for tableName, stat := range cleaningTimes { 106 db.metrics.GraveyardCleaningDuration( 107 tableName, 108 stat, 109 ) 110 } 111 112 // Update object count metrics. 113 txn = db.ReadTxn().getTxn() 114 for _, table := range txn.root { 115 name := table.meta.Name() 116 db.metrics.GraveyardObjectCount(string(name), table.numDeletedObjects()) 117 db.metrics.ObjectCount(string(name), table.numObjects()) 118 } 119 } 120 } 121 122 // graveyardIsEmpty returns true if no objects exist in the graveyard of any table. 123 // Used in tests. 124 func (db *DB) graveyardIsEmpty() bool { 125 txn := db.ReadTxn().getTxn() 126 for _, table := range txn.root { 127 indexEntry := table.indexes[table.meta.indexPos(GraveyardIndex)] 128 if indexEntry.tree.Len() != 0 { 129 return false 130 } 131 } 132 return true 133 }