feat: nlog, config hot-reload, and concurrency fixes
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/cedar2025/xboard-node/internal/nlog"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// Watcher reloads the config file on change (debounced) and calls onChange.
|
||||
type Watcher struct {
|
||||
path string
|
||||
debounce time.Duration
|
||||
onChange func(*Config)
|
||||
watcher *fsnotify.Watcher
|
||||
|
||||
stopOnce sync.Once
|
||||
stopCh chan struct{}
|
||||
stopped atomic.Bool
|
||||
}
|
||||
|
||||
// WatchConfig watches path; invalid reloads are logged and ignored.
|
||||
// Stop via Stop() or cancel ctx.
|
||||
func WatchConfig(ctx context.Context, path string, onChange func(*Config)) (*Watcher, error) {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fsw, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Watch parent dir so rename/atomic saves are visible.
|
||||
dir := filepath.Dir(absPath)
|
||||
if err := fsw.Add(dir); err != nil {
|
||||
fsw.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &Watcher{
|
||||
path: absPath,
|
||||
debounce: 1 * time.Second,
|
||||
onChange: onChange,
|
||||
watcher: fsw,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
go w.loop(ctx)
|
||||
nlog.Core().Info("config watcher started", "path", absPath)
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (w *Watcher) loop(ctx context.Context) {
|
||||
var timer *time.Timer
|
||||
defer func() {
|
||||
if timer != nil {
|
||||
timer.Stop()
|
||||
}
|
||||
w.watcher.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-w.stopCh:
|
||||
return
|
||||
|
||||
case ev, ok := <-w.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if filepath.Clean(ev.Name) != w.path {
|
||||
continue
|
||||
}
|
||||
if !ev.Has(fsnotify.Write) && !ev.Has(fsnotify.Create) {
|
||||
continue
|
||||
}
|
||||
|
||||
if timer == nil {
|
||||
timer = time.AfterFunc(w.debounce, w.reload)
|
||||
} else {
|
||||
timer.Reset(w.debounce)
|
||||
}
|
||||
|
||||
case err, ok := <-w.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
nlog.Core().Warn("config watcher error", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) reload() {
|
||||
if w.stopped.Load() {
|
||||
return
|
||||
}
|
||||
cfg, err := Load(w.path)
|
||||
if err != nil {
|
||||
nlog.Core().Error("config reload failed, keeping current config", "error", err)
|
||||
return
|
||||
}
|
||||
nlog.Core().Info("config reloaded successfully")
|
||||
w.onChange(cfg)
|
||||
}
|
||||
|
||||
func (w *Watcher) Stop() {
|
||||
w.stopped.Store(true)
|
||||
w.stopOnce.Do(func() { close(w.stopCh) })
|
||||
}
|
||||
Reference in New Issue
Block a user