carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "github.com/carlosframework/platform/internal/hostbox"
9 "github.com/carlosframework/platform/internal/hostguard"
10 "github.com/carlosframework/platform/internal/registry"
11)
12
13func TestPlan14GenerationTimerRetirement(t *testing.T) {
14 t.Setenv("CARLOS_HOST_CLAIM", "true")
15 for _, name := range []string{"exact", "replacement", "unrecorded", "legacy-unrecorded", "abandoned", "intended-row", "withdrawn"} {
16 mismatch := name == "replacement" || name == "unrecorded"
17 t.Run(name, func(t *testing.T) {
18 coordinator, req, _ := claimStopFixture(t)
19 result, err := coordinator.FenceAndStop(t.Context(), req)
20 if err != nil {
21 t.Fatal(err)
22 }
23 base := t.TempDir()
24 inventory, err := openLegacyInventory(t, filepath.Join(base, "inventory.db"))
25 if err != nil {
26 t.Fatal(err)
27 }
28 defer inventory.Close()
29 reg, err := registry.Open(filepath.Join(base, "registry.db"))
30 if err != nil {
31 t.Fatal(err)
32 }
33 defer reg.Close()
34 owner := registry.ClaimOwnership{ClaimIncarnation: req.Incarnation, ClaimStamp: req.InstallationStamp, ClaimOwner: "installation", ClaimRowID: "route-1"}
35 row := registry.Route{Host: req.Host, ClaimOwnership: owner}
36 if err := reg.Upsert(row); err != nil {
37 t.Fatal(err)
38 }
39 timer := registry.Timer{Host: req.Host, Name: "backup", Kind: "cron", Registration: "timer-1", ClaimOwnership: owner}
40 timer.ClaimRowID = "timer-1"
41 if name == "replacement" {
42 timer.Registration = "replacement"
43 timer.ClaimRowID = "replacement"
44 }
45 if err := reg.ReplaceTimers(req.Host, []registry.Timer{timer}); err != nil {
46 t.Fatal(err)
47 }
48 in := hostbox.StopInventory{Complete: true, Host: req.Host, Incarnation: req.Incarnation, InstallationStamp: req.InstallationStamp, Box: req.Box, Enrollment: req.Enrollment, DirectoryRevision: req.ExpectedDirectoryRevision, Entries: []hostbox.StopInventoryEntry{
49 {Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: req.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"},
50 {Kind: "timer", ArtifactID: "timer", ManagedRoot: "registry", RelativePath: req.Host + "/backup", RowIdentity: "timer-1", Ownership: owner.ClaimOwner, Materialization: "materialized"},
51 }}
52 if name == "unrecorded" || name == "legacy-unrecorded" {
53 in.Entries = in.Entries[:1]
54 }
55 if name == "abandoned" || name == "intended-row" {
56 in.Entries[1].Materialization = "intended"
57 }
58 if name == "withdrawn" {
59 in.Entries[1].TimerRetired = true
60 }
61 if name == "abandoned" || name == "withdrawn" {
62 if err := reg.DeleteTimerOwnedRegistration(timer); err != nil {
63 t.Fatal(err)
64 }
65 }
66 if err := inventory.WriteStopInventory(t.Context(), in); err != nil {
67 t.Fatal(err)
68 }
69 journal := filepath.Join(base, "retire")
70 root := ClaimRootHTTP{Stop: ClaimStopHTTP{Coordinator: coordinator}, Roots: map[string]string{"data": base}, Reconcile: hostbox.ReconcileAdapter{RequireTimerInventory: name != "legacy-unrecorded", Inventory: inventory, Registry: reg, Retirement: hostbox.TypedRetirement{JournalDir: journal}}}
71 // Remote acknowledgement is deliberately unavailable. Local completion
72 // must already be durable, and cannot be confused with the remote result.
73 if err := root.retire(t.Context(), result.Target); err == nil {
74 t.Fatal("missing claim accepted")
75 } else {
76 t.Logf("retirement: %v", err)
77 }
78 got, err := reg.Get(req.Host)
79 if err != nil {
80 t.Fatal(err)
81 }
82 timers, err := reg.TimersFor(req.Host)
83 if err != nil {
84 t.Fatal(err)
85 }
86 if mismatch {
87 if got == nil || len(timers) != 1 || timers[0].Registration != timer.Registration {
88 t.Fatal("replacement changed")
89 }
90 return
91 }
92 if got != nil || len(timers) != 0 {
93 t.Fatalf("retained timer prevented exact retirement: route=%v timers=%v", got, timers)
94 }
95 records, err := generationRetirementRecords(t.Context(), &rollbackDisk{dir: journal})
96 if err != nil || len(records) != 1 || !records[0].Complete {
97 t.Fatal("missing local completion", records, err)
98 }
99 if name != "legacy-unrecorded" {
100 receipt, err := coordinator.Guard.FenceReceiptFor(t.Context(), result.Target)
101 if err != nil {
102 t.Fatal(err)
103 }
104 snapshot := generationInventory{Retirements: records, Payloads: []hostbox.GenerationPayload{{Inventory: in}}, Installations: []hostguard.FenceView{{Host: req.Host, Receipts: []hostguard.FenceReceipt{receipt}}}}
105 if _, err := generationPhysicalPayloads(t.Context(), snapshot); err != nil {
106 t.Fatal("completed timer retirement still blocks completeness", err)
107 }
108 }
109 if len(records[0].Inventory.Artifacts) != 2 {
110 t.Fatal("duplicated timer", records[0])
111 }
112 before, err := os.ReadFile(filepath.Join(journal, result.Target.FenceID+".json"))
113 if err != nil {
114 t.Fatal(err)
115 }
116 row.ClaimStamp++
117 row.ClaimOwner = "successor"
118 row.ClaimRowID = "route-2"
119 if err := reg.Upsert(row); err != nil {
120 t.Fatal(err)
121 }
122 timer.ClaimOwnership = row.ClaimOwnership
123 timer.ClaimRowID = "timer-2"
124 timer.Registration = "timer-2"
125 if err := reg.ReplaceTimers(req.Host, []registry.Timer{timer}); err != nil {
126 t.Fatal(err)
127 }
128 if err := root.retire(t.Context(), result.Target); err == nil {
129 t.Fatal("missing claim accepted on replay")
130 }
131 got, err = reg.Get(req.Host)
132 if err != nil || got == nil || got.ClaimRowID != "route-2" {
133 t.Fatal("successor route changed", got, err)
134 }
135 timers, err = reg.TimersFor(req.Host)
136 if err != nil || len(timers) != 1 || timers[0].Registration != "timer-2" {
137 t.Fatal("successor timer changed", timers, err)
138 }
139 after, err := os.ReadFile(filepath.Join(journal, result.Target.FenceID+".json"))
140 if err != nil || string(before) != string(after) {
141 t.Fatal("replay changed completed journal", err)
142 }
143 })
144 }
145}
146