fix(config): debounce fsnotify reload events to avoid mid-write reads
Backend CI / Backend (push) Successful in 13m20s
Backend CI / Backend (push) Successful in 13m20s
os.WriteFile truncates then writes, so inotify can deliver back-to-back MODIFY events while the file is momentarily empty. Reading mid-sequence made TestStoreReloadsWhenLocalOverrideIsCreated and TestStoreReloadsConfigAndKeepsPreviousOnInvalidYAML flake on Linux CI by firing onReload with a blank config before the write settled. Coalesce updates over a 50ms quiet window before reloading, and skip onReload when the diff is empty so subscribers only see real changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -103,11 +103,40 @@ func (s *Store) Watch(ctx context.Context, logger *zap.Logger, onReload func(Rel
|
||||
case <-s.ctx.Done():
|
||||
return nil
|
||||
case <-s.updates:
|
||||
if !s.coalesceUpdates(ctx, reloadDebounce) {
|
||||
return nil
|
||||
}
|
||||
s.reload(logger, onReload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reloadDebounce coalesces fsnotify events from a single write. os.WriteFile
|
||||
// opens with O_CREATE|O_TRUNC then writes, so inotify can deliver back-to-back
|
||||
// MODIFY events with the file momentarily empty. Reading mid-sequence yields a
|
||||
// blank config; waiting for a brief quiet period lets the write settle.
|
||||
const reloadDebounce = 50 * time.Millisecond
|
||||
|
||||
func (s *Store) coalesceUpdates(ctx context.Context, window time.Duration) bool {
|
||||
timer := time.NewTimer(window)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-s.ctx.Done():
|
||||
return false
|
||||
case <-s.updates:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
timer.Reset(window)
|
||||
case <-timer.C:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
if s == nil {
|
||||
return
|
||||
@@ -138,6 +167,9 @@ func (s *Store) reload(logger *zap.Logger, onReload func(ReloadEvent)) {
|
||||
s.mu.Unlock()
|
||||
|
||||
changes := Diff(previous, next)
|
||||
if len(changes) == 0 {
|
||||
return
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Info("config reloaded",
|
||||
zap.Strings("files", files),
|
||||
|
||||
Reference in New Issue
Block a user