github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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/clock" 10 "github.com/juju/errors" 11 "gopkg.in/juju/charm.v6/hooks" 12 "gopkg.in/juju/worker.v1" 13 "gopkg.in/tomb.v2" 14 15 "github.com/juju/juju/worker/common/charmrunner" 16 ) 17 18 const ( 19 // defaultAmberGracePeriod is the time that the unit is allowed to 20 // function without a working API connection before its meter 21 // status is switched to AMBER. 22 defaultAmberGracePeriod = time.Minute * 5 23 24 // defaultRedGracePeriod is the time that a unit is allowed to function 25 // without a working API connection before its meter status is 26 // switched to RED. 27 defaultRedGracePeriod = time.Minute * 15 28 ) 29 30 // workerState defines all the possible states the isolatedStatusWorker can be in. 31 type WorkerState int 32 33 const ( 34 Uninitialized WorkerState = iota 35 WaitingAmber // Waiting for a signal to switch to AMBER status. 36 WaitingRed // Waiting for a signal to switch to RED status. 37 Done // No more transitions to perform. 38 ) 39 40 // IsolatedConfig stores all the dependencies required to create an isolated meter status worker. 41 type IsolatedConfig struct { 42 Runner HookRunner 43 StateFile *StateFile 44 Clock clock.Clock 45 AmberGracePeriod time.Duration 46 RedGracePeriod time.Duration 47 TriggerFactory TriggerCreator 48 } 49 50 // Validate validates the config structure and returns an error on failure. 51 func (c IsolatedConfig) Validate() error { 52 if c.Runner == nil { 53 return errors.New("hook runner not provided") 54 } 55 if c.StateFile == nil { 56 return errors.New("state file not provided") 57 } 58 if c.Clock == nil { 59 return errors.New("clock not provided") 60 } 61 if c.AmberGracePeriod <= 0 { 62 return errors.New("invalid amber grace period") 63 } 64 if c.RedGracePeriod <= 0 { 65 return errors.New("invalid 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 code, info, disconnected, err := w.config.StateFile.Read() 97 if err != nil { 98 return errors.Trace(err) 99 } 100 101 // Disconnected time has not been recorded yet. 102 if disconnected == nil { 103 disconnected = &Disconnected{w.config.Clock.Now().Unix(), WaitingAmber} 104 } 105 106 amberSignal, redSignal := w.config.TriggerFactory(disconnected.State, code, disconnected.When(), w.config.Clock, w.config.AmberGracePeriod, w.config.RedGracePeriod) 107 for { 108 select { 109 case <-w.tomb.Dying(): 110 return tomb.ErrDying 111 case <-redSignal: 112 logger.Debugf("triggering meter status transition to RED due to loss of connection") 113 currentCode := "RED" 114 currentInfo := "unit agent has been disconnected" 115 116 w.applyStatus(currentCode, currentInfo) 117 code, info = currentCode, currentInfo 118 disconnected.State = Done 119 case <-amberSignal: 120 logger.Debugf("triggering meter status transition to AMBER due to loss of connection") 121 currentCode := "AMBER" 122 currentInfo := "unit agent has been disconnected" 123 124 w.applyStatus(currentCode, currentInfo) 125 code, info = currentCode, currentInfo 126 disconnected.State = WaitingRed 127 } 128 err := w.config.StateFile.Write(code, info, disconnected) 129 if err != nil { 130 return errors.Annotate(err, "failed to record meter status worker state") 131 } 132 } 133 } 134 135 func (w *isolatedStatusWorker) applyStatus(code, info string) { 136 logger.Tracef("applying meter status change: %q (%q)", code, info) 137 err := w.config.Runner.RunHook(code, info, w.tomb.Dying()) 138 cause := errors.Cause(err) 139 switch { 140 case charmrunner.IsMissingHookError(cause): 141 logger.Infof("skipped %q hook (missing)", string(hooks.MeterStatusChanged)) 142 case err != nil: 143 logger.Errorf("meter status worker encountered hook error: %v", err) 144 } 145 } 146 147 // Kill is part of the worker.Worker interface. 148 func (w *isolatedStatusWorker) Kill() { 149 w.tomb.Kill(nil) 150 } 151 152 // Wait is part of the worker.Worker interface. 153 func (w *isolatedStatusWorker) Wait() error { 154 return w.tomb.Wait() 155 }