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 }