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:        &microTaskCnt,
   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  }