github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/leaseexpiry/worker.go (about) 1 // Copyright 2022 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package leaseexpiry 5 6 import ( 7 "context" 8 "database/sql" 9 "time" 10 11 "github.com/juju/clock" 12 "github.com/juju/errors" 13 "github.com/juju/worker/v3" 14 "gopkg.in/tomb.v2" 15 16 coredatabase "github.com/juju/juju/core/database" 17 "github.com/juju/juju/database/txn" 18 ) 19 20 // Config encapsulates the configuration options for 21 // instantiating a new lease expiry worker. 22 type Config struct { 23 Clock clock.Clock 24 Logger Logger 25 TrackedDB coredatabase.TrackedDB 26 } 27 28 // Validate checks whether the worker configuration settings are valid. 29 func (cfg Config) Validate() error { 30 if cfg.Clock == nil { 31 return errors.NotValidf("nil Clock") 32 } 33 if cfg.Logger == nil { 34 return errors.NotValidf("nil Logger") 35 } 36 if cfg.TrackedDB == nil { 37 return errors.NotValidf("nil TrackedDB") 38 } 39 40 return nil 41 } 42 43 type expiryWorker struct { 44 tomb tomb.Tomb 45 46 clock clock.Clock 47 logger Logger 48 trackedDB coredatabase.TrackedDB 49 dml string 50 } 51 52 // NewWorker returns a worker that periodically deletes 53 // expired leases from the controller database. 54 func NewWorker(cfg Config) (worker.Worker, error) { 55 var err error 56 57 if err = cfg.Validate(); err != nil { 58 return nil, errors.Trace(err) 59 } 60 61 w := &expiryWorker{ 62 clock: cfg.Clock, 63 logger: cfg.Logger, 64 trackedDB: cfg.TrackedDB, 65 dml: ` 66 DELETE FROM lease WHERE uuid in ( 67 SELECT l.uuid 68 FROM lease l LEFT JOIN lease_pin p ON l.uuid = p.lease_uuid 69 WHERE p.uuid IS NULL 70 AND l.expiry < datetime('now') 71 )`[1:], 72 } 73 74 w.tomb.Go(w.loop) 75 return w, nil 76 77 } 78 79 func (w *expiryWorker) loop() error { 80 timer := w.clock.NewTimer(time.Second) 81 defer timer.Stop() 82 83 // We pass this context to every database method that accepts one. 84 // It is cancelled by killing the tomb, which prevents shutdown 85 // being blocked by such calls. 86 ctx := w.tomb.Context(context.Background()) 87 88 for { 89 select { 90 case <-w.tomb.Dying(): 91 return tomb.ErrDying 92 case <-timer.Chan(): 93 if err := w.expireLeases(ctx); err != nil { 94 return errors.Trace(err) 95 } 96 timer.Reset(time.Second) 97 } 98 } 99 } 100 101 func (w *expiryWorker) expireLeases(ctx context.Context) error { 102 err := w.trackedDB.TxnNoRetry(ctx, func(ctx context.Context, tx *sql.Tx) error { 103 res, err := tx.ExecContext(ctx, w.dml) 104 if err != nil { 105 // TODO (manadart 2022-12-15): This incarnation of the worker runs on 106 // all controller nodes. Retryable errors are those that occur due to 107 // locking or other contention. We know we will retry very soon, 108 // so just log and indicate success for these cases. 109 // Rethink this if the worker cardinality changes to be singular. 110 if txn.IsErrRetryable(err) { 111 w.logger.Debugf("ignoring error during lease expiry: %s", err.Error()) 112 return nil 113 } 114 return errors.Trace(err) 115 } 116 117 expired, err := res.RowsAffected() 118 if err != nil { 119 return errors.Trace(err) 120 } 121 122 if expired > 0 { 123 w.logger.Infof("expired %d leases", expired) 124 } 125 126 return nil 127 }) 128 if err != nil { 129 return errors.Trace(err) 130 } 131 return errors.Trace(w.trackedDB.Err()) 132 } 133 134 // Kill is part of the worker.Worker interface. 135 func (w *expiryWorker) Kill() { 136 w.tomb.Kill(nil) 137 } 138 139 // Wait is part of the worker.Worker interface. 140 func (w *expiryWorker) Wait() error { 141 return w.tomb.Wait() 142 }