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():
|
case <-s.ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
case <-s.updates:
|
case <-s.updates:
|
||||||
|
if !s.coalesceUpdates(ctx, reloadDebounce) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
s.reload(logger, onReload)
|
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() {
|
func (s *Store) Close() {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return
|
return
|
||||||
@@ -138,6 +167,9 @@ func (s *Store) reload(logger *zap.Logger, onReload func(ReloadEvent)) {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
changes := Diff(previous, next)
|
changes := Diff(previous, next)
|
||||||
|
if len(changes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
if logger != nil {
|
if logger != nil {
|
||||||
logger.Info("config reloaded",
|
logger.Info("config reloaded",
|
||||||
zap.Strings("files", files),
|
zap.Strings("files", files),
|
||||||
|
|||||||
Reference in New Issue
Block a user