carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package rollout
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/carlosframework/platform/internal/appconfig"
9)
10
11func TestConfigBackoffRunsFromOneMinuteToThirty(t *testing.T) {
12 want := []time.Duration{time.Minute, 2 * time.Minute, 4 * time.Minute, 8 * time.Minute, 16 * time.Minute, 30 * time.Minute, 30 * time.Minute}
13 for i, w := range want {
14 if got := configBackoff(i + 1); got != w {
15 t.Errorf("configBackoff(%d) = %v, want %v", i+1, got, w)
16 }
17 }
18}
19
20func TestAnAckWaitsForItsRevisionToBeActive(t *testing.T) {
21 var wrote []string
22 write := func(_ context.Context, a appconfig.ConfigAck) { wrote = append(wrote, a.Host) }
23 tr := &configTrack{delivered: "c2", active: "c1"}
24 if now := tr.deferAck(appconfig.ConfigAck{Host: "a"}, write); now != nil {
25 t.Fatal("acknowledged a delivered revision that is not active")
26 }
27 if run, _ := tr.activate("c1"); run != nil {
28 t.Fatal("an older revision becoming active released the newer ack")
29 }
30 run, ok := tr.activate("c2")
31 if !ok {
32 t.Fatal("the ack for c2 did not come back when c2 became active")
33 }
34 run(context.Background())
35 if len(wrote) != 1 {
36 t.Fatalf("wrote %v", wrote)
37 }
38 // With nothing delivered through the scheduler (a shape it does not
39 // restart), the ack is written straight away, as today.
40 free := &configTrack{}
41 if now := free.deferAck(appconfig.ConfigAck{Host: "b"}, write); now == nil {
42 t.Fatal("an untracked route's ack must not wait")
43 }
44}
45
46// Spec §8.1: a sleeping instance needs no attempt, because its next wake
47// starts with the delivered file. Its ack goes out at delivery, as today,
48// marked Hibernating like unit config sync's ack for a sleeping route.
49func TestASleepersAckGoesOutAtDeliveryAsHibernating(t *testing.T) {
50 var got []appconfig.ConfigAck
51 tr := &configTrack{delivered: "c2", active: "c2", sleeping: true}
52 run := tr.deferAck(appconfig.ConfigAck{Host: "a", Restarted: true}, func(_ context.Context, a appconfig.ConfigAck) { got = append(got, a) })
53 if run == nil {
54 t.Fatal("a sleeper's ack waited for a wake")
55 }
56 run(context.Background())
57 if len(got) != 1 || !got[0].Hibernating || got[0].Restarted {
58 t.Fatalf("ack = %+v, want Hibernating and not Restarted", got)
59 }
60}
61
62// Spec §8.1: a revision is acknowledged only once active. A held ack that a
63// newer delivery bypassed must never be released afterwards, or it would be
64// written after the newer ack with a revision that is no longer delivered.
65func TestANewerAckSupersedesTheHeldOne(t *testing.T) {
66 t.Run("revert to the active revision", func(t *testing.T) {
67 var wrote []string
68 write := func(_ context.Context, a appconfig.ConfigAck) { wrote = append(wrote, a.Host) }
69 tr := &configTrack{delivered: "c2", active: "c1"}
70 if now := tr.deferAck(appconfig.ConfigAck{Host: "held-c2"}, write); now != nil {
71 t.Fatal("c2's ack was not held while c1 is active")
72 }
73 // The config reverts to c1, which is the active revision.
74 tr.delivered = "c1"
75 now := tr.deferAck(appconfig.ConfigAck{Host: "c1"}, write)
76 if now == nil {
77 t.Fatal("c1's ack waited though c1 is active")
78 }
79 now(context.Background())
80 // The in-flight generation finishes on c2.
81 if run, ok := tr.activate("c2"); run != nil || ok {
82 t.Fatal("the stale c2 ack was released after a newer ack went out")
83 }
84 if len(wrote) != 1 || wrote[0] != "c1" {
85 t.Fatalf("wrote %v, want only the c1 ack", wrote)
86 }
87 })
88 t.Run("sleep then hibernating ack", func(t *testing.T) {
89 var wrote []string
90 write := func(_ context.Context, a appconfig.ConfigAck) { wrote = append(wrote, a.Host) }
91 tr := &configTrack{delivered: "c2", active: "c1"}
92 if now := tr.deferAck(appconfig.ConfigAck{Host: "held-c2"}, write); now != nil {
93 t.Fatal("c2's ack was not held while c1 is active")
94 }
95 // The instance sleeps and c3 is delivered to it.
96 tr.delivered, tr.sleeping = "c3", true
97 now := tr.deferAck(appconfig.ConfigAck{Host: "c3"}, write)
98 if now == nil {
99 t.Fatal("a sleeper's ack waited")
100 }
101 now(context.Background())
102 if run, ok := tr.activate("c2"); run != nil || ok {
103 t.Fatal("the stale c2 ack was released after the Hibernating ack went out")
104 }
105 if len(wrote) != 1 || wrote[0] != "c3" {
106 t.Fatalf("wrote %v, want only the c3 ack", wrote)
107 }
108 })
109}
110
111// A held ack is released only for the revision that is delivered now, even
112// when delivered moved on without another ack arriving.
113func TestAHeldAckIsNotReleasedForARevisionNoLongerDelivered(t *testing.T) {
114 tr := &configTrack{delivered: "c2", active: "c1"}
115 tr.deferAck(appconfig.ConfigAck{Host: "a"}, func(context.Context, appconfig.ConfigAck) { t.Fatal("wrote a stale ack") })
116 tr.delivered = "c3"
117 if run, ok := tr.activate("c2"); run != nil || ok {
118 t.Fatal("released an ack for c2 while c3 is delivered")
119 }
120}
121