@@ -41,24 +41,26 @@ type authDirProvider interface {
4141
4242// Watcher manages file watching for configuration and authentication files
4343type 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
6466type stableIDGenerator struct {
@@ -113,7 +115,8 @@ type AuthUpdate struct {
113115const (
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
173176func (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
179192func (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
534565func (w * Watcher ) reloadConfig () bool {
535566 log .Debug ("=========================== CONFIG RELOAD ============================" )
0 commit comments