activator: an installation-complete marker, and the rule for trusting a local copy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+228
−0
internal/activator/installed.go+122 −0internal/activator/installed_test.go+106 −0
diff --git a/internal/activator/installed.go b/internal/activator/installed.go| new file mode 100644 |
| index 000000000..8ebdd6b3d |
| --- /dev/null |
| +++ b/internal/activator/installed.go |
| @@ -0,0 +1,122 @@ |
| +package activator |
| + |
| +import ( |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// InstallMarker is a box's own record that it finished installing one |
| +// generation of a host: the database restored and synced, the files |
| +// applied, the wake state imported (region HA spec 1 §5.5). It is written |
| +// last, so its presence vouches for all of them. |
| +// |
| +// It is evidence, not authority. Authority is the claim naming this box, |
| +// which the box holds as its active stamp. A box that crashed mid-install |
| +// has authority and no marker, and must restore. |
| +type InstallMarker struct { |
| + Incarnation string `json:"incarnation"` |
| + Generation string `json:"generation"` |
| + Stamp int64 `json:"stamp"` |
| + DBMarker string `json:"db_marker"` |
| + FilesSeq int64 `json:"files_seq"` |
| +} |
| + |
| +// Installation is what the claim says is current for a host. |
| +type Installation struct { |
| + Incarnation string |
| + Generation string |
| + Stamp int64 |
| +} |
| + |
| +func InstallMarkerPath(dataDir, host string) string { |
| + return filepath.Join(dataDir, ".installed", provision.InstanceID(host)+".json") |
| +} |
| + |
| +// WriteInstallMarker replaces the marker atomically and syncs both the |
| +// file and its directory, so a marker that is read back was fully written. |
| +func WriteInstallMarker(dataDir, host string, m InstallMarker) error { |
| + path := InstallMarkerPath(dataDir, host) |
| + dir := filepath.Dir(path) |
| + if err := os.MkdirAll(dir, 0o700); err != nil { |
| + return err |
| + } |
| + body, err := json.Marshal(m) |
| + if err != nil { |
| + return err |
| + } |
| + tmp, err := os.CreateTemp(dir, ".marker-*") |
| + if err != nil { |
| + return err |
| + } |
| + defer os.Remove(tmp.Name()) |
| + if _, err := tmp.Write(body); err != nil { |
| + tmp.Close() |
| + return err |
| + } |
| + if err := tmp.Sync(); err != nil { |
| + tmp.Close() |
| + return err |
| + } |
| + if err := tmp.Close(); err != nil { |
| + return err |
| + } |
| + if err := os.Rename(tmp.Name(), path); err != nil { |
| + return err |
| + } |
| + d, err := os.Open(dir) |
| + if err != nil { |
| + return err |
| + } |
| + defer d.Close() |
| + return d.Sync() |
| +} |
| + |
| +// ReadInstallMarker returns the marker and whether there is one. A marker |
| +// that exists but cannot be parsed is an error, never "absent": the caller |
| +// restores either way, but it must not mistake damage for a clean slate. |
| +func ReadInstallMarker(dataDir, host string) (InstallMarker, bool, error) { |
| + body, err := os.ReadFile(InstallMarkerPath(dataDir, host)) |
| + if errors.Is(err, os.ErrNotExist) { |
| + return InstallMarker{}, false, nil |
| + } |
| + if err != nil { |
| + return InstallMarker{}, false, err |
| + } |
| + var m InstallMarker |
| + if err := json.Unmarshal(body, &m); err != nil { |
| + return InstallMarker{}, false, fmt.Errorf("activator: install marker for %s: %w", host, err) |
| + } |
| + return m, true, nil |
| +} |
| + |
| +func RemoveInstallMarker(dataDir, host string) error { |
| + err := os.Remove(InstallMarkerPath(dataDir, host)) |
| + if errors.Is(err, os.ErrNotExist) { |
| + return nil |
| + } |
| + return err |
| +} |
| + |
| +// WarmCopyTrusted is invariant HA3: a box may skip restore and serve its |
| +// local copy only when it has authority for the current installation |
| +// (activeStamp equals the claim's stamp) AND evidence that it finished |
| +// installing exactly that one (the marker matches the claim, and the |
| +// database on disk is the one the marker was written for). |
| +// |
| +// dbMarker is the generation marker read from beside the local database. |
| +// Every empty value fails: nothing matches by being absent on both sides. |
| +func WarmCopyTrusted(want Installation, activeStamp int64, m InstallMarker, found bool, dbMarker string) bool { |
| + if !found || want.Stamp == 0 || want.Incarnation == "" || want.Generation == "" || dbMarker == "" { |
| + return false |
| + } |
| + return activeStamp == want.Stamp && |
| + m.Stamp == want.Stamp && |
| + m.Incarnation == want.Incarnation && |
| + m.Generation == want.Generation && |
| + m.DBMarker == dbMarker |
| +} |
diff --git a/internal/activator/installed_test.go b/internal/activator/installed_test.go| new file mode 100644 |
| index 000000000..d6551421b |
| --- /dev/null |
| +++ b/internal/activator/installed_test.go |
| @@ -0,0 +1,106 @@ |
| +package activator |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +func TestInstallMarkerRoundTrips(t *testing.T) { |
| + dir := t.TempDir() |
| + if _, found, err := ReadInstallMarker(dir, "shop.test"); found || err != nil { |
| + t.Fatalf("before any write: found=%v err=%v", found, err) |
| + } |
| + m := InstallMarker{Incarnation: "inc1", Generation: "g-op7", Stamp: 41, DBMarker: "dbm", FilesSeq: 1882} |
| + if err := WriteInstallMarker(dir, "shop.test", m); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, found, err := ReadInstallMarker(dir, "shop.test") |
| + if err != nil || !found || got != m { |
| + t.Fatalf("got %+v found=%v err=%v", got, found, err) |
| + } |
| + m.Stamp = 57 |
| + if err := WriteInstallMarker(dir, "shop.test", m); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, _, _ := ReadInstallMarker(dir, "shop.test"); got.Stamp != 57 { |
| + t.Fatalf("a second write did not replace the first: %+v", got) |
| + } |
| + if err := RemoveInstallMarker(dir, "shop.test"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := RemoveInstallMarker(dir, "shop.test"); err != nil { |
| + t.Fatalf("removing an absent marker: %v", err) |
| + } |
| + if _, found, _ := ReadInstallMarker(dir, "shop.test"); found { |
| + t.Fatal("marker still there") |
| + } |
| +} |
| + |
| +func TestInstallMarkerLeavesNoTemporaryFile(t *testing.T) { |
| + dir := t.TempDir() |
| + if err := WriteInstallMarker(dir, "shop.test", InstallMarker{Stamp: 1}); err != nil { |
| + t.Fatal(err) |
| + } |
| + entries, err := os.ReadDir(filepath.Join(dir, ".installed")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(entries) != 1 { |
| + t.Fatalf("%d files in .installed, want 1", len(entries)) |
| + } |
| +} |
| + |
| +func TestInstallMarkerForALongHostHasAShortFileName(t *testing.T) { |
| + host := strings.Repeat("a", 200) + ".example.com" |
| + p := InstallMarkerPath(t.TempDir(), host) |
| + if n := len(filepath.Base(p)); n > 64 { |
| + t.Fatalf("file name is %d bytes", n) |
| + } |
| +} |
| + |
| +func TestADamagedMarkerIsAnErrorNotAbsence(t *testing.T) { |
| + dir := t.TempDir() |
| + if err := WriteInstallMarker(dir, "shop.test", InstallMarker{Stamp: 1}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(InstallMarkerPath(dir, "shop.test"), []byte("{half"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, found, err := ReadInstallMarker(dir, "shop.test"); err == nil || found { |
| + t.Fatalf("found=%v err=%v: a damaged marker must be an error", found, err) |
| + } |
| +} |
| + |
| +func TestWarmCopyTrusted(t *testing.T) { |
| + want := Installation{Incarnation: "inc1", Generation: "g-op7", Stamp: 41} |
| + good := InstallMarker{Incarnation: "inc1", Generation: "g-op7", Stamp: 41, DBMarker: "dbm", FilesSeq: 9} |
| + if !WarmCopyTrusted(want, 41, good, true, "dbm") { |
| + t.Fatal("a complete, current installation was not trusted") |
| + } |
| + for name, tc := range map[string]struct { |
| + active int64 |
| + m InstallMarker |
| + found bool |
| + db string |
| + }{ |
| + "no marker: crashed before the install finished": {41, InstallMarker{}, false, "dbm"}, |
| + "authority without evidence": {41, InstallMarker{Incarnation: "inc1", Generation: "g-op7", Stamp: 40, DBMarker: "dbm"}, true, "dbm"}, |
| + "evidence without authority": {40, good, true, "dbm"}, |
| + "no authority at all": {0, good, true, "dbm"}, |
| + "another generation": {41, InstallMarker{Incarnation: "inc1", Generation: "g-op6", Stamp: 41, DBMarker: "dbm"}, true, "dbm"}, |
| + "another incarnation": {41, InstallMarker{Incarnation: "inc0", Generation: "g-op7", Stamp: 41, DBMarker: "dbm"}, true, "dbm"}, |
| + "the database is not the one installed": {41, good, true, "other"}, |
| + "the database has no marker": {41, good, true, ""}, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + if WarmCopyTrusted(want, tc.active, tc.m, tc.found, tc.db) { |
| + t.Fatal("trusted") |
| + } |
| + }) |
| + } |
| + if WarmCopyTrusted(Installation{}, 0, InstallMarker{}, true, "") { |
| + t.Fatal("an empty claim matched an empty marker") |
| + } |
| +} |