mirror of
https://github.com/safing/portbase
synced 2025-09-01 01:59:48 +00:00
210 lines
5 KiB
Go
210 lines
5 KiB
Go
package modules
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/safing/portbase/log"
|
|
"github.com/tevino/abool"
|
|
)
|
|
|
|
var (
|
|
modulesLock sync.Mutex
|
|
modules = make(map[string]*Module)
|
|
|
|
// ErrCleanExit is returned by Start() when the program is interrupted before starting. This can happen for example, when using the "--help" flag.
|
|
ErrCleanExit = errors.New("clean exit requested")
|
|
)
|
|
|
|
// Module represents a module.
|
|
type Module struct {
|
|
Name string
|
|
|
|
// lifecycle mgmt
|
|
Prepped *abool.AtomicBool
|
|
Started *abool.AtomicBool
|
|
Stopped *abool.AtomicBool
|
|
inTransition *abool.AtomicBool
|
|
|
|
// lifecycle callback functions
|
|
prep func() error
|
|
start func() error
|
|
stop func() error
|
|
|
|
// shutdown mgmt
|
|
Ctx context.Context
|
|
cancelCtx func()
|
|
shutdownFlag *abool.AtomicBool
|
|
workerGroup sync.WaitGroup
|
|
workerCnt *int32
|
|
|
|
// dependency mgmt
|
|
depNames []string
|
|
depModules []*Module
|
|
depReverse []*Module
|
|
}
|
|
|
|
// AddWorkers adds workers to the worker waitgroup. This is a failsafe wrapper for sync.Waitgroup.
|
|
func (m *Module) AddWorkers(n uint) {
|
|
if !m.ShutdownInProgress() {
|
|
if atomic.AddInt32(m.workerCnt, int32(n)) > 0 {
|
|
// only add to workgroup if cnt is positive (try to compensate wrong usage)
|
|
m.workerGroup.Add(int(n))
|
|
}
|
|
}
|
|
}
|
|
|
|
// FinishWorker removes a worker from the worker waitgroup. This is a failsafe wrapper for sync.Waitgroup.
|
|
func (m *Module) FinishWorker() {
|
|
// check worker cnt
|
|
if atomic.AddInt32(m.workerCnt, -1) < 0 {
|
|
log.Warningf("modules: %s module tried to finish more workers than added, this may lead to undefined behavior when shutting down", m.Name)
|
|
return
|
|
}
|
|
// also mark worker done in workgroup
|
|
m.workerGroup.Done()
|
|
}
|
|
|
|
// ShutdownInProgress returns whether the module has started shutting down. In most cases, you should use ShuttingDown instead.
|
|
func (m *Module) ShutdownInProgress() bool {
|
|
return m.shutdownFlag.IsSet()
|
|
}
|
|
|
|
// ShuttingDown lets you listen for the shutdown signal.
|
|
func (m *Module) ShuttingDown() <-chan struct{} {
|
|
return m.Ctx.Done()
|
|
}
|
|
|
|
func (m *Module) shutdown() error {
|
|
// signal shutdown
|
|
m.shutdownFlag.Set()
|
|
m.cancelCtx()
|
|
|
|
// wait for workers
|
|
done := make(chan struct{})
|
|
go func() {
|
|
m.workerGroup.Wait()
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(3 * time.Second):
|
|
return errors.New("timed out while waiting for module workers to finish")
|
|
}
|
|
|
|
// call shutdown function
|
|
return m.stop()
|
|
}
|
|
|
|
func dummyAction() error {
|
|
return nil
|
|
}
|
|
|
|
// Register registers a new module. The control functions `prep`, `start` and `stop` are technically optional. `stop` is called _after_ all added module workers finished.
|
|
func Register(name string, prep, start, stop func() error, dependencies ...string) *Module {
|
|
ctx, cancelCtx := context.WithCancel(context.Background())
|
|
var workerCnt int32
|
|
|
|
newModule := &Module{
|
|
Name: name,
|
|
Prepped: abool.NewBool(false),
|
|
Started: abool.NewBool(false),
|
|
Stopped: abool.NewBool(false),
|
|
inTransition: abool.NewBool(false),
|
|
Ctx: ctx,
|
|
cancelCtx: cancelCtx,
|
|
shutdownFlag: abool.NewBool(false),
|
|
workerGroup: sync.WaitGroup{},
|
|
workerCnt: &workerCnt,
|
|
prep: prep,
|
|
start: start,
|
|
stop: stop,
|
|
depNames: dependencies,
|
|
}
|
|
|
|
// replace nil arguments with dummy action
|
|
if newModule.prep == nil {
|
|
newModule.prep = dummyAction
|
|
}
|
|
if newModule.start == nil {
|
|
newModule.start = dummyAction
|
|
}
|
|
if newModule.stop == nil {
|
|
newModule.stop = dummyAction
|
|
}
|
|
|
|
modulesLock.Lock()
|
|
defer modulesLock.Unlock()
|
|
modules[name] = newModule
|
|
return newModule
|
|
}
|
|
|
|
func initDependencies() error {
|
|
for _, m := range modules {
|
|
for _, depName := range m.depNames {
|
|
|
|
// get dependency
|
|
depModule, ok := modules[depName]
|
|
if !ok {
|
|
return fmt.Errorf("module %s declares dependency \"%s\", but this module has not been registered", m.Name, depName)
|
|
}
|
|
|
|
// link together
|
|
m.depModules = append(m.depModules, depModule)
|
|
depModule.depReverse = append(depModule.depReverse, m)
|
|
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadyToPrep returns whether all dependencies are ready for this module to prep.
|
|
func (m *Module) ReadyToPrep() bool {
|
|
if m.inTransition.IsSet() || m.Prepped.IsSet() {
|
|
return false
|
|
}
|
|
|
|
for _, dep := range m.depModules {
|
|
if !dep.Prepped.IsSet() {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ReadyToStart returns whether all dependencies are ready for this module to start.
|
|
func (m *Module) ReadyToStart() bool {
|
|
if m.inTransition.IsSet() || m.Started.IsSet() {
|
|
return false
|
|
}
|
|
|
|
for _, dep := range m.depModules {
|
|
if !dep.Started.IsSet() {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ReadyToStop returns whether all dependencies are ready for this module to stop.
|
|
func (m *Module) ReadyToStop() bool {
|
|
if !m.Started.IsSet() || m.inTransition.IsSet() || m.Stopped.IsSet() {
|
|
return false
|
|
}
|
|
|
|
for _, revDep := range m.depReverse {
|
|
// not ready if a reverse dependency was started, but not yet stopped
|
|
if revDep.Started.IsSet() && !revDep.Stopped.IsSet() {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|