github.com/safing/portbase@v0.19.5/modules/modules.go (about) 1 package modules 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "sync/atomic" 9 "time" 10 11 "github.com/tevino/abool" 12 13 "github.com/safing/portbase/log" 14 ) 15 16 var ( 17 modules = make(map[string]*Module) 18 mgmtLock sync.Mutex 19 20 // modulesLocked locks `modules` during starting. 21 modulesLocked = abool.New() 22 23 sleepMode = abool.NewBool(false) 24 25 moduleStartTimeout = 2 * time.Minute 26 moduleStopTimeout = 1 * time.Minute 27 28 // ErrCleanExit is returned by Start() when the program is interrupted before starting. This can happen for example, when using the "--help" flag. 29 ErrCleanExit = errors.New("clean exit requested") 30 ) 31 32 // Module represents a module. 33 type Module struct { //nolint:maligned 34 sync.RWMutex 35 36 Name string 37 38 // status mgmt 39 enabled *abool.AtomicBool 40 enabledAsDependency *abool.AtomicBool 41 status uint8 42 sleepMode *abool.AtomicBool 43 sleepWaitingChannel chan time.Time 44 45 // failure status 46 failureStatus uint8 47 failureID string 48 failureTitle string 49 failureMsg string 50 51 // lifecycle callback functions 52 prepFn func() error 53 startFn func() error 54 stopFn func() error 55 56 // lifecycle mgmt 57 // start 58 startComplete chan struct{} 59 // stop 60 Ctx context.Context 61 cancelCtx func() 62 stopFlag *abool.AtomicBool 63 stopCompleted *abool.AtomicBool 64 stopComplete chan struct{} 65 66 // workers/tasks 67 ctrlFuncRunning *abool.AtomicBool 68 workerCnt *int32 69 taskCnt *int32 70 microTaskCnt *int32 71 72 // events 73 eventHooks map[string]*eventHooks 74 eventHooksLock sync.RWMutex 75 76 // dependency mgmt 77 depNames []string 78 depModules []*Module 79 depReverse []*Module 80 } 81 82 // StartCompleted returns a channel read that triggers when the module has finished starting. 83 func (m *Module) StartCompleted() <-chan struct{} { 84 m.RLock() 85 defer m.RUnlock() 86 return m.startComplete 87 } 88 89 // Stopping returns a channel read that triggers when the module has initiated the stop procedure. 90 func (m *Module) Stopping() <-chan struct{} { 91 m.RLock() 92 defer m.RUnlock() 93 return m.Ctx.Done() 94 } 95 96 // IsStopping returns whether the module has started shutting down. In most cases, you should use Stopping instead. 97 func (m *Module) IsStopping() bool { 98 return m.stopFlag.IsSet() 99 } 100 101 // Dependencies returns the module's dependencies. 102 func (m *Module) Dependencies() []*Module { 103 m.RLock() 104 defer m.RUnlock() 105 return m.depModules 106 } 107 108 // Sleep enables or disables sleep mode. 109 func (m *Module) Sleep(enable bool) { 110 set := m.sleepMode.SetToIf(!enable, enable) 111 if !set { 112 return 113 } 114 115 m.Lock() 116 defer m.Unlock() 117 118 if enable { 119 m.sleepWaitingChannel = make(chan time.Time) 120 } else { 121 // Notify all waiting tasks that we are not sleeping anymore. 122 close(m.sleepWaitingChannel) 123 } 124 } 125 126 // IsSleeping returns true if sleep mode is enabled. 127 func (m *Module) IsSleeping() bool { 128 return m.sleepMode.IsSet() 129 } 130 131 // WaitIfSleeping returns channel that will signal when it exits sleep mode. 132 // The channel will always return a zero-value time.Time. 133 // It uses time.Time to be easier dropped in to replace a time.Ticker. 134 func (m *Module) WaitIfSleeping() <-chan time.Time { 135 m.RLock() 136 defer m.RUnlock() 137 return m.sleepWaitingChannel 138 } 139 140 // NewSleepyTicker returns new sleepyTicker that will respect the modules sleep mode. 141 func (m *Module) NewSleepyTicker(normalDuration, sleepDuration time.Duration) *SleepyTicker { 142 return newSleepyTicker(m, normalDuration, sleepDuration) 143 } 144 145 func (m *Module) prep(reports chan *report) { 146 // check and set intermediate status 147 m.Lock() 148 if m.status != StatusDead { 149 m.Unlock() 150 go func() { 151 reports <- &report{ 152 module: m, 153 err: fmt.Errorf("module already prepped"), 154 } 155 }() 156 return 157 } 158 m.status = StatusPreparing 159 m.Unlock() 160 161 // run prep function 162 go func() { 163 var err error 164 if m.prepFn != nil { 165 // execute function 166 err = m.runCtrlFnWithTimeout( 167 "prep module", 168 moduleStartTimeout, 169 m.prepFn, 170 ) 171 } 172 // set status 173 if err != nil { 174 m.Error( 175 fmt.Sprintf("%s:prep-failed", m.Name), 176 fmt.Sprintf("Preparing module %s failed", m.Name), 177 fmt.Sprintf("Failed to prep module: %s", err.Error()), 178 ) 179 } else { 180 m.Lock() 181 m.status = StatusOffline 182 m.Unlock() 183 m.notifyOfChange() 184 } 185 // send report 186 reports <- &report{ 187 module: m, 188 err: err, 189 } 190 }() 191 } 192 193 func (m *Module) start(reports chan *report) { 194 // check and set intermediate status 195 m.Lock() 196 if m.status != StatusOffline { 197 m.Unlock() 198 go func() { 199 reports <- &report{ 200 module: m, 201 err: fmt.Errorf("module not offline"), 202 } 203 }() 204 return 205 } 206 m.status = StatusStarting 207 208 // reset stop management 209 if m.cancelCtx != nil { 210 // trigger cancel just to be sure 211 m.cancelCtx() 212 } 213 m.Ctx, m.cancelCtx = context.WithCancel(context.Background()) 214 m.stopFlag.UnSet() 215 216 m.Unlock() 217 218 // run start function 219 go func() { 220 var err error 221 if m.startFn != nil { 222 // execute function 223 err = m.runCtrlFnWithTimeout( 224 "start module", 225 moduleStartTimeout, 226 m.startFn, 227 ) 228 } 229 // set status 230 if err != nil { 231 m.Error( 232 fmt.Sprintf("%s:start-failed", m.Name), 233 fmt.Sprintf("Starting module %s failed", m.Name), 234 fmt.Sprintf("Failed to start module: %s", err.Error()), 235 ) 236 } else { 237 m.Lock() 238 m.status = StatusOnline 239 // init start management 240 close(m.startComplete) 241 m.Unlock() 242 m.notifyOfChange() 243 } 244 // send report 245 reports <- &report{ 246 module: m, 247 err: err, 248 } 249 }() 250 } 251 252 func (m *Module) checkIfStopComplete() { 253 if m.stopFlag.IsSet() && 254 m.ctrlFuncRunning.IsNotSet() && 255 atomic.LoadInt32(m.workerCnt) == 0 && 256 atomic.LoadInt32(m.taskCnt) == 0 && 257 atomic.LoadInt32(m.microTaskCnt) == 0 { 258 259 if m.stopCompleted.SetToIf(false, true) { 260 m.Lock() 261 defer m.Unlock() 262 close(m.stopComplete) 263 } 264 } 265 } 266 267 func (m *Module) stop(reports chan *report) { 268 m.Lock() 269 defer m.Unlock() 270 271 // check and set intermediate status 272 if m.status != StatusOnline { 273 go func() { 274 reports <- &report{ 275 module: m, 276 err: fmt.Errorf("module not online"), 277 } 278 }() 279 return 280 } 281 282 // Reset start/stop signal channels. 283 m.startComplete = make(chan struct{}) 284 m.stopComplete = make(chan struct{}) 285 m.stopCompleted.SetTo(false) 286 287 // Set status. 288 m.status = StatusStopping 289 290 go m.stopAllTasks(reports) 291 } 292 293 func (m *Module) stopAllTasks(reports chan *report) { 294 // Manually set the control function flag in order to stop completion by race 295 // condition before stop function has even started. 296 m.ctrlFuncRunning.Set() 297 298 // Set stop flag for everyone checking this flag before we activate any stop trigger. 299 m.stopFlag.Set() 300 301 // Cancel the context to notify all workers and tasks. 302 m.cancelCtx() 303 304 // Start stop function. 305 stopFnError := m.startCtrlFn("stop module", m.stopFn) 306 307 // wait for results 308 select { 309 case <-m.stopComplete: 310 // Complete! 311 case <-time.After(moduleStopTimeout): 312 log.Warningf( 313 "%s: timed out while waiting for stopfn/workers/tasks to finish: stopFn=%v workers=%d tasks=%d microtasks=%d, continuing shutdown...", 314 m.Name, 315 m.ctrlFuncRunning.IsSet(), 316 atomic.LoadInt32(m.workerCnt), 317 atomic.LoadInt32(m.taskCnt), 318 atomic.LoadInt32(m.microTaskCnt), 319 ) 320 } 321 322 // Check for stop fn status. 323 var err error 324 select { 325 case err = <-stopFnError: 326 if err != nil { 327 // Set error as module error. 328 m.Error( 329 fmt.Sprintf("%s:stop-failed", m.Name), 330 fmt.Sprintf("Stopping module %s failed", m.Name), 331 fmt.Sprintf("Failed to stop module: %s", err.Error()), 332 ) 333 } 334 default: 335 } 336 337 // Always set to offline in order to let other modules shutdown in order. 338 m.Lock() 339 m.status = StatusOffline 340 m.Unlock() 341 m.notifyOfChange() 342 343 // Resolve any errors still on the module. 344 m.Resolve("") 345 346 // send report 347 reports <- &report{ 348 module: m, 349 err: err, 350 } 351 } 352 353 // Register registers a new module. The control functions `prep`, `start` and `stop` are technically optional. `stop` is called _after_ all added module workers finished. 354 func Register(name string, prep, start, stop func() error, dependencies ...string) *Module { 355 if modulesLocked.IsSet() { 356 return nil 357 } 358 359 newModule := initNewModule(name, prep, start, stop, dependencies...) 360 361 // check for already existing module 362 _, ok := modules[name] 363 if ok { 364 panic(fmt.Sprintf("modules: module %s is already registered", name)) 365 } 366 // add new module 367 modules[name] = newModule 368 369 return newModule 370 } 371 372 func initNewModule(name string, prep, start, stop func() error, dependencies ...string) *Module { 373 ctx, cancelCtx := context.WithCancel(context.Background()) 374 var workerCnt int32 375 var taskCnt int32 376 var microTaskCnt int32 377 378 newModule := &Module{ 379 Name: name, 380 enabled: abool.NewBool(false), 381 enabledAsDependency: abool.NewBool(false), 382 sleepMode: abool.NewBool(true), // Change (for init) is triggered below. 383 sleepWaitingChannel: make(chan time.Time), 384 prepFn: prep, 385 startFn: start, 386 stopFn: stop, 387 startComplete: make(chan struct{}), 388 Ctx: ctx, 389 cancelCtx: cancelCtx, 390 stopFlag: abool.NewBool(false), 391 stopCompleted: abool.NewBool(true), 392 ctrlFuncRunning: abool.NewBool(false), 393 workerCnt: &workerCnt, 394 taskCnt: &taskCnt, 395 microTaskCnt: µTaskCnt, 396 eventHooks: make(map[string]*eventHooks), 397 depNames: dependencies, 398 } 399 400 // Sleep mode is disabled by default. 401 newModule.Sleep(false) 402 403 return newModule 404 } 405 406 func initDependencies() error { 407 for _, m := range modules { 408 for _, depName := range m.depNames { 409 410 // get dependency 411 depModule, ok := modules[depName] 412 if !ok { 413 return fmt.Errorf("module %s declares dependency \"%s\", but this module has not been registered", m.Name, depName) 414 } 415 416 // link together 417 m.depModules = append(m.depModules, depModule) 418 depModule.depReverse = append(depModule.depReverse, m) 419 420 } 421 } 422 423 return nil 424 } 425 426 // SetSleepMode enables or disables sleep mode for all the modules. 427 func SetSleepMode(enabled bool) { 428 // Update all modules 429 for _, m := range modules { 430 m.Sleep(enabled) 431 } 432 433 // Check if differs with the old state. 434 set := sleepMode.SetToIf(!enabled, enabled) 435 if set { 436 // Send signal to the task schedular. 437 select { 438 case notifyTaskScheduler <- struct{}{}: 439 default: 440 } 441 } 442 }