github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/aagent/watchers/schedulewatcher/schedule.go (about) 1 // Copyright (c) 2019-2024, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package schedulewatcher 6 7 import ( 8 "context" 9 "fmt" 10 "sync" 11 "time" 12 13 "github.com/choria-io/go-choria/aagent/model" 14 "github.com/choria-io/go-choria/aagent/util" 15 "github.com/choria-io/go-choria/aagent/watchers/event" 16 "github.com/choria-io/go-choria/aagent/watchers/watcher" 17 ) 18 19 type State int 20 21 const ( 22 Unknown State = iota 23 Off 24 On 25 Skipped 26 27 wtype = "schedule" 28 version = "v1" 29 ) 30 31 var stateNames = map[State]string{ 32 Unknown: "unknown", 33 Off: "off", 34 On: "on", 35 Skipped: "skipped", 36 } 37 38 type properties struct { 39 Duration time.Duration 40 StartSplay time.Duration `mapstructure:"start_splay"` 41 SkipTriggerOnReenter bool `mapstructure:"skip_trigger_on_reenter"` 42 Schedules []string 43 } 44 45 type Watcher struct { 46 *watcher.Watcher 47 properties *properties 48 name string 49 machine model.Machine 50 items []*scheduleItem 51 52 // each item sends a 1 or -1 into this to increment or decrement the counter 53 // when the ctr is > 0 the switch should be on, this handles multiple schedules 54 // overlapping and keeping it on for longer than a single schedule would be 55 ctrq chan int 56 ctr int 57 58 triggered bool 59 60 state State 61 previousState State 62 63 mu *sync.Mutex 64 } 65 66 func New(machine model.Machine, name string, states []string, failEvent string, successEvent string, interval string, ai time.Duration, properties map[string]any) (any, error) { 67 var err error 68 69 sw := &Watcher{ 70 name: name, 71 machine: machine, 72 ctrq: make(chan int, 1), 73 ctr: 0, 74 mu: &sync.Mutex{}, 75 } 76 77 sw.Watcher, err = watcher.NewWatcher(name, wtype, ai, states, machine, failEvent, successEvent) 78 if err != nil { 79 return nil, err 80 } 81 82 err = sw.setProperties(properties) 83 if err != nil { 84 return nil, fmt.Errorf("could not set properties: %s", err) 85 } 86 87 return sw, nil 88 } 89 90 func (w *Watcher) watchSchedule(ctx context.Context, wg *sync.WaitGroup) { 91 defer wg.Done() 92 93 for { 94 select { 95 case i := <-w.ctrq: 96 w.Debugf("Handling state change counter %v while ctr=%v", i, w.ctr) 97 w.mu.Lock() 98 99 w.ctr = w.ctr + i 100 101 // shouldn't happen but lets handle it 102 if w.ctr < 0 { 103 w.ctr = 0 104 } 105 106 if w.ctr == 0 { 107 w.Debugf("State going off due to ctr change to 0") 108 w.state = Off 109 } else { 110 w.Debugf("State going on due to ctr change of %v", i) 111 w.state = On 112 } 113 114 w.mu.Unlock() 115 116 case <-ctx.Done(): 117 return 118 } 119 } 120 } 121 122 func (w *Watcher) setPreviousState(s State) { 123 w.mu.Lock() 124 defer w.mu.Unlock() 125 126 w.previousState = s 127 } 128 129 func (w *Watcher) watch() (err error) { 130 if !w.ShouldWatch() { 131 w.setPreviousState(Skipped) 132 133 return nil 134 } 135 136 // nothing changed 137 if w.previousState == w.state { 138 return nil 139 } 140 141 w.setPreviousState(w.state) 142 143 switch w.state { 144 case Off, Unknown: 145 w.setTriggered(false) 146 w.NotifyWatcherState(w.CurrentState()) 147 return w.FailureTransition() 148 149 case On: 150 if w.properties.SkipTriggerOnReenter && w.didTrigger() { 151 w.Debugf("Skipping success transition that's already fired in this schedule due to skip_trigger_on_reenter") 152 return nil 153 } 154 155 w.setTriggered(true) 156 w.setPreviousState(w.state) 157 w.NotifyWatcherState(w.CurrentState()) 158 return w.SuccessTransition() 159 160 case Skipped: 161 // not doing anything when we aren't eligible, regular announces happen 162 163 } 164 165 return nil 166 } 167 168 func (w *Watcher) setTriggered(s bool) { 169 w.mu.Lock() 170 w.triggered = s 171 w.mu.Unlock() 172 } 173 174 func (w *Watcher) didTrigger() bool { 175 w.mu.Lock() 176 defer w.mu.Unlock() 177 178 return w.triggered 179 } 180 181 func (w *Watcher) Run(ctx context.Context, wg *sync.WaitGroup) { 182 defer wg.Done() 183 184 w.Infof("schedule watcher starting with %d items", len(w.items)) 185 186 wg.Add(1) 187 go w.watchSchedule(ctx, wg) 188 189 for _, item := range w.items { 190 wg.Add(1) 191 go item.start(ctx, wg) 192 } 193 194 tick := time.NewTicker(500 * time.Millisecond) 195 196 for { 197 select { 198 case <-tick.C: 199 err := w.watch() 200 if err != nil { 201 w.Errorf("Could not handle current scheduler state: %s", err) 202 } 203 204 case <-w.StateChangeC(): 205 err := w.watch() 206 if err != nil { 207 w.Errorf("Could not handle current scheduler state: %s", err) 208 } 209 210 case <-ctx.Done(): 211 tick.Stop() 212 w.Infof("Stopping on context interrupt") 213 return 214 } 215 } 216 } 217 218 func (w *Watcher) validate() error { 219 if w.properties.Duration < time.Second { 220 w.properties.Duration = time.Minute 221 } 222 223 if len(w.properties.Schedules) == 0 { 224 return fmt.Errorf("no schedules defined") 225 } 226 227 return nil 228 } 229 230 func (w *Watcher) setProperties(props map[string]any) error { 231 if w.properties == nil { 232 w.properties = &properties{ 233 Schedules: []string{}, 234 } 235 } 236 237 err := util.ParseMapStructure(props, w.properties) 238 if err != nil { 239 return err 240 } 241 242 for _, spec := range w.properties.Schedules { 243 item, err := newSchedItem(spec, w) 244 if err != nil { 245 return fmt.Errorf("could not parse '%s': %s", spec, err) 246 } 247 248 w.items = append(w.items, item) 249 } 250 251 if w.properties.StartSplay > w.properties.Duration/2 { 252 return fmt.Errorf("start splay %v is bigger than half the duration %v", w.properties.StartSplay, w.properties.Duration) 253 } 254 255 return w.validate() 256 } 257 258 func (w *Watcher) CurrentState() any { 259 w.mu.Lock() 260 defer w.mu.Unlock() 261 262 s := &StateNotification{ 263 Event: event.New(w.name, wtype, version, w.machine), 264 State: stateNames[w.state], 265 } 266 267 return s 268 }