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  }