v0.297.0: burn-down round 2 — 24 small rows (R-591 R-568 R-567 R-363 R-547 R-10 R-552 R-251 R-104 R-619 R-362 R-675 R-256 R-257 R-240 R-365 R-425 R-565 R-564 R-603 R-454 R-208 R-457-swept) + the banner countdown and deepCopyStack twins; MinAgent 0.131.0
gates / gates (push) Failing after 50s
gates / gates (push) Failing after 50s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -228,6 +228,17 @@ func DumpAll(ctx context.Context, dbs []DiscoveredDB, dumpDir string, logger *lo
|
||||
return results
|
||||
}
|
||||
|
||||
// syncDumpDir fsyncs a directory so a rename inside it survives a power cut (R-10). A variable so a
|
||||
// test can observe the call; production never replaces it.
|
||||
var syncDumpDir = func(dir string) error {
|
||||
d, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer d.Close()
|
||||
return d.Sync()
|
||||
}
|
||||
|
||||
// CanonicalDumpName is the app's own dump filename for one database: `<stack>-<dbtype>.sql`. It is
|
||||
// the name the replay loop matches EXACTLY, which is why nothing else may ever be written to it.
|
||||
func CanonicalDumpName(db DiscoveredDB) string {
|
||||
@@ -393,6 +404,14 @@ func DumpOneTo(ctx context.Context, db DiscoveredDB, finalPath string, logger *l
|
||||
result.Duration = time.Since(start)
|
||||
return result
|
||||
}
|
||||
// R-10 (T-6E-1): the rename is durable only once the DIRECTORY entry is on disk. Without this a
|
||||
// power cut right after the rename can leave the old dump (or none) under the final name, even
|
||||
// though the data itself was fsynced above. Best-effort, like atomicPromoteTar's dir.Sync() in
|
||||
// internal/backup/backup.go — the dump is already complete, so a failed dir sync is logged, not
|
||||
// fatal. Pinned by TestDumpOneTo_SyncsTheDumpDirectoryAfterRename.
|
||||
if err := syncDumpDir(dumpDir); err != nil {
|
||||
logger.Printf("[DEBUG] DumpOne: syncing dump directory %s after rename failed (best-effort): %v", dumpDir, err)
|
||||
}
|
||||
|
||||
result.FilePath = finalPath
|
||||
result.Size = stat.Size()
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestAppBindAndCaptureRootAgree(t *testing.T) {
|
||||
const sys = "/mnt/sys_drive"
|
||||
for _, drive := range []string{"/mnt/felhom-usb", "/mnt/felhom-drives/hdd_1", "/mnt/sys_drive"} {
|
||||
nsRoot := NamespaceRootFor(drive, sys)
|
||||
appBind := UserdataDir(nsRoot) // what the deploy sets as ${USERDATA_PATH}
|
||||
appBind := UserdataDir(nsRoot) // what the deploy sets as ${USERDATA_PATH}
|
||||
captureRoot := UserdataDir(nsRoot) // what the capture set resolves RootUserdata against
|
||||
if appBind != captureRoot {
|
||||
t.Fatalf("drive %q: the app binds %q while the backup captures %q", drive, appBind, captureRoot)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package appbackup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-10 (T-6E-1): DumpOneTo fsynced the dump FILE and renamed it, but never fsynced the DIRECTORY, so
|
||||
// the rename itself was not durable — the asymmetry with atomicPromoteTar (backup.go), which does.
|
||||
// The consequence asserted: a successful dump syncs the directory that holds the final file, AFTER
|
||||
// the final name exists (the sync must cover the rename, not precede it).
|
||||
//
|
||||
// No real docker: a `docker` stub in t.TempDir() on PATH (R-650's sanctioned seam) answers the
|
||||
// running-check and prints a small SQL dump.
|
||||
func TestDumpOneTo_SyncsTheDumpDirectoryAfterRename(t *testing.T) {
|
||||
bin := t.TempDir()
|
||||
stub := "#!/bin/sh\ncase \"$1\" in\n inspect) echo true ;;\n exec) printf 'CREATE TABLE t (id int);\\nINSERT INTO t VALUES (1);\\n' ;;\nesac\n"
|
||||
if err := os.WriteFile(filepath.Join(bin, "docker"), []byte(stub), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", bin)
|
||||
|
||||
dumpDir := filepath.Join(t.TempDir(), "unit")
|
||||
final := filepath.Join(dumpDir, "app-postgres.sql")
|
||||
|
||||
var synced []string
|
||||
finalExistedAtSync := false
|
||||
orig := syncDumpDir
|
||||
syncDumpDir = func(dir string) error {
|
||||
synced = append(synced, dir)
|
||||
if _, err := os.Stat(final); err == nil {
|
||||
finalExistedAtSync = true
|
||||
}
|
||||
return orig(dir)
|
||||
}
|
||||
t.Cleanup(func() { syncDumpDir = orig })
|
||||
|
||||
db := DiscoveredDB{ContainerName: "app-db", ContainerID: "0123456789abcdef", DBType: DBTypePostgres, DBUser: "u", DBName: "d", StackName: "app"}
|
||||
res := DumpOneTo(context.Background(), db, final, log.New(io.Discard, "", 0), false)
|
||||
if res.Error != nil {
|
||||
t.Fatalf("dump failed: %v", res.Error)
|
||||
}
|
||||
if _, err := os.Stat(final + ".tmp"); !os.IsNotExist(err) {
|
||||
t.Errorf("the .tmp scratch file is still present after a successful dump")
|
||||
}
|
||||
if len(synced) != 1 || synced[0] != dumpDir {
|
||||
t.Fatalf("R-10: the dump directory was not fsynced after the rename: synced=%v, want [%s]", synced, dumpDir)
|
||||
}
|
||||
if !finalExistedAtSync {
|
||||
t.Errorf("R-10: the directory was synced BEFORE the final name existed — the rename is not covered")
|
||||
}
|
||||
}
|
||||
@@ -190,9 +190,9 @@ func TestImportDumpErrorDoesNotCarryEngineStderr(t *testing.T) {
|
||||
// test; what CAN be pinned is the thing the mutation changes.
|
||||
//
|
||||
// TWO PROPERTIES, both mutation-detectable:
|
||||
// 1. `finalPath` is never reassigned — the caller's destination is the destination.
|
||||
// 2. the scratch file is derived from `finalPath`, not from the canonical name, so a nightly dump
|
||||
// and a safety dump running into the same directory cannot share it.
|
||||
// 1. `finalPath` is never reassigned — the caller's destination is the destination.
|
||||
// 2. the scratch file is derived from `finalPath`, not from the canonical name, so a nightly dump
|
||||
// and a safety dump running into the same directory cannot share it.
|
||||
func TestDumpOneToHonoursTheDestinationItWasGiven(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "dbdump.go", nil, 0)
|
||||
|
||||
Reference in New Issue
Block a user