armWatcher lazily initializes the fsnotify-backed configWatcher and syncs it to the latest config paths. Lazy initialization keeps unit tests that never call Reload free of extra goroutines and file descriptors. If the underlying watcher cannot be created (e.g. inotify limit reached), the error is
(paths []string)
| 312 | // primary mechanism; the watcher is an optimization that closes |
| 313 | // the dual-agent race window. |
| 314 | func (m *Manager) armWatcher(paths []string) { |
| 315 | m.watcherOnce.Do(func() { |
| 316 | cw, err := newConfigWatcher( |
| 317 | m.logger.Named("config_watcher"), |
| 318 | m.clock, |
| 319 | m.watchDebounce, |
| 320 | m.handleWatchedConfigChange, |
| 321 | ) |
| 322 | if err != nil { |
| 323 | m.logger.Warn(m.ctx, |
| 324 | "failed to start MCP config watcher; falling back to lazy stat", |
| 325 | slog.Error(err)) |
| 326 | return |
| 327 | } |
| 328 | // Close the watcher if the manager was closed between |
| 329 | // newConfigWatcher returning and us acquiring m.mu. |
| 330 | // Otherwise its goroutine and inotify fd leak. |
| 331 | m.mu.Lock() |
| 332 | if m.closed { |
| 333 | m.mu.Unlock() |
| 334 | _ = cw.Close() |
| 335 | return |
| 336 | } |
| 337 | m.watcher = cw |
| 338 | m.mu.Unlock() |
| 339 | }) |
| 340 | |
| 341 | m.mu.Lock() |
| 342 | m.lastPaths = slices.Clone(paths) |
| 343 | w := m.watcher |
| 344 | closed := m.closed |
| 345 | m.mu.Unlock() |
| 346 | if w == nil || closed { |
| 347 | return |
| 348 | } |
| 349 | w.Sync(paths) |
| 350 | } |
| 351 | |
| 352 | // handleWatchedConfigChange is invoked by the watcher on a |
| 353 | // debounced fire. It triggers a singleflight Reload using the |
no test coverage detected