github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/meterstatus/isolated.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package meterstatus
     5  
     6  import (
     7  	"time"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/worker/v3"
    11  	"gopkg.in/tomb.v2"
    12  )
    13  
    14  const (
    15  	// defaultAmberGracePeriod is the time that the unit is allowed to
    16  	// function without a working API connection before its meter
    17  	// status is switched to AMBER.
    18  	defaultAmberGracePeriod = time.Minute * 5
    19  
    20  	// defaultRedGracePeriod is the time that a unit is allowed to function
    21  	// without a working API connection before its meter status is
    22  	// switched to RED.
    23  	defaultRedGracePeriod = time.Minute * 15
    24  )
    25  
    26  // workerState defines all the possible states the isolatedStatusWorker can be in.
    27  type WorkerState int
    28  
    29  const (
    30  	Uninitialized WorkerState = iota
    31  	WaitingAmber              // Waiting for a signal to switch to AMBER status.
    32  	WaitingRed                // Waiting for a signal to switch to RED status.
    33  	Done                      // No more transitions to perform.
    34  )
    35  
    36  // IsolatedConfig stores all the dependencies required to create an isolated meter status worker.
    37  type IsolatedConfig struct {
    38  	Runner           HookRunner
    39  	StateReadWriter  StateReadWriter
    40  	Clock            Clock
    41  	Logger           Logger
    42  	AmberGracePeriod time.Duration
    43  	RedGracePeriod   time.Duration
    44  	TriggerFactory   TriggerCreator
    45  }
    46  
    47  // Validate validates the config structure and returns an error on failure.
    48  func (c IsolatedConfig) Validate() error {
    49  	if c.Runner == nil {
    50  		return errors.NotValidf("missing Runner")
    51  	}
    52  	if c.StateReadWriter == nil {
    53  		return errors.NotValidf("missing StateReadWriter")
    54  	}
    55  	if c.Clock == nil {
    56  		return errors.NotValidf("missing Clock")
    57  	}
    58  	if c.Logger == nil {
    59  		return errors.NotValidf("missing Logger")
    60  	}
    61  	if c.AmberGracePeriod <= 0 {
    62  		return errors.NotValidf("amber grace period")
    63  	}
    64  	if c.RedGracePeriod <= 0 {
    65  		return errors.NotValidf("red grace period")
    66  	}
    67  	if c.AmberGracePeriod >= c.RedGracePeriod {
    68  		return errors.New("amber grace period must be shorter than the red grace period")
    69  	}
    70  	return nil
    71  }
    72  
    73  // isolatedStatusWorker is a worker that is instantiated by the
    74  // meter status manifold when the API connection is unavailable.
    75  // Its main function is to escalate the meter status of the unit
    76  // to amber and later to red.
    77  type isolatedStatusWorker struct {
    78  	config IsolatedConfig
    79  
    80  	tomb tomb.Tomb
    81  }
    82  
    83  // NewIsolatedStatusWorker creates a new status worker that runs without an API connection.
    84  func NewIsolatedStatusWorker(cfg IsolatedConfig) (worker.Worker, error) {
    85  	if err := cfg.Validate(); err != nil {
    86  		return nil, errors.Trace(err)
    87  	}
    88  	w := &isolatedStatusWorker{
    89  		config: cfg,
    90  	}
    91  	w.tomb.Go(w.loop)
    92  	return w, nil
    93  }
    94  
    95  func (w *isolatedStatusWorker) loop() error {
    96  	st, err := w.config.StateReadWriter.Read()
    97  	if err != nil {
    98  		if !errors.IsNotFound(err) {
    99  			return errors.Trace(err)
   100  		}
   101  
   102  		// No state found; create a blank instance.
   103  		st = new(State)
   104  	}
   105  
   106  	// Disconnected time has not been recorded yet.
   107  	if st.Disconnected == nil {
   108  		st.Disconnected = &Disconnected{w.config.Clock.Now().Unix(), WaitingAmber}
   109  	}
   110  
   111  	amberSignal, redSignal := w.config.TriggerFactory(st.Disconnected.State, st.Code, st.Disconnected.When(), w.config.Clock, w.config.AmberGracePeriod, w.config.RedGracePeriod)
   112  	for {
   113  		select {
   114  		case <-w.tomb.Dying():
   115  			return tomb.ErrDying
   116  		case <-redSignal:
   117  			w.config.Logger.Debugf("triggering meter status transition to RED due to loss of connection")
   118  			currentCode := "RED"
   119  			currentInfo := "unit agent has been disconnected"
   120  
   121  			w.applyStatus(currentCode, currentInfo)
   122  			st.Code, st.Info = currentCode, currentInfo
   123  			st.Disconnected.State = Done
   124  		case <-amberSignal:
   125  			w.config.Logger.Debugf("triggering meter status transition to AMBER due to loss of connection")
   126  			currentCode := "AMBER"
   127  			currentInfo := "unit agent has been disconnected"
   128  
   129  			w.applyStatus(currentCode, currentInfo)
   130  			st.Code, st.Info = currentCode, currentInfo
   131  			st.Disconnected.State = WaitingRed
   132  		}
   133  		if err := w.config.StateReadWriter.Write(st); err != nil {
   134  			return errors.Annotate(err, "failed to record meter status worker state")
   135  		}
   136  	}
   137  }
   138  
   139  func (w *isolatedStatusWorker) applyStatus(code, info string) {
   140  	w.config.Logger.Tracef("applying meter status change: %q (%q)", code, info)
   141  	w.config.Runner.RunHook(code, info, w.tomb.Dying())
   142  }
   143  
   144  // Kill is part of the worker.Worker interface.
   145  func (w *isolatedStatusWorker) Kill() {
   146  	w.tomb.Kill(nil)
   147  }
   148  
   149  // Wait is part of the worker.Worker interface.
   150  func (w *isolatedStatusWorker) Wait() error {
   151  	return w.tomb.Wait()
   152  }