github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/aagent/watchers/timerwatcher/timer.go (about)

     1  // Copyright (c) 2020-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package timerwatcher
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"math/rand"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/choria-io/go-choria/aagent/model"
    15  	"github.com/choria-io/go-choria/aagent/util"
    16  	"github.com/choria-io/go-choria/aagent/watchers/event"
    17  	"github.com/choria-io/go-choria/aagent/watchers/watcher"
    18  )
    19  
    20  type State int
    21  
    22  const (
    23  	Stopped State = iota
    24  	Running
    25  
    26  	wtype   = "timer"
    27  	version = "v1"
    28  )
    29  
    30  var stateNames = map[State]string{
    31  	Running: "running",
    32  	Stopped: "stopped",
    33  }
    34  
    35  type properties struct {
    36  	Timer time.Duration
    37  	Splay bool
    38  }
    39  
    40  type Watcher struct {
    41  	*watcher.Watcher
    42  	properties *properties
    43  
    44  	name    string
    45  	machine model.Machine
    46  	state   State
    47  
    48  	terminate   chan struct{}
    49  	cancelTimer func()
    50  	mu          *sync.Mutex
    51  }
    52  
    53  func New(machine model.Machine, name string, states []string, failEvent string, successEvent string, interval string, ai time.Duration, properties map[string]any) (any, error) {
    54  	var err error
    55  
    56  	if successEvent != "" {
    57  		return nil, fmt.Errorf("timer watcher does not support success events")
    58  	}
    59  
    60  	tw := &Watcher{
    61  		name:      name,
    62  		machine:   machine,
    63  		state:     0,
    64  		terminate: make(chan struct{}),
    65  		mu:        &sync.Mutex{},
    66  	}
    67  
    68  	tw.Watcher, err = watcher.NewWatcher(name, wtype, ai, states, machine, failEvent, successEvent)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	err = tw.setProperties(properties)
    74  	if err != nil {
    75  		return nil, fmt.Errorf("could not set properties: %s", err)
    76  	}
    77  
    78  	return tw, nil
    79  }
    80  
    81  func (w *Watcher) Delete() {
    82  	close(w.terminate)
    83  }
    84  
    85  func (w *Watcher) forceTimerStop() {
    86  	w.mu.Lock()
    87  	cancel := w.cancelTimer
    88  	w.mu.Unlock()
    89  
    90  	if cancel != nil {
    91  		w.Infof("Stopping timer early on state transition to %s", w.machine.State())
    92  		cancel()
    93  	}
    94  }
    95  
    96  func (w *Watcher) timeStart() {
    97  	w.mu.Lock()
    98  	cancel := w.cancelTimer
    99  	w.mu.Unlock()
   100  
   101  	if cancel != nil {
   102  		w.Infof("Timer was running, resetting to %v", w.properties.Timer)
   103  		cancel()
   104  	}
   105  
   106  	go func() {
   107  		timer := time.NewTimer(w.properties.Timer)
   108  		ctx, cancel := context.WithCancel(context.Background())
   109  
   110  		w.mu.Lock()
   111  		w.state = Running
   112  		w.cancelTimer = cancel
   113  		w.mu.Unlock()
   114  
   115  		w.NotifyWatcherState(w.CurrentState())
   116  
   117  		select {
   118  		case <-timer.C:
   119  			w.mu.Lock()
   120  			w.state = Stopped
   121  			if w.cancelTimer != nil {
   122  				w.cancelTimer()
   123  			}
   124  			w.cancelTimer = nil
   125  			w.mu.Unlock()
   126  
   127  			w.NotifyWatcherState(w.CurrentState())
   128  			w.FailureTransition()
   129  
   130  		case <-ctx.Done():
   131  			w.mu.Lock()
   132  			w.cancelTimer = nil
   133  			timer.Stop()
   134  			w.state = Stopped
   135  			w.mu.Unlock()
   136  
   137  			w.NotifyWatcherState(w.CurrentState())
   138  
   139  		case <-w.terminate:
   140  			w.mu.Lock()
   141  			w.cancelTimer = nil
   142  			w.state = Stopped
   143  			w.mu.Unlock()
   144  			return
   145  		}
   146  	}()
   147  }
   148  
   149  func (w *Watcher) watch() {
   150  	if !w.ShouldWatch() {
   151  		w.forceTimerStop()
   152  		return
   153  	}
   154  
   155  	w.Infof("Starting timer")
   156  	w.timeStart()
   157  }
   158  
   159  func (w *Watcher) Run(ctx context.Context, wg *sync.WaitGroup) {
   160  	defer wg.Done()
   161  
   162  	w.Infof("Timer watcher starting with %v timer", w.properties.Timer)
   163  
   164  	// handle initial state
   165  	w.watch()
   166  
   167  	for {
   168  		select {
   169  		case <-w.StateChangeC():
   170  			w.watch()
   171  		case <-w.terminate:
   172  			w.Infof("Handling terminate notification")
   173  			return
   174  		case <-ctx.Done():
   175  			w.Infof("Stopping on context interrupt")
   176  			return
   177  		}
   178  	}
   179  }
   180  
   181  func (w *Watcher) validate() error {
   182  	if w.properties.Timer < time.Second {
   183  		w.properties.Timer = time.Second
   184  	}
   185  
   186  	if w.properties.Splay {
   187  		w.properties.Timer = time.Duration(rand.Int63n(int64(w.properties.Timer)))
   188  		w.Infof("Adjusting timer to %v due to splay setting", w.properties.Timer)
   189  	}
   190  
   191  	return nil
   192  }
   193  
   194  func (w *Watcher) setProperties(props map[string]any) error {
   195  	if w.properties == nil {
   196  		w.properties = &properties{}
   197  	}
   198  
   199  	err := util.ParseMapStructure(props, w.properties)
   200  	if err != nil {
   201  		return err
   202  	}
   203  
   204  	return w.validate()
   205  }
   206  
   207  func (w *Watcher) CurrentState() any {
   208  	w.mu.Lock()
   209  	defer w.mu.Unlock()
   210  
   211  	s := &StateNotification{
   212  		Event: event.New(w.name, wtype, version, w.machine),
   213  		State: stateNames[w.state],
   214  		Timer: w.properties.Timer,
   215  	}
   216  
   217  	return s
   218  }