initial commit

This commit is contained in:
developer committed 2026-09-27 11:20:34 +00:00
1 parent 5b935301e5
commit 85453852d5
105 files changed
+24846

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
package config
import (
"sync"
"sync/atomic"
)
// Store is the process-wide, hot-reloadable configuration holder. Readers take
// a snapshot pointer (cheap, no lock) and writers swap a fully-built *Config
// in atomically, so a request never observes a half-applied config.
type Store struct {
cur atomic.Pointer[Config]
mu sync.Mutex // serialises writers so LastError/Version stay coherent
// path is the file the config was loaded from, used by the API to
// persist mutations.
path string
// lastError records why the most recent reload attempt failed, so the
// health endpoint can report a stale-but-running state.
lastErr atomic.Pointer[string]
// generation increments on every successful swap.
generation atomic.Uint64
// hooks run after a successful swap (server re-binds listeners etc).
hooksMu sync.RWMutex
hooks []func(*Config)
}
// NewStore wraps cfg.
func NewStore(cfg *Config, path string) *Store {
s := &Store{path: path}
s.cur.Store(cfg)
return s
}
// Get returns the current configuration snapshot.
func (s *Store) Get() *Config { return s.cur.Load() }
// Path is the file this config was loaded from ("" when running without one).
func (s *Store) Path() string { return s.path }
// Generation counts successful swaps; the cluster uses it to detect
// divergence between instances.
func (s *Store) Generation() uint64 { return s.generation.Load() }
// LastError returns the error from the most recent failed reload, or nil.
func (s *Store) LastError() string {
if p := s.lastErr.Load(); p != nil {
return *p
}
return ""
}
// Swap installs cfg as the current configuration. Callers are responsible for
// validating first; Swap itself only guards structural invariants that would
// otherwise panic subsystems.
func (s *Store) Swap(cfg *Config) error {
if cfg == nil {
return errNilConfig
}
if cfg.Cache.Shards <= 0 {
cfg.Cache.Shards = 32
}
s.mu.Lock()
s.cur.Store(cfg)
s.generation.Add(1)
s.lastErr.Store(nil)
s.mu.Unlock()
s.runHooks(cfg)
return nil
}
// Reload re-reads the backing file and swaps it in on success. On failure the
// previous configuration stays active and the error is recorded.
func (s *Store) Reload() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.path == "" {
return errNoPath
}
cfg, err := Load(s.path)
if err != nil {
msg := err.Error()
s.lastErr.Store(&msg)
return err
}
// Preserve the working dir of the running instance so relative paths keep
// resolving the same way even if the file moved.
cfg.Server.WorkingDir = s.cur.Load().Server.WorkingDir
s.cur.Store(cfg)
s.generation.Add(1)
s.lastErr.Store(nil)
s.runHooks(cfg)
return nil
}
// OnSwap registers a callback invoked with the new config after each swap.
// Hooks run outside the writer lock; a panicking hook is contained so one bad
// subsystem cannot take down a reload.
func (s *Store) OnSwap(fn func(*Config)) {
if fn == nil {
return
}
s.hooksMu.Lock()
s.hooks = append(s.hooks, fn)
s.hooksMu.Unlock()
}
func (s *Store) runHooks(cfg *Config) {
s.hooksMu.RLock()
hooks := s.hooks
s.hooksMu.RUnlock()
for _, h := range hooks {
func() {
defer func() { _ = recover() }()
h(cfg)
}()
}
}
type storeError string
func (e storeError) Error() string { return string(e) }
const (
errNilConfig = storeError("config: refusing to swap a nil configuration")
errNoPath = storeError("config: no config file path known, cannot reload")
)