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  }