feat(watcher): debounce config reloads to prevent redundant operations

Introduce `scheduleConfigReload` with debounce functionality for config reloads, ensuring efficient handling of frequent changes. Added `stopConfigReloadTimer` for stopping timers during watcher shutdown.
This commit is contained in:
Luis Pater
2025-11-10 12:57:40 +08:00
parent 8c947cafbe
commit 40f7061b04

View File

@@ -45,6 +45,8 @@ type Watcher struct {
authDir string authDir string
config *config.Config config *config.Config
clientsMutex sync.RWMutex clientsMutex sync.RWMutex
configReloadMu sync.Mutex
configReloadTimer *time.Timer
reloadCallback func(*config.Config) reloadCallback func(*config.Config)
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
lastAuthHashes map[string]string lastAuthHashes map[string]string
@@ -114,6 +116,7 @@ const (
// replaceCheckDelay is a short delay to allow atomic replace (rename) to settle // replaceCheckDelay is a short delay to allow atomic replace (rename) to settle
// before deciding whether a Remove event indicates a real deletion. // before deciding whether a Remove event indicates a real deletion.
replaceCheckDelay = 50 * time.Millisecond replaceCheckDelay = 50 * time.Millisecond
configReloadDebounce = 150 * time.Millisecond
) )
// NewWatcher creates a new file watcher instance // NewWatcher creates a new file watcher instance
@@ -172,9 +175,19 @@ func (w *Watcher) Start(ctx context.Context) error {
// Stop stops the file watcher // Stop stops the file watcher
func (w *Watcher) Stop() error { func (w *Watcher) Stop() error {
w.stopDispatch() w.stopDispatch()
w.stopConfigReloadTimer()
return w.watcher.Close() return w.watcher.Close()
} }
func (w *Watcher) stopConfigReloadTimer() {
w.configReloadMu.Lock()
if w.configReloadTimer != nil {
w.configReloadTimer.Stop()
w.configReloadTimer = nil
}
w.configReloadMu.Unlock()
}
// SetConfig updates the current configuration // SetConfig updates the current configuration
func (w *Watcher) SetConfig(cfg *config.Config) { func (w *Watcher) SetConfig(cfg *config.Config) {
w.clientsMutex.Lock() w.clientsMutex.Lock()
@@ -476,6 +489,42 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
// Handle config file changes // Handle config file changes
if isConfigEvent { if isConfigEvent {
log.Debugf("config file change details - operation: %s, timestamp: %s", event.Op.String(), now.Format("2006-01-02 15:04:05.000")) log.Debugf("config file change details - operation: %s, timestamp: %s", event.Op.String(), now.Format("2006-01-02 15:04:05.000"))
w.scheduleConfigReload()
return
}
// Handle auth directory changes incrementally (.json only)
fmt.Printf("auth file changed (%s): %s, processing incrementally\n", event.Op.String(), filepath.Base(event.Name))
if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write {
w.addOrUpdateClient(event.Name)
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
// Atomic replace on some platforms may surface as Remove+Create for the target path.
// Wait briefly; if the file exists again, treat as update instead of removal.
time.Sleep(replaceCheckDelay)
if _, statErr := os.Stat(event.Name); statErr == nil {
// File exists after a short delay; handle as an update.
w.addOrUpdateClient(event.Name)
return
}
w.removeClient(event.Name)
}
}
func (w *Watcher) scheduleConfigReload() {
w.configReloadMu.Lock()
defer w.configReloadMu.Unlock()
if w.configReloadTimer != nil {
w.configReloadTimer.Stop()
}
w.configReloadTimer = time.AfterFunc(configReloadDebounce, func() {
w.configReloadMu.Lock()
w.configReloadTimer = nil
w.configReloadMu.Unlock()
w.reloadConfigIfChanged()
})
}
func (w *Watcher) reloadConfigIfChanged() {
data, err := os.ReadFile(w.configPath) data, err := os.ReadFile(w.configPath)
if err != nil { if err != nil {
log.Errorf("failed to read config file for hash check: %v", err) log.Errorf("failed to read config file for hash check: %v", err)
@@ -510,24 +559,6 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
w.clientsMutex.Unlock() w.clientsMutex.Unlock()
w.persistConfigAsync() w.persistConfigAsync()
} }
return
}
// Handle auth directory changes incrementally (.json only)
fmt.Printf("auth file changed (%s): %s, processing incrementally\n", event.Op.String(), filepath.Base(event.Name))
if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write {
w.addOrUpdateClient(event.Name)
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
// Atomic replace on some platforms may surface as Remove+Create for the target path.
// Wait briefly; if the file exists again, treat as update instead of removal.
time.Sleep(replaceCheckDelay)
if _, statErr := os.Stat(event.Name); statErr == nil {
// File exists after a short delay; handle as an update.
w.addOrUpdateClient(event.Name)
return
}
w.removeClient(event.Name)
}
} }
// reloadConfig reloads the configuration and triggers a full reload // reloadConfig reloads the configuration and triggers a full reload