carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "errors"
8 "math"
9 "strconv"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/hostguard"
14)
15
16// Fix round 1, finding 3: root bounds an agent-sent RosterRevision by
17// 0 < n <= the revision of the roster it reads and verifies itself at that
18// moment, before any durable fence, drain or receipt. Through the real
19// instance-control assembly: rootStopCoordinator's bound is the cleanup
20// enrollment's fresh read of the signed roster. A future numeral
21// (math.MaxUint64, or the roster's revision + 1) is refused for an
22// operation stop and for the no-process witness, with no fence, no drain
23// and nothing published; the roster's own revision passes, and a retry
24// after roster writes keeps the original numeral (B2). While quarantined
25// the fresh read still bounds historical cleanup.
26func TestStopBoundsRosterNumeralByRootsRosterRead(t *testing.T) {
27 ctx := t.Context()
28 w := newAssemblyWorld(t)
29 w.mustStart()
30 b0 := w.fresh()
31 recA, routeA := w.newHost("a")
32 recB, routeB := w.newHost("b")
33 for _, rec := range []struct {
34 host string
35 agent func() error
36 }{{recA.Host, func() error { return w.agent(b0).install(recA, routeA) }}, {recB.Host, func() error { return w.agent(b0).install(recB, routeB) }}} {
37 if err := rec.agent(); err != nil {
38 t.Fatal(rec.host, err)
39 }
40 }
41 drains := 0
42 inner := w.root.Stop.Coordinator.Drainer
43 w.root.Stop.Coordinator.Drainer = stopDrainFunc(func(ctx context.Context, req StopRequest, fence string) (DrainAcknowledgement, error) {
44 drains++
45 return inner.DrainClaim(ctx, req, fence)
46 })
47 published := func(host string) int {
48 t.Helper()
49 keys, err := w.store.List(ctx, "control/hosts/"+host+"/")
50 if err != nil {
51 t.Fatal(err)
52 }
53 n := 0
54 for _, k := range keys {
55 if strings.HasPrefix(k, "control/hosts/"+host+"/evidence/") || strings.HasPrefix(k, "control/hosts/"+host+"/serving-evidence/") {
56 n++
57 }
58 }
59 return n
60 }
61 current := w.roster.read().Roster.Revision
62
63 // Operation stop of A's writer session.
64 o := w.operationSession(recA.Host)
65 captured := hostguard.Captured{Incarnation: o.Claim.Holder.Incarnation, Stamp: o.Claim.Holder.Stamp}
66 for _, n := range []uint64{math.MaxUint64, current + 1} {
67 req := w.priorStop(o, b0.Binding)
68 req.RosterRevision = n
69 if _, err := w.root.Stop.Coordinator.FenceAndStop(ctx, req); !errors.Is(err, ErrClaimStopIdentity) {
70 t.Fatalf("operation stop with numeral %d: %v", n, err)
71 }
72 if v := w.view(recA.Host, captured); v.Pending != nil || len(v.Receipts) != 0 || drains != 0 || published(recA.Host) != 0 {
73 t.Fatalf("numeral %d left effects: pending %v receipts %d drains %d published %d", n, v.Pending, len(v.Receipts), drains, published(recA.Host))
74 }
75 }
76 req := w.priorStop(o, b0.Binding)
77 req.RosterRevision = current
78 stopped, err := w.root.Stop.Coordinator.FenceAndStop(ctx, req)
79 if err != nil {
80 t.Fatal("operation stop with the roster's revision:", err)
81 }
82 if r := decodeStopReceipt(t, stopped.Bytes); r.DirectoryRevision != strconv.FormatUint(current, 10) {
83 t.Fatalf("receipt numeral %q, want %d", r.DirectoryRevision, current)
84 }
85 // Roster writes, then a retry with the newer numeral: the original
86 // numeral and receipt (B2).
87 w.roster.holdChurn()
88 newer := w.roster.read().Roster.Revision
89 if newer <= current {
90 t.Fatalf("hold churn did not move the roster: %d -> %d", current, newer)
91 }
92 req.RosterRevision = newer
93 again, err := w.root.Stop.Coordinator.FenceAndStop(ctx, req)
94 if err != nil || string(again.Bytes) != string(stopped.Bytes) {
95 t.Fatalf("retry after roster writes: %v (same receipt %v)", err, string(again.Bytes) == string(stopped.Bytes))
96 }
97
98 // No-process witness for B (session zero).
99 before := published(recB.Host)
100 for _, n := range []uint64{math.MaxUint64, newer + 1} {
101 if _, err := w.noProcessStop(recB.Host, b0.Binding, n); !errors.Is(err, ErrClaimStopIdentity) {
102 t.Fatalf("no-process witness with numeral %d: %v", n, err)
103 }
104 if published(recB.Host) != before {
105 t.Fatalf("numeral %d published a no-process receipt", n)
106 }
107 }
108
109 // Quarantined: the fresh read still bounds historical cleanup.
110 w.roster.quarantine("loss_1")
111 quarantined := w.roster.read().Roster.Revision
112 if _, err := w.noProcessStop(recB.Host, b0.Binding, quarantined+1); !errors.Is(err, ErrClaimStopIdentity) {
113 t.Fatal("quarantined no-process witness with a future numeral:", err)
114 }
115 witnessed, err := w.noProcessStop(recB.Host, b0.Binding, quarantined)
116 if err != nil {
117 t.Fatal("quarantined no-process witness with the roster's revision:", err)
118 }
119 if r := decodeStopReceipt(t, witnessed.Bytes); r.DirectoryRevision != strconv.FormatUint(quarantined, 10) {
120 t.Fatalf("quarantined receipt numeral %q, want %d", r.DirectoryRevision, quarantined)
121 }
122 cleanup := w.priorStop(o, b0.Binding)
123 cleanup.Cleanup, cleanup.RosterRevision = true, quarantined
124 if again, err := w.root.Stop.Coordinator.FenceAndStop(ctx, cleanup); err != nil || string(again.Bytes) != string(stopped.Bytes) {
125 t.Fatalf("quarantined cleanup stop: %v", err)
126 }
127 cleanup.RosterRevision = math.MaxUint64
128 if _, err := w.root.Stop.Coordinator.FenceAndStop(ctx, cleanup); !errors.Is(err, ErrClaimStopIdentity) {
129 t.Fatal("quarantined cleanup stop with a future numeral:", err)
130 }
131}
132