Revamp module structure

- Add shutdown mechanics to module
- Adapt dbmodule to new mechanics
This commit is contained in:
Daniel 2019-08-09 16:45:43 +02:00
parent 090669728d
commit 402429cd70
11 changed files with 108 additions and 37 deletions

View file

@ -13,7 +13,7 @@ var (
) )
func init() { func init() {
modules.Register("api", prep, start, nil, "core") modules.Register("api", prep, start, stop, "base", "database", "config")
} }
func prep() error { func prep() error {

View file

@ -22,7 +22,7 @@ func SetDataRoot(root *utils.DirStructure) {
} }
func init() { func init() {
modules.Register("config", prep, start, nil, "core") modules.Register("config", prep, start, nil, "base", "database")
} }
func prep() error { func prep() error {

View file

@ -23,7 +23,7 @@ var (
) )
func init() { func init() {
modules.Register("random", prep, Start, stop) modules.Register("random", prep, Start, stop, "base")
config.Register(&config.Option{ config.Register(&config.Option{
Name: "RNG Cipher", Name: "RNG Cipher",

View file

@ -2,51 +2,46 @@ package dbmodule
import ( import (
"errors" "errors"
"flag"
"sync"
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
"github.com/safing/portbase/modules" "github.com/safing/portbase/modules"
"github.com/safing/portbase/utils"
) )
var ( var (
databaseDir string databasePath string
shutdownSignal = make(chan struct{}) databaseStructureRoot *utils.DirStructure
maintenanceWg sync.WaitGroup
module *modules.Module
) )
// SetDatabaseLocation sets the location of the database. Must be called before modules.Start and will be overridden by command line options. Intended for unit tests. func init() {
func SetDatabaseLocation(location string) { module = modules.Register("database", prep, start, stop, "base")
databaseDir = location
} }
func init() { // SetDatabaseLocation sets the location of the database for initialization. Supply either a path or dir structure.
flag.StringVar(&databaseDir, "db", "", "set database directory") func SetDatabaseLocation(dirPath string, dirStructureRoot *utils.DirStructure) {
databasePath = dirPath
modules.Register("database", prep, start, stop) databaseStructureRoot = dirStructureRoot
} }
func prep() error { func prep() error {
if databaseDir == "" { if databasePath == "" && databaseStructureRoot == nil {
return errors.New("no database location specified, set with `-db=/path/to/db`") return errors.New("no database location specified")
}
ok := database.SetLocation(databaseDir)
if !ok {
return errors.New("database location already set")
} }
return nil return nil
} }
func start() error { func start() error {
err := database.Initialize() err := database.Initialize(databasePath, databaseStructureRoot)
if err == nil { if err != nil {
startMaintainer() return err
} }
return err
startMaintainer()
return nil
} }
func stop() error { func stop() error {
close(shutdownSignal)
maintenanceWg.Wait()
return database.Shutdown() return database.Shutdown()
} }

View file

@ -13,7 +13,7 @@ var (
) )
func startMaintainer() { func startMaintainer() {
maintenanceWg.Add(1) module.AddWorkers(1)
go maintenanceWorker() go maintenanceWorker()
} }
@ -37,8 +37,8 @@ func maintenanceWorker() {
if err != nil { if err != nil {
log.Errorf("database: thorough maintenance error: %s", err) log.Errorf("database: thorough maintenance error: %s", err)
} }
case <-shutdownSignal: case <-module.ShuttingDown():
maintenanceWg.Done() module.FinishWorker()
return return
} }
} }

View file

@ -23,7 +23,7 @@ var (
databasesStructure *utils.DirStructure databasesStructure *utils.DirStructure
) )
// Initialize initialized the database at the specified location. Supply either a path or dir structure. // Initialize initializes the database at the specified location. Supply either a path or dir structure.
func Initialize(dirPath string, dirStructureRoot *utils.DirStructure) error { func Initialize(dirPath string, dirStructureRoot *utils.DirStructure) error {
if initialized.SetToIf(false, true) { if initialized.SetToIf(false, true) {

View file

@ -3,6 +3,7 @@ package modules
import "flag" import "flag"
var ( var (
// HelpFlag triggers printing flag.Usage. It's exported for custom help handling.
HelpFlag bool HelpFlag bool
) )

View file

@ -1,10 +1,14 @@
package modules package modules
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"sync" "sync"
"sync/atomic"
"time"
"github.com/safing/portbase/log"
"github.com/tevino/abool" "github.com/tevino/abool"
) )
@ -18,33 +22,104 @@ var (
// Module represents a module. // Module represents a module.
type Module struct { type Module struct {
Name string Name string
// lifecycle mgmt
Prepped *abool.AtomicBool Prepped *abool.AtomicBool
Started *abool.AtomicBool Started *abool.AtomicBool
Stopped *abool.AtomicBool Stopped *abool.AtomicBool
inTransition *abool.AtomicBool inTransition *abool.AtomicBool
// lifecycle callback functions
prep func() error prep func() error
start func() error start func() error
stop func() error stop func() error
// shutdown mgmt
Ctx context.Context
cancelCtx func()
shutdownFlag *abool.AtomicBool
workerGroup sync.WaitGroup
workerCnt *int32
// dependency mgmt
depNames []string depNames []string
depModules []*Module depModules []*Module
depReverse []*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 { func dummyAction() error {
return nil return nil
} }
// Register registers a new module. // 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 { func Register(name string, prep, start, stop func() error, dependencies ...string) *Module {
ctx, cancelCtx := context.WithCancel(context.Background())
var workerCnt int32
newModule := &Module{ newModule := &Module{
Name: name, Name: name,
Prepped: abool.NewBool(false), Prepped: abool.NewBool(false),
Started: abool.NewBool(false), Started: abool.NewBool(false),
Stopped: abool.NewBool(false), Stopped: abool.NewBool(false),
inTransition: abool.NewBool(false), inTransition: abool.NewBool(false),
Ctx: ctx,
cancelCtx: cancelCtx,
shutdownFlag: abool.NewBool(false),
workerGroup: sync.WaitGroup{},
workerCnt: &workerCnt,
prep: prep, prep: prep,
start: start, start: start,
stop: stop, stop: stop,

View file

@ -202,11 +202,11 @@ func TestErrors(t *testing.T) {
startCompleteSignal = make(chan struct{}) startCompleteSignal = make(chan struct{})
// test help flag // test help flag
helpFlag = true HelpFlag = true
err = Start() err = Start()
if err == nil { if err == nil {
t.Error("should fail") t.Error("should fail")
} }
helpFlag = false HelpFlag = false
} }

View file

@ -74,7 +74,7 @@ func stopModules() error {
go func() { go func() {
reports <- &report{ reports <- &report{
module: execM, module: execM,
err: execM.stop(), err: execM.shutdown(),
} }
}() }()
} }

View file

@ -12,7 +12,7 @@ var (
) )
func init() { func init() {
modules.Register("notifications", nil, start, nil, "core") modules.Register("notifications", nil, start, nil, "base", "database")
} }
func start() error { func start() error {