| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package dyncfg |
| 4 | |
| 5 | import "sync" |
| 6 | |
| 7 | // Config is the constraint interface for configs stored in handler caches. |
| 8 | type Config interface { |
| 9 | UID() string // SeenCache key (globally unique per source) |
| 10 | ExposedKey() string // ExposedCache key (one per logical name) |
| 11 | SourceType() string // "dyncfg", "user", "stock" |
| 12 | SourceTypePriority() int // dyncfg=16, user=8, stock=2 |
| 13 | Source() string // source identifier |
| 14 | Hash() uint64 // content hash for change detection |
| 15 | } |
| 16 | |
| 17 | // Entry pairs a config with its current dyncfg status. |
| 18 | type Entry[C Config] struct { |
| 19 | Cfg C |
| 20 | Status Status |
| 21 | } |
| 22 | |
| 23 | // SeenCache stores all discovered configs keyed by UID(). |
| 24 | // Thread-safe: SD dispatches read-only commands concurrently. |
| 25 | type SeenCache[C Config] struct { |
| 26 | mux sync.RWMutex |
| 27 | items map[string]C |
| 28 | } |
| 29 | |
| 30 | func NewSeenCache[C Config]() *SeenCache[C] { |
| 31 | return &SeenCache[C]{items: make(map[string]C)} |
| 32 | } |
| 33 | |
| 34 | func (c *SeenCache[C]) Add(cfg C) { |
| 35 | c.mux.Lock() |
| 36 | defer c.mux.Unlock() |
| 37 | c.items[cfg.UID()] = cfg |
| 38 | } |
| 39 | |
| 40 | func (c *SeenCache[C]) Remove(cfg C) { |
| 41 | c.mux.Lock() |
| 42 | defer c.mux.Unlock() |
| 43 | delete(c.items, cfg.UID()) |
| 44 | } |
| 45 | |
| 46 | func (c *SeenCache[C]) Lookup(cfg C) (C, bool) { |
| 47 | return c.LookupByUID(cfg.UID()) |
| 48 | } |
| 49 | |
| 50 | func (c *SeenCache[C]) LookupByUID(uid string) (C, bool) { |
| 51 | c.mux.RLock() |
| 52 | defer c.mux.RUnlock() |
| 53 | v, ok := c.items[uid] |
| 54 | return v, ok |
| 55 | } |
| 56 | |
| 57 | func (c *SeenCache[C]) Count() int { |
| 58 | c.mux.RLock() |
| 59 | defer c.mux.RUnlock() |
| 60 | return len(c.items) |
| 61 | } |
| 62 | |
| 63 | // ForEach iterates over all entries. Return false to stop iteration. |
| 64 | func (c *SeenCache[C]) ForEach(fn func(uid string, cfg C) bool) { |
| 65 | c.mux.RLock() |
| 66 | defer c.mux.RUnlock() |
| 67 | for uid, cfg := range c.items { |
| 68 | if !fn(uid, cfg) { |
| 69 | return |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | // ExposedCache stores active config+status per logical name, keyed by ExposedKey(). |
| 75 | // LookupByKey returns a pointer to the stored Entry — mutations to Status |
| 76 | // are visible through the pointer. The mutex protects map access only. |
| 77 | // Entry.Status is written exclusively by the serialized command goroutine |
| 78 | // (via Handler.Cmd*) and is not read by any concurrent code path in production. |
| 79 | // Concurrent read-only commands (schema/get/test/userconfig) access only |
| 80 | // Entry.Cfg, which is immutable after creation. |
| 81 | type ExposedCache[C Config] struct { |
| 82 | mux sync.RWMutex |
| 83 | items map[string]*Entry[C] |
| 84 | } |
| 85 | |
| 86 | func NewExposedCache[C Config]() *ExposedCache[C] { |
| 87 | return &ExposedCache[C]{items: make(map[string]*Entry[C])} |
| 88 | } |
| 89 | |
| 90 | // Add inserts or overwrites an entry by cfg.ExposedKey(). |
| 91 | func (c *ExposedCache[C]) Add(entry *Entry[C]) { |
| 92 | c.mux.Lock() |
| 93 | defer c.mux.Unlock() |
| 94 | c.items[entry.Cfg.ExposedKey()] = entry |
| 95 | } |
| 96 | |
| 97 | func (c *ExposedCache[C]) Remove(cfg C) { |
| 98 | c.mux.Lock() |
| 99 | defer c.mux.Unlock() |
| 100 | delete(c.items, cfg.ExposedKey()) |
| 101 | } |
| 102 | |
| 103 | // LookupByKey returns a pointer to the stored entry. |
| 104 | func (c *ExposedCache[C]) LookupByKey(key string) (*Entry[C], bool) { |
| 105 | c.mux.RLock() |
| 106 | defer c.mux.RUnlock() |
| 107 | v, ok := c.items[key] |
| 108 | return v, ok |
| 109 | } |
| 110 | |
| 111 | // ForEach iterates over all entries. Return false to stop iteration. |
| 112 | func (c *ExposedCache[C]) ForEach(fn func(key string, entry *Entry[C]) bool) { |
| 113 | c.mux.RLock() |
| 114 | defer c.mux.RUnlock() |
| 115 | for k, e := range c.items { |
| 116 | if !fn(k, e) { |
| 117 | return |
| 118 | } |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | func (c *ExposedCache[C]) Count() int { |
| 123 | c.mux.RLock() |
| 124 | defer c.mux.RUnlock() |
| 125 | return len(c.items) |
| 126 | } |