github.com/safing/portbase@v0.19.5/modules/subsystems/registry.go (about)

     1  package subsystems
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/tevino/abool"
    13  
    14  	"github.com/safing/portbase/config"
    15  	"github.com/safing/portbase/database/record"
    16  	"github.com/safing/portbase/modules"
    17  	"github.com/safing/portbase/runtime"
    18  )
    19  
    20  var (
    21  	// ErrManagerStarted is returned when subsystem registration attempt
    22  	// occurs after the manager has been started.
    23  	ErrManagerStarted = errors.New("subsystem manager already started")
    24  	// ErrDuplicateSubsystem is returned when the subsystem to be registered
    25  	// is alreadey known (duplicated subsystem ID).
    26  	ErrDuplicateSubsystem = errors.New("subsystem is already registered")
    27  )
    28  
    29  // Manager manages subsystems, provides access via a runtime
    30  // value providers and can takeover module management.
    31  type Manager struct {
    32  	l              sync.RWMutex
    33  	subsys         map[string]*Subsystem
    34  	pushUpdate     runtime.PushFunc
    35  	immutable      *abool.AtomicBool
    36  	debounceUpdate *abool.AtomicBool
    37  	runtime        *runtime.Registry
    38  }
    39  
    40  // NewManager returns a new subsystem manager that registers
    41  // itself at rtReg.
    42  func NewManager(rtReg *runtime.Registry) (*Manager, error) {
    43  	mng := &Manager{
    44  		subsys:         make(map[string]*Subsystem),
    45  		immutable:      abool.New(),
    46  		debounceUpdate: abool.New(),
    47  	}
    48  
    49  	push, err := rtReg.Register("subsystems/", runtime.SimpleValueGetterFunc(mng.Get))
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	mng.pushUpdate = push
    55  	mng.runtime = rtReg
    56  
    57  	return mng, nil
    58  }
    59  
    60  // Start starts managing subsystems. Note that it's not possible
    61  // to define new subsystems once Start() has been called.
    62  func (mng *Manager) Start() error {
    63  	mng.immutable.Set()
    64  
    65  	seen := make(map[string]struct{}, len(mng.subsys))
    66  	configKeyPrefixes := make(map[string]*Subsystem, len(mng.subsys))
    67  	// mark all sub-systems as seen. This prevents sub-systems
    68  	// from being added as a sub-systems dependency in addAndMarkDependencies.
    69  	for _, sub := range mng.subsys {
    70  		seen[sub.module.Name] = struct{}{}
    71  		configKeyPrefixes[sub.ConfigKeySpace] = sub
    72  	}
    73  
    74  	// aggregate all modules dependencies (and the subsystem module itself)
    75  	// into the Modules slice. Configuration options form dependent modules
    76  	// will be marked using config.SubsystemAnnotation if not already set.
    77  	for _, sub := range mng.subsys {
    78  		sub.Modules = append(sub.Modules, statusFromModule(sub.module))
    79  		sub.addDependencies(sub.module, seen)
    80  	}
    81  
    82  	// Annotate all configuration options with their respective subsystem.
    83  	_ = config.ForEachOption(func(opt *config.Option) error {
    84  		subsys, ok := configKeyPrefixes[opt.Key]
    85  		if !ok {
    86  			return nil
    87  		}
    88  
    89  		// Add a new subsystem annotation is it is not already set!
    90  		opt.AddAnnotation(config.SubsystemAnnotation, subsys.ID)
    91  
    92  		return nil
    93  	})
    94  
    95  	return nil
    96  }
    97  
    98  // Get implements runtime.ValueProvider.
    99  func (mng *Manager) Get(keyOrPrefix string) ([]record.Record, error) {
   100  	mng.l.RLock()
   101  	defer mng.l.RUnlock()
   102  
   103  	dbName := mng.runtime.DatabaseName()
   104  	records := make([]record.Record, 0, len(mng.subsys))
   105  	for _, subsys := range mng.subsys {
   106  		subsys.Lock()
   107  		if !subsys.KeyIsSet() {
   108  			subsys.SetKey(dbName + ":subsystems/" + subsys.ID)
   109  		}
   110  		if strings.HasPrefix(subsys.DatabaseKey(), keyOrPrefix) {
   111  			records = append(records, subsys)
   112  		}
   113  		subsys.Unlock()
   114  	}
   115  
   116  	// make sure the order is always the same
   117  	sort.Sort(bySubsystemID(records))
   118  
   119  	return records, nil
   120  }
   121  
   122  // Register registers a new subsystem. The given option must be a bool option.
   123  // Should be called in init() directly after the modules.Register() function.
   124  // The config option must not yet be registered and will be registered for
   125  // you. Pass a nil option to force enable.
   126  //
   127  // TODO(ppacher): IMHO the subsystem package is not responsible of registering
   128  // the "toggle option". This would also remove runtime
   129  // dependency to the config package. Users should either pass
   130  // the BoolOptionFunc and the expertise/release level directly
   131  // or just pass the configuration key so those information can
   132  // be looked up by the registry.
   133  func (mng *Manager) Register(id, name, description string, module *modules.Module, configKeySpace string, option *config.Option) error {
   134  	mng.l.Lock()
   135  	defer mng.l.Unlock()
   136  
   137  	if mng.immutable.IsSet() {
   138  		return ErrManagerStarted
   139  	}
   140  
   141  	if _, ok := mng.subsys[id]; ok {
   142  		return ErrDuplicateSubsystem
   143  	}
   144  
   145  	s := &Subsystem{
   146  		ID:             id,
   147  		Name:           name,
   148  		Description:    description,
   149  		ConfigKeySpace: configKeySpace,
   150  		module:         module,
   151  		toggleOption:   option,
   152  	}
   153  
   154  	s.CreateMeta()
   155  
   156  	if s.toggleOption != nil {
   157  		s.ToggleOptionKey = s.toggleOption.Key
   158  		s.ExpertiseLevel = s.toggleOption.ExpertiseLevel
   159  		s.ReleaseLevel = s.toggleOption.ReleaseLevel
   160  
   161  		if err := config.Register(s.toggleOption); err != nil {
   162  			return fmt.Errorf("failed to register subsystem option: %w", err)
   163  		}
   164  
   165  		s.toggleValue = config.GetAsBool(s.ToggleOptionKey, false)
   166  	} else {
   167  		s.toggleValue = func() bool { return true }
   168  	}
   169  
   170  	mng.subsys[id] = s
   171  
   172  	return nil
   173  }
   174  
   175  func (mng *Manager) shouldServeUpdates() bool {
   176  	if !mng.immutable.IsSet() {
   177  		// the manager must be marked as immutable before we
   178  		// are going to handle any module changes.
   179  		return false
   180  	}
   181  	if modules.IsShuttingDown() {
   182  		// we don't care if we are shutting down anyway
   183  		return false
   184  	}
   185  	return true
   186  }
   187  
   188  // CheckConfig checks subsystem configuration values and enables
   189  // or disables subsystems and their dependencies as required.
   190  func (mng *Manager) CheckConfig(ctx context.Context) error {
   191  	// DEBUG SNIPPET
   192  	// Slow-start for non-attributable performance issues.
   193  	// You'll need the snippet in the modules too.
   194  	// time.Sleep(11 * time.Second)
   195  	// END DEBUG SNIPPET
   196  	return mng.handleConfigChanges(ctx)
   197  }
   198  
   199  func (mng *Manager) handleModuleUpdate(m *modules.Module) {
   200  	if !mng.shouldServeUpdates() {
   201  		return
   202  	}
   203  
   204  	// Read lock is fine as the subsystems are write-locked on their own
   205  	mng.l.RLock()
   206  	defer mng.l.RUnlock()
   207  
   208  	subsys, ms := mng.findParentSubsystem(m)
   209  	if subsys == nil {
   210  		// the updated module is not handled by any
   211  		// subsystem. We're done here.
   212  		return
   213  	}
   214  
   215  	subsys.Lock()
   216  	defer subsys.Unlock()
   217  
   218  	updated := compareAndUpdateStatus(m, ms)
   219  	if updated {
   220  		subsys.makeSummary()
   221  	}
   222  
   223  	if updated {
   224  		mng.pushUpdate(subsys)
   225  	}
   226  }
   227  
   228  func (mng *Manager) handleConfigChanges(_ context.Context) error {
   229  	if !mng.shouldServeUpdates() {
   230  		return nil
   231  	}
   232  
   233  	if mng.debounceUpdate.SetToIf(false, true) {
   234  		time.Sleep(100 * time.Millisecond)
   235  		mng.debounceUpdate.UnSet()
   236  	} else {
   237  		return nil
   238  	}
   239  
   240  	mng.l.RLock()
   241  	defer mng.l.RUnlock()
   242  
   243  	var changed bool
   244  	for _, subsystem := range mng.subsys {
   245  		if subsystem.module.SetEnabled(subsystem.toggleValue()) {
   246  			changed = true
   247  		}
   248  	}
   249  	if !changed {
   250  		return nil
   251  	}
   252  
   253  	return modules.ManageModules()
   254  }
   255  
   256  func (mng *Manager) findParentSubsystem(m *modules.Module) (*Subsystem, *ModuleStatus) {
   257  	for _, subsys := range mng.subsys {
   258  		for _, ms := range subsys.Modules {
   259  			if ms.Name == m.Name {
   260  				return subsys, ms
   261  			}
   262  		}
   263  	}
   264  	return nil, nil
   265  }
   266  
   267  // helper type to sort a slice of []*Subsystem (casted as []record.Record) by
   268  // id. Only use if it's guaranteed that all record.Records are *Subsystem.
   269  // Otherwise Less() will panic.
   270  type bySubsystemID []record.Record
   271  
   272  func (sl bySubsystemID) Less(i, j int) bool { return sl[i].(*Subsystem).ID < sl[j].(*Subsystem).ID } //nolint:forcetypeassert // Can only be *Subsystem.
   273  func (sl bySubsystemID) Swap(i, j int)      { sl[i], sl[j] = sl[j], sl[i] }
   274  func (sl bySubsystemID) Len() int           { return len(sl) }