Skip to content

Commit 40f7061

Browse files
committed
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.
1 parent 8c947ca commit 40f7061

File tree

1 file changed

+84
-53
lines changed

1 file changed

+84
-53
lines changed

internal/watcher/watcher.go

Lines changed: 84 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,26 @@ type authDirProvider interface {
4141

4242
// Watcher manages file watching for configuration and authentication files
4343
type Watcher struct {
44-
configPath string
45-
authDir string
46-
config *config.Config
47-
clientsMutex sync.RWMutex
48-
reloadCallback func(*config.Config)
49-
watcher *fsnotify.Watcher
50-
lastAuthHashes map[string]string
51-
lastConfigHash string
52-
authQueue chan<- AuthUpdate
53-
currentAuths map[string]*coreauth.Auth
54-
dispatchMu sync.Mutex
55-
dispatchCond *sync.Cond
56-
pendingUpdates map[string]AuthUpdate
57-
pendingOrder []string
58-
dispatchCancel context.CancelFunc
59-
storePersister storePersister
60-
mirroredAuthDir string
61-
oldConfigYaml []byte
44+
configPath string
45+
authDir string
46+
config *config.Config
47+
clientsMutex sync.RWMutex
48+
configReloadMu sync.Mutex
49+
configReloadTimer *time.Timer
50+
reloadCallback func(*config.Config)
51+
watcher *fsnotify.Watcher
52+
lastAuthHashes map[string]string
53+
lastConfigHash string
54+
authQueue chan<- AuthUpdate
55+
currentAuths map[string]*coreauth.Auth
56+
dispatchMu sync.Mutex
57+
dispatchCond *sync.Cond
58+
pendingUpdates map[string]AuthUpdate
59+
pendingOrder []string
60+
dispatchCancel context.CancelFunc
61+
storePersister storePersister
62+
mirroredAuthDir string
63+
oldConfigYaml []byte
6264
}
6365

6466
type stableIDGenerator struct {
@@ -113,7 +115,8 @@ type AuthUpdate struct {
113115
const (
114116
// replaceCheckDelay is a short delay to allow atomic replace (rename) to settle
115117
// before deciding whether a Remove event indicates a real deletion.
116-
replaceCheckDelay = 50 * time.Millisecond
118+
replaceCheckDelay = 50 * time.Millisecond
119+
configReloadDebounce = 150 * time.Millisecond
117120
)
118121

119122
// NewWatcher creates a new file watcher instance
@@ -172,9 +175,19 @@ func (w *Watcher) Start(ctx context.Context) error {
172175
// Stop stops the file watcher
173176
func (w *Watcher) Stop() error {
174177
w.stopDispatch()
178+
w.stopConfigReloadTimer()
175179
return w.watcher.Close()
176180
}
177181

182+
func (w *Watcher) stopConfigReloadTimer() {
183+
w.configReloadMu.Lock()
184+
if w.configReloadTimer != nil {
185+
w.configReloadTimer.Stop()
186+
w.configReloadTimer = nil
187+
}
188+
w.configReloadMu.Unlock()
189+
}
190+
178191
// SetConfig updates the current configuration
179192
func (w *Watcher) SetConfig(cfg *config.Config) {
180193
w.clientsMutex.Lock()
@@ -476,40 +489,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
476489
// Handle config file changes
477490
if isConfigEvent {
478491
log.Debugf("config file change details - operation: %s, timestamp: %s", event.Op.String(), now.Format("2006-01-02 15:04:05.000"))
479-
data, err := os.ReadFile(w.configPath)
480-
if err != nil {
481-
log.Errorf("failed to read config file for hash check: %v", err)
482-
return
483-
}
484-
if len(data) == 0 {
485-
log.Debugf("ignoring empty config file write event")
486-
return
487-
}
488-
sum := sha256.Sum256(data)
489-
newHash := hex.EncodeToString(sum[:])
490-
491-
w.clientsMutex.RLock()
492-
currentHash := w.lastConfigHash
493-
w.clientsMutex.RUnlock()
494-
495-
if currentHash != "" && currentHash == newHash {
496-
log.Debugf("config file content unchanged (hash match), skipping reload")
497-
return
498-
}
499-
fmt.Printf("config file changed, reloading: %s\n", w.configPath)
500-
if w.reloadConfig() {
501-
finalHash := newHash
502-
if updatedData, errRead := os.ReadFile(w.configPath); errRead == nil && len(updatedData) > 0 {
503-
sumUpdated := sha256.Sum256(updatedData)
504-
finalHash = hex.EncodeToString(sumUpdated[:])
505-
} else if errRead != nil {
506-
log.WithError(errRead).Debug("failed to compute updated config hash after reload")
507-
}
508-
w.clientsMutex.Lock()
509-
w.lastConfigHash = finalHash
510-
w.clientsMutex.Unlock()
511-
w.persistConfigAsync()
512-
}
492+
w.scheduleConfigReload()
513493
return
514494
}
515495

@@ -530,6 +510,57 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
530510
}
531511
}
532512

513+
func (w *Watcher) scheduleConfigReload() {
514+
w.configReloadMu.Lock()
515+
defer w.configReloadMu.Unlock()
516+
if w.configReloadTimer != nil {
517+
w.configReloadTimer.Stop()
518+
}
519+
w.configReloadTimer = time.AfterFunc(configReloadDebounce, func() {
520+
w.configReloadMu.Lock()
521+
w.configReloadTimer = nil
522+
w.configReloadMu.Unlock()
523+
w.reloadConfigIfChanged()
524+
})
525+
}
526+
527+
func (w *Watcher) reloadConfigIfChanged() {
528+
data, err := os.ReadFile(w.configPath)
529+
if err != nil {
530+
log.Errorf("failed to read config file for hash check: %v", err)
531+
return
532+
}
533+
if len(data) == 0 {
534+
log.Debugf("ignoring empty config file write event")
535+
return
536+
}
537+
sum := sha256.Sum256(data)
538+
newHash := hex.EncodeToString(sum[:])
539+
540+
w.clientsMutex.RLock()
541+
currentHash := w.lastConfigHash
542+
w.clientsMutex.RUnlock()
543+
544+
if currentHash != "" && currentHash == newHash {
545+
log.Debugf("config file content unchanged (hash match), skipping reload")
546+
return
547+
}
548+
fmt.Printf("config file changed, reloading: %s\n", w.configPath)
549+
if w.reloadConfig() {
550+
finalHash := newHash
551+
if updatedData, errRead := os.ReadFile(w.configPath); errRead == nil && len(updatedData) > 0 {
552+
sumUpdated := sha256.Sum256(updatedData)
553+
finalHash = hex.EncodeToString(sumUpdated[:])
554+
} else if errRead != nil {
555+
log.WithError(errRead).Debug("failed to compute updated config hash after reload")
556+
}
557+
w.clientsMutex.Lock()
558+
w.lastConfigHash = finalHash
559+
w.clientsMutex.Unlock()
560+
w.persistConfigAsync()
561+
}
562+
}
563+
533564
// reloadConfig reloads the configuration and triggers a full reload
534565
func (w *Watcher) reloadConfig() bool {
535566
log.Debug("=========================== CONFIG RELOAD ============================")

0 commit comments

Comments
 (0)