watcher: save state when it stops and wait for that save (closes #114)
check / check (push) Successful in 1m24s
check / check (push) Successful in 1m24s
The final save at shutdown came from the state's own stop hook, while the watcher's stop hook only cancelled its run loop, so a check under way could change state after that save or be cut off at exit. Run now saves state as it returns, and the watcher's stop hook waits for Run, bounded by the shutdown deadline. The state's own save stays; Save holds the state lock for the whole write, so the two cannot overlap. The start hook derives the watcher's context with WithoutCancel, so the linter needs no exception. A new test stops a watcher built by New and reads the change back from the state file, with no DNS. Model: opus-5-5
This commit was merged in pull request #186.
This commit is contained in:
+31
-13
@@ -56,6 +56,7 @@ type Watcher struct {
|
||||
tlsCheck TLSChecker
|
||||
notify Notifier
|
||||
cancel context.CancelFunc
|
||||
done chan struct{} // closed when Run returns
|
||||
firstRun bool
|
||||
expiryNotifiedMu sync.Mutex
|
||||
expiryNotified map[string]time.Time
|
||||
@@ -79,31 +80,47 @@ func New(
|
||||
}
|
||||
|
||||
lifecycle.Append(fx.Hook{
|
||||
OnStart: func(_ context.Context) error {
|
||||
// Use context.Background() — the fx startup context
|
||||
// expires after startup completes, so deriving from it
|
||||
// would cancel the watcher immediately. The watcher's
|
||||
// lifetime is controlled by w.cancel in OnStop.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
OnStart: func(startCtx context.Context) error {
|
||||
// The fx startup context expires after startup
|
||||
// completes, so the watcher's context drops its
|
||||
// cancellation. The watcher's lifetime is controlled
|
||||
// by w.cancel in OnStop.
|
||||
ctx, cancel := context.WithCancel(
|
||||
context.WithoutCancel(startCtx),
|
||||
)
|
||||
w.cancel = cancel
|
||||
w.done = make(chan struct{})
|
||||
|
||||
go w.Run(ctx) //nolint:contextcheck // intentionally not derived from startCtx
|
||||
go func() {
|
||||
defer close(w.done)
|
||||
|
||||
w.Run(ctx)
|
||||
}()
|
||||
|
||||
return nil
|
||||
},
|
||||
OnStop: func(_ context.Context) error {
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
OnStop: func(ctx context.Context) error {
|
||||
w.cancel()
|
||||
|
||||
return nil
|
||||
// Run saves state as it returns. Waiting for it here
|
||||
// means the save is done before shutdown goes on.
|
||||
select {
|
||||
case <-w.done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf(
|
||||
"waiting for the watcher to stop: %w",
|
||||
ctx.Err(),
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Run starts the monitoring loop with periodic scheduling.
|
||||
// Run starts the monitoring loop with periodic scheduling. When ctx
|
||||
// is cancelled, it saves state and returns.
|
||||
func (w *Watcher) Run(ctx context.Context) {
|
||||
w.log.Info(
|
||||
"watcher starting",
|
||||
@@ -125,6 +142,7 @@ func (w *Watcher) Run(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
w.saveState()
|
||||
w.log.Info("watcher stopped")
|
||||
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user