registry: SnapshotTo — consistent VACUUM INTO copy for the snapshot pass
2 files changed,
+78
−0
internal/registry/registry.go+25 −0internal/registry/snapshot_test.go+53 −0
diff --git a/internal/registry/registry.go b/internal/registry/registry.go| index a0778d0b8..e62115a8a 100644 |
| --- a/internal/registry/registry.go |
| +++ b/internal/registry/registry.go |
| @@ -13,6 +13,7 @@ import ( |
| "crypto/sha256" |
| "database/sql" |
| "fmt" |
| + "os" |
| "regexp" |
| "strings" |
| "time" |
| @@ -1295,3 +1296,27 @@ FROM fleet_boxes ORDER BY fleet, label`) |
| return fmt.Sprintf("%d:%x", n, h.Sum(nil)), nil |
| } |
| + |
| +// SnapshotTo writes a consistent single-file copy of the registry to path |
| +// via VACUUM INTO on a fresh connection — same fresh-connection recipe as |
| +// Fingerprint (modernc WAL visibility). path must not already exist: |
| +// modernc's VACUUM INTO silently overwrites an existing file rather than |
| +// refusing it (unlike upstream SQLite's CLI), so we check first — callers |
| +// stage into a temp dir. The bound-parameter form (not fmt.Sprintf/%q) is |
| +// required: %q is Go string escaping, not SQL escaping, so a path |
| +// containing a backslash gets VACUUM INTO writing to a different on-disk |
| +// path than the one os.Stat just checked, silently defeating the guard. |
| +func (r *Registry) SnapshotTo(path string) error { |
| + if _, err := os.Stat(path); err == nil { |
| + return fmt.Errorf("registry: snapshot target already exists: %s", path) |
| + } else if !os.IsNotExist(err) { |
| + return err |
| + } |
| + db, err := sql.Open("sqlite", r.path) |
| + if err != nil { |
| + return err |
| + } |
| + defer db.Close() |
| + _, err = db.Exec("VACUUM INTO ?", path) |
| + return err |
| +} |
diff --git a/internal/registry/snapshot_test.go b/internal/registry/snapshot_test.go| new file mode 100644 |
| index 000000000..65d73c1e2 |
| --- /dev/null |
| +++ b/internal/registry/snapshot_test.go |
| @@ -0,0 +1,53 @@ |
| +package registry |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "testing" |
| +) |
| + |
| +func TestSnapshotToProducesOpenableCopy(t *testing.T) { |
| + dir := t.TempDir() |
| + reg, err := Open(filepath.Join(dir, "registry.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer reg.Close() |
| + if err := reg.Upsert(Route{Host: "a.example.com", Kind: "service", Addr: "10.0.0.1:3000", Status: "ready"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + out := filepath.Join(dir, "snap.db") |
| + if err := reg.SnapshotTo(out); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + copyReg, err := Open(out) |
| + if err != nil { |
| + t.Fatalf("open snapshot: %v", err) |
| + } |
| + defer copyReg.Close() |
| + rt, err := copyReg.Get("a.example.com") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if rt.Addr != "10.0.0.1:3000" { |
| + t.Fatalf("snapshot route addr = %q, want 10.0.0.1:3000", rt.Addr) |
| + } |
| +} |
| + |
| +func TestSnapshotToRefusesExistingTarget(t *testing.T) { |
| + dir := t.TempDir() |
| + reg, err := Open(filepath.Join(dir, "registry.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer reg.Close() |
| + out := filepath.Join(dir, "snap.db") |
| + if err := os.WriteFile(out, []byte("x"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.SnapshotTo(out); err == nil { |
| + t.Fatal("SnapshotTo over an existing file: want error, got nil") |
| + } |
| +} |