fix: prevent mutex deadlock on panic in config watcher
- commit
- 200ec03cdfacac1a547f7a06c7013ea8bf021b03
- author
- brice · · 2026-01-24 16:36 UTC
- parent
- 677e3c0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +11 -2
M internal/infrastructure/config/watcher.go +11 -2
| @@ -24,6 +24,14 @@ func (m *Manager) Watch() error { | |||
| 24 | 24 | // Acquire write lock before reload (reload modifies m.config) | |
| 25 | 25 | m.mu.Lock() | |
| 26 | 26 | ||
| 27 | + | // Track whether notifyCallbacksLocked will handle the unlock | |
| 28 | + | willNotify := false | |
| 29 | + | defer func() { | |
| 30 | + | if !willNotify { | |
| 31 | + | m.mu.Unlock() | |
| 32 | + | } | |
| 33 | + | }() | |
| 34 | + | ||
| 27 | 35 | // Skip reload if this was triggered by our own Save() - the in-memory | |
| 28 | 36 | // config is already correct and viper may have stale cached data | |
| 29 | 37 | if m.skipNextReload { | |
| @@ -34,6 +42,7 @@ func (m *Manager) Watch() error { | |||
| 34 | 42 | log.Warn().Err(err).Msg("failed to sync viper config after Save") | |
| 35 | 43 | // Continue anyway - in-memory config is correct, just viper internal state is off | |
| 36 | 44 | } | |
| 45 | + | willNotify = true | |
| 37 | 46 | m.notifyCallbacksLocked() | |
| 38 | 47 | return | |
| 39 | 48 | } | |
| @@ -41,9 +50,9 @@ func (m *Manager) Watch() error { | |||
| 41 | 50 | log.Debug().Msg("reloading config from external change") | |
| 42 | 51 | if err := m.reload(); err != nil { | |
| 43 | 52 | log.Warn().Err(err).Msg("failed to reload config") | |
| 44 | - | m.mu.Unlock() | |
| 45 | - | return | |
| 53 | + | return // defer will unlock | |
| 46 | 54 | } | |
| 55 | + | willNotify = true | |
| 47 | 56 | m.notifyCallbacksLocked() | |
| 48 | 57 | }) | |
| 49 | 58 | ||
| @@ -24,6 +24,14 @@ func (m *Manager) Watch() error { | |||||
| 24 | // Acquire write lock before reload (reload modifies m.config) | 24 | // Acquire write lock before reload (reload modifies m.config) | ||
| 25 | m.mu.Lock() | 25 | m.mu.Lock() | ||
| 26 | 26 | ||||
| 27 | + | // Track whether notifyCallbacksLocked will handle the unlock | |||
| 28 | + | willNotify := false | |||
| 29 | + | defer func() { | |||
| 30 | + | if !willNotify { | |||
| 31 | + | m.mu.Unlock() | |||
| 32 | + | } | |||
| 33 | + | }() | |||
| 34 | + | ||||
| 27 | // Skip reload if this was triggered by our own Save() - the in-memory | 35 | // Skip reload if this was triggered by our own Save() - the in-memory | ||
| 28 | // config is already correct and viper may have stale cached data | 36 | // config is already correct and viper may have stale cached data | ||
| 29 | if m.skipNextReload { | 37 | if m.skipNextReload { | ||
| @@ -34,6 +42,7 @@ func (m *Manager) Watch() error { | |||||
| 34 | log.Warn().Err(err).Msg("failed to sync viper config after Save") | 42 | log.Warn().Err(err).Msg("failed to sync viper config after Save") | ||
| 35 | // Continue anyway - in-memory config is correct, just viper internal state is off | 43 | // Continue anyway - in-memory config is correct, just viper internal state is off | ||
| 36 | } | 44 | } | ||
| 45 | + | willNotify = true | |||
| 37 | m.notifyCallbacksLocked() | 46 | m.notifyCallbacksLocked() | ||
| 38 | return | 47 | return | ||
| 39 | } | 48 | } | ||
| @@ -41,9 +50,9 @@ func (m *Manager) Watch() error { | |||||
| 41 | log.Debug().Msg("reloading config from external change") | 50 | log.Debug().Msg("reloading config from external change") | ||
| 42 | if err := m.reload(); err != nil { | 51 | if err := m.reload(); err != nil { | ||
| 43 | log.Warn().Err(err).Msg("failed to reload config") | 52 | log.Warn().Err(err).Msg("failed to reload config") | ||
| 44 | - | m.mu.Unlock() | 53 | + | return // defer will unlock |
| 45 | - | return | |||
| 46 | } | 54 | } | ||
| 55 | + | willNotify = true | |||
| 47 | m.notifyCallbacksLocked() | 56 | m.notifyCallbacksLocked() | ||
| 48 | }) | 57 | }) | ||
| 49 | 58 | ||||