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 }