fix(proxy): use mtime-based config watching

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-05 02:04:03 +00:00
parent cbde021d0c
commit 356eb9cec1
3 changed files with 56 additions and 27 deletions

View file

@ -48,6 +48,10 @@ func TestConfigWatcher_Start_Good(t *testing.T) {
if err := os.WriteFile(path, updated, 0o644); err != nil { if err := os.WriteFile(path, updated, 0o644); err != nil {
t.Fatalf("write updated config file: %v", err) t.Fatalf("write updated config file: %v", err)
} }
now := time.Now()
if err := os.Chtimes(path, now, now.Add(2*time.Second)); err != nil {
t.Fatalf("touch updated config file: %v", err)
}
select { select {
case cfg := <-updates: case cfg := <-updates:
@ -61,3 +65,42 @@ func TestConfigWatcher_Start_Good(t *testing.T) {
t.Fatal("expected watcher to reload updated config") t.Fatal("expected watcher to reload updated config")
} }
} }
func TestConfigWatcher_Start_Ugly(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
initial := []byte(`{"mode":"nicehash","workers":"false","bind":[{"host":"127.0.0.1","port":3333}],"pools":[{"url":"pool.example:3333","enabled":true}]}`)
if err := os.WriteFile(path, initial, 0o644); err != nil {
t.Fatalf("write initial config file: %v", err)
}
updates := make(chan *Config, 1)
watcher := NewConfigWatcher(path, func(cfg *Config) {
select {
case updates <- cfg:
default:
}
})
if watcher == nil {
t.Fatal("expected watcher")
}
watcher.Start()
defer watcher.Stop()
now := time.Now()
if err := os.Chtimes(path, now, now.Add(2*time.Second)); err != nil {
t.Fatalf("touch config file: %v", err)
}
select {
case cfg := <-updates:
if cfg == nil {
t.Fatal("expected config update")
}
if got := cfg.Mode; got != "nicehash" {
t.Fatalf("expected unchanged mode, got %q", got)
}
case <-time.After(5 * time.Second):
t.Fatal("expected watcher to reload touched config")
}
}

View file

@ -364,9 +364,6 @@ func NewConfigWatcher(configPath string, onChange func(*Config)) *ConfigWatcher
onChange: onChange, onChange: onChange,
done: make(chan struct{}), done: make(chan struct{}),
} }
if data, err := os.ReadFile(configPath); err == nil {
watcher.lastSum = sha256.Sum256(data)
}
if info, err := os.Stat(configPath); err == nil { if info, err := os.Stat(configPath); err == nil {
watcher.lastMod = info.ModTime() watcher.lastMod = info.ModTime()
} }
@ -392,28 +389,20 @@ func (w *ConfigWatcher) Start() {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
data, err := os.ReadFile(w.path)
if err != nil {
continue
}
sum := sha256.Sum256(data)
w.mu.Lock()
changed := sum != w.lastSum
if changed {
w.lastSum = sum
}
w.mu.Unlock()
if !changed {
continue
}
if info, err := os.Stat(w.path); err == nil { if info, err := os.Stat(w.path); err == nil {
w.mu.Lock() w.mu.Lock()
w.lastMod = info.ModTime() changed := info.ModTime() != w.lastMod
if changed {
w.lastMod = info.ModTime()
}
w.mu.Unlock() w.mu.Unlock()
} if !changed {
config, result := LoadConfig(w.path) continue
if result.OK && config != nil { }
w.onChange(config) config, result := LoadConfig(w.path)
if result.OK && config != nil {
w.onChange(config)
}
} }
case <-w.done: case <-w.done:
return return

View file

@ -110,16 +110,13 @@ type CloseEvent struct {
Miner *Miner Miner *Miner
} }
// ConfigWatcher polls a config file for changes. // ConfigWatcher polls a config file every second and reloads on modification.
// //
// watcher := proxy.NewConfigWatcher("config.json", func(cfg *proxy.Config) { // watcher := proxy.NewConfigWatcher("config.json", func(cfg *proxy.Config) { p.Reload(cfg) })
// p.Reload(cfg)
// })
type ConfigWatcher struct { type ConfigWatcher struct {
path string path string
onChange func(*Config) onChange func(*Config)
lastMod time.Time lastMod time.Time
lastSum [32]byte
done chan struct{} done chan struct{}
mu sync.Mutex mu sync.Mutex
started bool started bool