router: registry watcher — fingerprint poll instead of PRAGMA data_version (#767)
data_version proved blind to cross-process commits on the production box (modernc/sqlite WAL-visibility quirk under the router's long-lived connections) — the watcher sat silent while the tunnel gateway inserted routes, and only a SIGHUP made them serve. Ordinary SELECTs on the same handle do see fresh rows, so poll a cheap fingerprint of the routes table (count, max rowid, a status/socket-sensitive sum) every two seconds and reload when it moves.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+18
−11
router.go+18 −11
diff --git a/router.go b/router.go| index 3f797c0..8b7ae69 100644 |
| --- a/router.go |
| +++ b/router.go |
| @@ -313,25 +313,32 @@ func runRouter(args []string) { |
| } |
| // Watch the registry for writes from other processes (the tunnel gateway |
| - // publishes trial routes this way, without needing rights to signal us). |
| - // SQLite's data_version increments on any commit by another connection, so |
| - // this is one cheap PRAGMA every couple of seconds — the route map itself |
| - // is only rebuilt when something actually changed. |
| + // publishes routes this way, without needing rights to signal us). We poll |
| + // a cheap fingerprint of the routes table rather than PRAGMA data_version: |
| + // the pragma proved blind to cross-process commits on the production box |
| + // (a modernc/sqlite WAL-visibility quirk under the router's long-lived |
| + // connections), while ordinary SELECTs on this same handle demonstrably do |
| + // see fresh rows — reload()'s own queries are the proof. The fingerprint |
| + // moves on any insert, delete, or status/socket change; scanning a few |
| + // dozen rows every two seconds is noise. |
| go func() { |
| - var last int64 = -1 |
| + var last string |
| + first := true |
| for range time.Tick(2 * time.Second) { |
| - var v int64 |
| - if err := reg.sql.QueryRow("PRAGMA data_version").Scan(&v); err != nil { |
| + 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) |
| + if err != nil { |
| continue |
| } |
| - if last == -1 { |
| - last = v |
| + if first { |
| + last, first = fp, false |
| continue |
| } |
| - if v == last { |
| + if fp == last { |
| continue |
| } |
| - last = v |
| + last = fp |
| if err := rt.reload(); err != nil { |
| log.Printf("router: registry-watch reload failed: %v", err) |
| continue |