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  }