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

     1  // Copyright (c) 2019-2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package watchers
     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/internal/util"
    15  	"github.com/nats-io/jsm.go"
    16  	"github.com/tidwall/gjson"
    17  )
    18  
    19  type State int
    20  
    21  // Machine is a Choria Machine
    22  type Machine interface {
    23  	model.Machine
    24  	Watchers() []*WatcherDef
    25  }
    26  
    27  // Manager manages all the defined watchers in a specific machine
    28  // implements machine.WatcherManager
    29  type Manager struct {
    30  	watchers map[string]model.Watcher
    31  	machine  Machine
    32  
    33  	ctx    context.Context
    34  	cancel func()
    35  
    36  	sync.Mutex
    37  }
    38  
    39  var (
    40  	plugins map[string]model.WatcherConstructor
    41  
    42  	mu sync.Mutex
    43  )
    44  
    45  // RegisterWatcherPlugin registers a new type of watcher
    46  func RegisterWatcherPlugin(name string, plugin model.WatcherConstructor) error {
    47  	mu.Lock()
    48  	defer mu.Unlock()
    49  
    50  	if plugins == nil {
    51  		plugins = map[string]model.WatcherConstructor{}
    52  	}
    53  
    54  	_, exist := plugins[plugin.Type()]
    55  	if exist {
    56  		return fmt.Errorf("plugin %q already exist", plugin.Type())
    57  	}
    58  
    59  	plugins[plugin.Type()] = plugin
    60  
    61  	util.BuildInfo().RegisterMachineWatcher(name)
    62  
    63  	return nil
    64  }
    65  
    66  func New(ctx context.Context) *Manager {
    67  	m := &Manager{
    68  		watchers: make(map[string]model.Watcher),
    69  	}
    70  
    71  	m.ctx, m.cancel = context.WithCancel(ctx)
    72  
    73  	return m
    74  }
    75  
    76  func ParseWatcherState(state []byte) (any, error) {
    77  	r := gjson.GetBytes(state, "protocol")
    78  	if !r.Exists() {
    79  		return nil, fmt.Errorf("no protocol header in state json")
    80  	}
    81  
    82  	proto := r.String()
    83  	var plugin model.WatcherConstructor
    84  
    85  	mu.Lock()
    86  	for _, w := range plugins {
    87  		if w.EventType() == proto {
    88  			plugin = w
    89  		}
    90  	}
    91  	mu.Unlock()
    92  
    93  	if plugin == nil {
    94  		return nil, fmt.Errorf("unknown event type %q", proto)
    95  	}
    96  
    97  	return plugin.UnmarshalNotification(state)
    98  }
    99  
   100  // Delete gets called before a watcher is being deleted after
   101  // its files were removed from disk
   102  func (m *Manager) Delete() {
   103  	m.machine.Infof(m.machine.Name(), "Stopping manager")
   104  	m.cancel()
   105  
   106  	m.Lock()
   107  	defer m.Unlock()
   108  
   109  	for _, w := range m.watchers {
   110  		w.Delete()
   111  	}
   112  }
   113  
   114  // JetStreamConnection is a NATS connection for accessing the JetStream API
   115  func (m *Manager) JetStreamConnection() (*jsm.Manager, error) {
   116  	m.Lock()
   117  	defer m.Unlock()
   118  
   119  	return m.machine.JetStreamConnection()
   120  }
   121  
   122  // SetMachine supplies the machine this manager will manage
   123  func (m *Manager) SetMachine(t any) (err error) {
   124  	machine, ok := t.(Machine)
   125  	if !ok {
   126  		return fmt.Errorf("supplied machine does not implement watchers.Machine")
   127  	}
   128  
   129  	m.machine = machine
   130  
   131  	return nil
   132  }
   133  
   134  // AddWatcher adds a watcher to a managed machine
   135  func (m *Manager) AddWatcher(w model.Watcher) error {
   136  	m.Lock()
   137  	defer m.Unlock()
   138  
   139  	_, ok := m.watchers[w.Name()]
   140  	if ok {
   141  		m.machine.Errorf("manager", "Already have a watcher %s", w.Name())
   142  		return fmt.Errorf("watcher %s already exist", w.Name())
   143  	}
   144  
   145  	m.watchers[w.Name()] = w
   146  
   147  	return nil
   148  }
   149  
   150  // WatcherState retrieves the current status for a given watcher, boolean result is false for unknown watchers
   151  func (m *Manager) WatcherState(watcher string) (any, bool) {
   152  	m.Lock()
   153  	defer m.Unlock()
   154  	w, ok := m.watchers[watcher]
   155  	if !ok {
   156  		return nil, false
   157  	}
   158  
   159  	return w.CurrentState(), true
   160  }
   161  
   162  func (m *Manager) configureWatchers() (err error) {
   163  	for _, w := range m.machine.Watchers() {
   164  		err = w.ParseAnnounceInterval()
   165  		if err != nil {
   166  			return fmt.Errorf("could not create %s watcher '%s': %s", w.Type, w.Name, err)
   167  		}
   168  
   169  		m.machine.Infof("manager", "Starting %s watcher %s", w.Type, w.Name)
   170  
   171  		var watcher model.Watcher
   172  		var err error
   173  		var ok bool
   174  
   175  		mu.Lock()
   176  		plugin, known := plugins[w.Type]
   177  		mu.Unlock()
   178  		if !known {
   179  			return fmt.Errorf("unknown watcher '%s'", w.Type)
   180  		}
   181  
   182  		wi, err := plugin.New(m.machine, w.Name, w.StateMatch, w.FailTransition, w.SuccessTransition, w.Interval, w.AnnounceDuration, w.Properties)
   183  		if err != nil {
   184  			return fmt.Errorf("could not create %s watcher '%s': %s", w.Type, w.Name, err)
   185  		}
   186  
   187  		watcher, ok = wi.(model.Watcher)
   188  		if !ok {
   189  			return fmt.Errorf("%q watcher is not a valid watcher", w.Type)
   190  		}
   191  
   192  		err = m.AddWatcher(watcher)
   193  		if err != nil {
   194  			return err
   195  		}
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  // Run starts all the defined watchers and periodically announce
   202  // their state based on AnnounceInterval
   203  func (m *Manager) Run(ctx context.Context, wg *sync.WaitGroup) error {
   204  	if m.machine == nil {
   205  		return fmt.Errorf("manager requires a machine to manage")
   206  	}
   207  
   208  	err := m.configureWatchers()
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	for _, watcher := range m.watchers {
   214  		wg.Add(1)
   215  		go watcher.Run(ctx, wg)
   216  
   217  		if watcher.AnnounceInterval() > 0 {
   218  			wg.Add(1)
   219  			go m.announceWatcherState(ctx, wg, watcher)
   220  		}
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  func (m *Manager) announceWatcherState(ctx context.Context, wg *sync.WaitGroup, w model.Watcher) {
   227  	defer wg.Done()
   228  
   229  	announceTick := time.NewTicker(w.AnnounceInterval())
   230  
   231  	for {
   232  		select {
   233  		case <-announceTick.C:
   234  			m.machine.NotifyWatcherState(w.Name(), w.CurrentState())
   235  		case <-ctx.Done():
   236  			m.machine.Infof("manager", "Stopping on context interrupt")
   237  			return
   238  		}
   239  	}
   240  }
   241  
   242  // NotifyStateChance implements machine.WatcherManager
   243  func (m *Manager) NotifyStateChance() {
   244  	m.Lock()
   245  	defer m.Unlock()
   246  
   247  	for _, watcher := range m.watchers {
   248  		watcher.NotifyStateChance()
   249  	}
   250  }