carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package registry
2
3import (
4 "strings"
5 "testing"
6)
7
8// TestUpsertPreservesReplication is the fence that makes the new door worth
9// anything. The caller it stands against is `carlos add` re-run over a host
10// that already exists (cli.go's runAdd, which prints "updated ... (existing
11// route)"): it builds its Route literal from flags alone, and it has no
12// replication flag, so with a plain excluded.replication a re-add to restamp
13// a unit or repoint an addr would return an operator's `daily` route to the
14// every-second default — silently, and at roughly 100x the S3 traffic the
15// setting was chosen to avoid (discussion #10). Same shape, same reason, as
16// `environment`.
17//
18// NOT the provisioning reconciler, whatever the first draft of this comment
19// said: its only reg.Upsert is the mint branch (reconcile.go, `case !ok`,
20// where there is no existing row to clobber), and the owned branch converges
21// through narrow setters. The domains reconciler's two Upserts are mint-only
22// as well. The re-Upsert below is therefore a stand-in for `carlos add`'s
23// shape — a full-row write that knows nothing about this column — which is
24// the shape any future full-row caller will have too.
25func TestUpsertPreservesReplication(t *testing.T) {
26 r := openTestRegistry(t)
27 host := "seapointish.example.com"
28 if err := r.Upsert(Route{Host: host, Kind: "instance", App: "seapointish", Hibernate: true}); err != nil {
29 t.Fatalf("upsert: %v", err)
30 }
31 if err := r.SetReplication(host, ReplicationDaily); err != nil {
32 t.Fatalf("SetReplication: %v", err)
33 }
34
35 // A full-row Upsert built by a caller that knows nothing about this
36 // column — `carlos add`'s shape, and the reconciler's mint's shape.
37 if err := r.Upsert(Route{Host: host, Kind: "instance", App: "seapointish", Hibernate: true, Channel: "edge"}); err != nil {
38 t.Fatalf("re-upsert: %v", err)
39 }
40 rt, err := r.Get(host)
41 if err != nil || rt == nil {
42 t.Fatalf("get: %+v %v", rt, err)
43 }
44 if rt.Replication != ReplicationDaily {
45 t.Fatalf("replication = %q after a full-row re-Upsert, want %q — a re-add silently returned the route to the most expensive rung", rt.Replication, ReplicationDaily)
46 }
47 if rt.Channel != "edge" {
48 t.Fatalf("channel = %q, want the re-Upsert's own value: preserving replication must not freeze the columns a full-row caller DOES own", rt.Channel)
49 }
50}
51
52// TestUpsertHonoursAnExplicitReplication: preservation is for the zero value
53// only. A caller that carries a class still wins, exactly as with environment.
54// Both rows hibernate, because a rung on a row that does not is a state this
55// table refuses to hold — see TestReplicationImpliesHibernation.
56func TestUpsertHonoursAnExplicitReplication(t *testing.T) {
57 r := openTestRegistry(t)
58 host := "a.example.com"
59 if err := r.Upsert(Route{Host: host, Hibernate: true, Replication: ReplicationDaily}); err != nil {
60 t.Fatalf("upsert: %v", err)
61 }
62 if err := r.Upsert(Route{Host: host, Hibernate: true, Replication: ReplicationBatched}); err != nil {
63 t.Fatalf("re-upsert: %v", err)
64 }
65 rt, _ := r.Get(host)
66 if rt.Replication != ReplicationBatched {
67 t.Fatalf("replication = %q, want %q", rt.Replication, ReplicationBatched)
68 }
69}
70
71// TestReplicationImpliesHibernation is the invariant that stops the column
72// from lying after the fact. `carlos route --replication` refuses a route
73// that does not hibernate, because the rung is read on the activator's wake
74// path and nowhere else — but that rail is point-in-time, and hibernation is
75// flipped afterwards by three writers: `carlos route --hibernate=false`, the
76// provisioning converge reading an instance record, and a full-row Upsert
77// from `carlos add`. Without this invariant a `daily` route becomes a
78// non-hibernating route that `carlos routes` still reports as `daily`, while
79// box-wide /etc/litestream.yml replicates its database at its own interval:
80// the lying column the rail exists to prevent, relocated to a later moment.
81//
82// Both halves are pinned here because they are two different statements —
83// one in SetHibernate's UPDATE, one in Upsert's INSERT and ON CONFLICT — and
84// a fix to either alone leaves a door open.
85func TestReplicationImpliesHibernation(t *testing.T) {
86 r := openTestRegistry(t)
87
88 // The narrow setter: `carlos route --hibernate=false` and the converge
89 // pass both arrive here.
90 host := "sleeper.example.com"
91 if err := r.Upsert(Route{Host: host, Kind: "instance", App: "sleeper", Hibernate: true,
92 DBPath: "/data/sleeper.example.com.db"}); err != nil {
93 t.Fatalf("upsert: %v", err)
94 }
95 if err := r.SetReplication(host, ReplicationDaily); err != nil {
96 t.Fatalf("SetReplication: %v", err)
97 }
98 if err := r.SetHibernate(host, false); err != nil {
99 t.Fatalf("SetHibernate off: %v", err)
100 }
101 rt, err := r.Get(host)
102 if err != nil || rt == nil {
103 t.Fatalf("get: %+v %v", rt, err)
104 }
105 if rt.Replication != "" {
106 t.Fatalf("replication = %q on a route that stopped hibernating — box-wide litestream owns this database now, and the column claims otherwise", rt.Replication)
107 }
108 // Turning hibernation back ON does not resurrect the rung: a preserved
109 // class is invisible state that changes behaviour weeks later, and the
110 // operator is at a prompt where naming it again costs one command.
111 if err := r.SetHibernate(host, true); err != nil {
112 t.Fatalf("SetHibernate on: %v", err)
113 }
114 if rt, _ = r.Get(host); rt.Replication != "" {
115 t.Fatalf("re-hibernating restored %q — the rung is set, not remembered", rt.Replication)
116 }
117
118 // The full-row write: `carlos add` with no --hibernate over an existing
119 // tuned row. It turns hibernation off (hibernate is a plain overwrite),
120 // so the preserved-when-empty CASE must not hand the new row a rung.
121 if err := r.SetReplication(host, ReplicationBatched); err != nil {
122 t.Fatalf("SetReplication batched: %v", err)
123 }
124 if err := r.Upsert(Route{Host: host, Kind: "instance", App: "sleeper",
125 DBPath: "/data/sleeper.example.com.db"}); err != nil {
126 t.Fatalf("re-upsert awake: %v", err)
127 }
128 if rt, _ = r.Get(host); rt.Replication != "" {
129 t.Fatalf("replication = %q after an Upsert that turned hibernation off", rt.Replication)
130 }
131
132 // And on INSERT, where there is no ON CONFLICT clause to do it.
133 fresh := "awake.example.com"
134 if err := r.Upsert(Route{Host: fresh, Kind: "instance", App: "awake", Replication: ReplicationDaily}); err != nil {
135 t.Fatalf("insert: %v", err)
136 }
137 if rt, _ = r.Get(fresh); rt.Replication != "" {
138 t.Fatalf("a fresh non-hibernating row was inserted carrying %q", rt.Replication)
139 }
140}
141
142// TestSetReplicationValidates: the value reaches internal/activator's
143// replicationSyncInterval, which answers anything it does not recognise with
144// litestream's default — so an unvalidated write is a route that quietly
145// never leaves the most expensive rung. SetUnit validates for the same
146// reason; SetBacking does not need to, because its two legal values are
147// compared at every door.
148func TestSetReplicationValidates(t *testing.T) {
149 r := openTestRegistry(t)
150 host := "a.example.com"
151 if err := r.Upsert(Route{Host: host}); err != nil {
152 t.Fatalf("upsert: %v", err)
153 }
154 err := r.SetReplication(host, "dayly")
155 if err == nil {
156 t.Fatal("SetReplication accepted an unknown class")
157 }
158 for _, want := range ReplicationClasses() {
159 if !strings.Contains(err.Error(), want) {
160 t.Errorf("refusal %q does not list %q", err, want)
161 }
162 }
163 rt, _ := r.Get(host)
164 if rt.Replication != "" {
165 t.Fatalf("a refused write still landed: %q", rt.Replication)
166 }
167
168 // "" is legal: it is the column's default and the state a cleared route
169 // returns to.
170 if err := r.SetReplication(host, ReplicationBatched); err != nil {
171 t.Fatalf("SetReplication batched: %v", err)
172 }
173 if err := r.SetReplication(host, ReplicationDefault); err != nil {
174 t.Fatalf("SetReplication to the default: %v", err)
175 }
176 rt, _ = r.Get(host)
177 if rt.Replication != "" {
178 t.Fatalf("clear left %q", rt.Replication)
179 }
180}
181
182// TestSetReplicationUnknownHost: like SetChannel and SetUnit, this is a
183// setter a human types a hostname into, so an unknown host is an error rather
184// than a silent no-op.
185func TestSetReplicationUnknownHost(t *testing.T) {
186 r := openTestRegistry(t)
187 err := r.SetReplication("nope.example.com", ReplicationDaily)
188 if err == nil || !strings.Contains(err.Error(), "nope.example.com") {
189 t.Fatalf("SetReplication(unknown host) = %v, want an error naming the host", err)
190 }
191}
192
193// TestValidReplicationVocabulary pins the set itself. It lives here now
194// rather than unexported in internal/activator, because a setter and a reader
195// with two independent spellings of the same three strings is how they drift
196// into disagreeing about what `daily` means.
197func TestValidReplicationVocabulary(t *testing.T) {
198 for _, ok := range []string{"", "stream", "batched", "daily"} {
199 if !ValidReplication(ok) {
200 t.Errorf("ValidReplication(%q) = false", ok)
201 }
202 }
203 for _, bad := range []string{"dayly", "STREAM", "hourly", "none", "5m"} {
204 if ValidReplication(bad) {
205 t.Errorf("ValidReplication(%q) = true", bad)
206 }
207 }
208 // The default is deliberately absent from the offered list: it is what a
209 // route has before anyone chooses, and the way back to it is the CLI's
210 // clear sentinel, not a name to type.
211 for _, c := range ReplicationClasses() {
212 if c == ReplicationDefault {
213 t.Errorf("ReplicationClasses() offers the default as a rung to type")
214 }
215 }
216 if len(ReplicationClasses()) != 3 {
217 t.Errorf("ReplicationClasses() = %v, want the three rungs", ReplicationClasses())
218 }
219}
220