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) }