Bound each shutdown stage by the stop deadline (closes #100)
check / check (push) Canceled after 0s

Shutdown passes its context to every stage: the streamer, the four
handlers, the WHOIS fetcher, the database maintainer and the HTTP server
each stop waiting when it ends. A handler then skips its last write and
logs how many updates it did not write, and the HTTP server closes the
connections still open. The database is checkpointed and closed last,
and left open once the deadline has passed, since a write may still
hold it. The stop hook gives Shutdown 8 seconds and fx 9, inside the 10
seconds upaas and docker stop allow, so a stop signal exits 0.

Model: opus-5-5
This commit was merged in pull request #130.
This commit is contained in:
2026-10-20 02:15:10 +02:00
parent 237f73fc49
commit 09d3073fa5
20 changed files with 1097 additions and 130 deletions
+41 -18
View File
@@ -137,7 +137,12 @@ func (rw *RouteWatch) Run(ctx context.Context) error {
return nil
}
// Shutdown performs graceful shutdown of all services
// Shutdown stops all services in order: the streamer, the handlers, which
// write what they hold, the WHOIS fetcher, the database maintainer and the
// HTTP server, and last the database, which it checkpoints and closes. Each
// stage waits at most until ctx ends, so Shutdown returns soon after: a
// handler logs what it could not write by then, and the HTTP server closes
// the connections of requests still running.
func (rw *RouteWatch) Shutdown(ctx context.Context) {
rw.mu.Lock()
if rw.shutdown {
@@ -150,51 +155,48 @@ func (rw *RouteWatch) Shutdown(ctx context.Context) {
rw.mu.Unlock()
// Stop the streamer first: it returns once the handlers have been handed
// every message it queued. Then each handler writes what it holds.
rw.streamer.Stop()
// every message it queued, or when ctx ends. Then each handler writes
// what it holds.
rw.streamer.Stop(ctx)
if rw.dbHandler != nil {
rw.logger.Info("Flushing database handler")
rw.dbHandler.Stop()
rw.dbHandler.Stop(ctx)
}
if rw.peerHandler != nil {
rw.logger.Info("Flushing peer handler")
rw.peerHandler.Stop()
rw.peerHandler.Stop(ctx)
}
if rw.prefixHandler != nil {
rw.logger.Info("Flushing prefix handler")
rw.prefixHandler.Stop()
rw.prefixHandler.Stop(ctx)
}
if rw.peeringHandler != nil {
rw.logger.Info("Flushing peering handler")
rw.peeringHandler.Stop()
rw.peeringHandler.Stop(ctx)
}
// Stop ASN WHOIS fetcher
if rw.asnFetcher != nil {
rw.asnFetcher.Stop()
rw.asnFetcher.Stop(ctx)
}
// Stop database maintainer
if rw.dbMaintainer != nil {
rw.dbMaintainer.Stop()
rw.dbMaintainer.Stop(ctx)
}
// Stop HTTP server with a timeout of its own, however much of ctx is left
const serverStopTimeout = 5 * time.Second
stopCtx, cancel := context.WithTimeout(
context.WithoutCancel(ctx), serverStopTimeout)
defer cancel()
err := rw.server.Stop(stopCtx)
err := rw.server.Stop(ctx)
if err != nil {
rw.logger.Error("Failed to stop HTTP server gracefully", "error", err)
rw.logger.Error("Stopped the HTTP server before every request finished",
"error", err)
}
rw.closeDatabase(ctx)
// Log final metrics
metrics := rw.streamer.GetMetrics()
rw.logger.Info("Final metrics",
@@ -206,6 +208,27 @@ func (rw *RouteWatch) Shutdown(ctx context.Context) {
)
}
// closeDatabase checkpoints and closes the database, unless ctx has ended: a
// write may then still be running, and both would wait for it. SQLite rolls
// back a write cut off by the exit when the database next opens.
func (rw *RouteWatch) closeDatabase(ctx context.Context) {
if ctx.Err() != nil {
rw.logger.Error("Stop deadline passed; the database is not closed")
return
}
err := rw.db.Checkpoint(ctx)
if err != nil {
rw.logger.Error("Failed to checkpoint the database", "error", err)
}
err = rw.db.Close()
if err != nil {
rw.logger.Error("Failed to close the database", "error", err)
}
}
// getModule provides all fx dependencies
func getModule() []fx.Option {
return []fx.Option{