router: registry watcher polls on a fresh connection each tick (#769)
The shared long-lived handle went blind to cross-process commits in production — first PRAGMA data_version never advanced (#767), then even the fingerprint SELECT read a stale snapshot in one process lifetime while an identical process later saw changes fine. A brand-new connection per poll reads the latest committed state by construction; opening SQLite costs ~1ms, nothing at a 2s cadence. Also arms loudly and logs poll errors (deduped) instead of swallowing them.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+27
−3
router.go+27 −3
diff --git a/router.go b/router.go| index 8b7ae69..98c4abd 100644 |
| --- a/router.go |
| +++ b/router.go |
| @@ -3,6 +3,7 @@ package main |
| import ( |
| "context" |
| "crypto/tls" |
| + "database/sql" |
| "errors" |
| "flag" |
| "fmt" |
| @@ -322,15 +323,38 @@ func runRouter(args []string) { |
| // moves on any insert, delete, or status/socket change; scanning a few |
| // dozen rows every two seconds is noise. |
| go func() { |
| + log.Printf("router: registry watcher armed (2s fingerprint poll)") |
| + // Each poll opens a FRESH connection: a brand-new connection reads the |
| + // latest committed state by construction, immune to whatever pins a |
| + // long-lived handle to a stale WAL snapshot (observed in production: |
| + // the shared handle went blind to cross-process commits — first with |
| + // PRAGMA data_version, then intermittently with plain SELECTs — while |
| + // separate processes saw fresh rows throughout). Opening SQLite costs |
| + // about a millisecond; at one poll every two seconds that is nothing. |
| + poll := func() (string, error) { |
| + db, err := sql.Open("sqlite", *registryPath) |
| + if err != nil { |
| + return "", err |
| + } |
| + defer db.Close() |
| + var fp string |
| + err = db.QueryRow(`SELECT COUNT(*) || ':' || COALESCE(MAX(rowid),0) || ':' || |
| + COALESCE(SUM(rowid*17 + LENGTH(status)*3 + LENGTH(socket)),0) FROM routes`).Scan(&fp) |
| + return fp, err |
| + } |
| var last string |
| first := true |
| + errs := 0 |
| for range time.Tick(2 * time.Second) { |
| - var fp string |
| - err := reg.sql.QueryRow(`SELECT COUNT(*) || ':' || COALESCE(MAX(rowid),0) || ':' || |
| - COALESCE(SUM(rowid*17 + LENGTH(status)*3 + LENGTH(socket)),0) FROM routes`).Scan(&fp) |
| + fp, err := poll() |
| if err != nil { |
| + errs++ |
| + if errs == 1 || errs%300 == 0 { |
| + log.Printf("router: registry-watch poll error (#%d): %v", errs, err) |
| + } |
| continue |
| } |
| + errs = 0 |
| if first { |
| last, first = fp, false |
| continue |