carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package edge
2
3import (
4 "io"
5 "net/http"
6 "net/http/httptest"
7 "path/filepath"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/registry"
12)
13
14// The guard at the cache's own level: a tagged entry is live only while its
15// generation is its route's newest, and a stale one is refused at Put.
16func TestCacheGenerationGuard(t *testing.T) {
17 c := NewCache(DefaultCacheMaxEntries, DefaultCacheMaxBodyBytes)
18 cur := uint64(1)
19 c.SetGenerations(func(route string) (uint64, bool) { return cur, route == "inst.example.com" })
20 key := CacheKey{Host: "www.customer.example", Method: "GET", Path: "/"}
21 tagged := func(n uint64) *CachedResponse {
22 return &CachedResponse{StatusCode: 200, Body: []byte("x"), StoredAt: time.Now(), TTL: time.Minute, Route: "inst.example.com", Gen: n}
23 }
24 c.Put(key, "", tagged(1))
25 if _, ok := c.get(key); !ok {
26 t.Fatal("an entry tagged with the current generation missed")
27 }
28 cur = 2
29 if _, ok := c.get(key); ok {
30 t.Fatal("an entry from generation 1 hit after generation 2 became current")
31 }
32 other := CacheKey{Host: "inst.example.com", Method: "GET", Path: "/late"}
33 c.Put(other, "", tagged(1))
34 if len(c.byURL[other.urlKey()]) != 0 {
35 t.Fatal("Put stored a response from a generation that is no longer current")
36 }
37 c.Put(other, "", &CachedResponse{StatusCode: 200, Body: []byte("y"), StoredAt: time.Now(), TTL: time.Minute})
38 if _, ok := c.get(other); !ok {
39 t.Fatal("an untagged entry was refused; the guard must leave unmanaged routes alone")
40 }
41 unarmed := NewCache(DefaultCacheMaxEntries, DefaultCacheMaxBodyBytes)
42 unarmed.Put(key, "", tagged(2))
43 if _, ok := unarmed.get(key); ok {
44 t.Fatal("a tagged entry was served by a cache with no generation guard")
45 }
46}
47
48// genCacheFixture is genFixture's managed instance reached under four names
49// (its own host, a custom-domain alias, a wildcard alias, and a path-routed
50// front door), with the cache on and a second generation's app ready.
51func genCacheFixture(t *testing.T) (e *Edge, act *genActivator, a, b *genApp) {
52 t.Helper()
53 dir := genSockDir(t)
54 a = startGenApp(t, filepath.Join(dir, "a.sock"), body("A", "max-age=60"))
55 b = startGenApp(t, filepath.Join(dir, "b.sock"), body("B", "max-age=60"))
56 front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "front") }))
57 t.Cleanup(front.Close)
58 reg := openReg(t)
59 for _, rt := range []registry.Route{
60 {Host: "inst.example.com", Kind: "instance", Hibernate: true, Socket: filepath.Join(dir, "canon.sock"), Status: "ready", Version: "v0"},
61 {Host: "front.example", Kind: "service", Addr: front.Listener.Addr().String(), Status: "ready"},
62 } {
63 if err := reg.Upsert(rt); err != nil {
64 t.Fatal(err)
65 }
66 }
67 upsertAlias(t, reg, "www.customer.example", "inst.example.com")
68 upsertAlias(t, reg, "*.wild.example", "inst.example.com")
69 upsertPath(t, reg, "front.example", "acct", "inst.example.com")
70 act = &genActivator{fakeGens: newFakeGens(true)}
71 act.switchTo("inst.example.com", GenInfo{Socket: a.sock, N: 1, Build: "v1"})
72 var err error
73 e, err = New(reg, act)
74 if err != nil {
75 t.Fatalf("New: %v", err)
76 }
77 e.EnableCache(NewCache(DefaultCacheMaxEntries, DefaultCacheMaxBodyBytes))
78 return e, act, a, b
79}
80
81// Spec §12 "Cache aliases": a custom domain, a wildcard and a path-routed
82// request each miss after the switch, without anything listing the aliases.
83func TestCachedAliasesMissAfterTheSwitch(t *testing.T) {
84 e, act, _, b := genCacheFixture(t)
85 urls := []string{
86 "http://inst.example.com/p",
87 "http://www.customer.example/p",
88 "http://x.wild.example/p",
89 "http://front.example/acct/p",
90 }
91 for _, u := range urls {
92 if got := serve(t, e, u).Body.String(); got != "A" {
93 t.Fatalf("GET %s = %q, want A from generation 1", u, got)
94 }
95 }
96 before := act.served.Load()
97 for _, u := range urls {
98 if got := serve(t, e, u).Body.String(); got != "A" {
99 t.Fatalf("repeat GET %s = %q, want the cached A", u, got)
100 }
101 }
102 if act.served.Load() != before {
103 t.Fatal("repeat requests reached the activator; they should have been cache hits")
104 }
105 act.switchTo("inst.example.com", GenInfo{Socket: b.sock, N: 2, Build: "v2"})
106 for _, u := range urls {
107 rec := serve(t, e, u)
108 if rec.Body.String() != "B" || rec.Header().Get("X-Carlos-Version") != "v2" {
109 t.Fatalf("GET %s after the switch = %q (version %q), want B from v2", u, rec.Body.String(), rec.Header().Get("X-Carlos-Version"))
110 }
111 }
112}
113
114// A response that finishes on the old generation after the switch is
115// returned to its client and never stored (spec §7.2 insertion rule).
116func TestLateResponseFromARetiredGenerationIsNotCached(t *testing.T) {
117 dir := genSockDir(t)
118 entered, gate := make(chan struct{}), make(chan struct{})
119 a := startGenApp(t, filepath.Join(dir, "a.sock"), http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
120 close(entered)
121 <-gate
122 w.Header().Set("Cache-Control", "max-age=60")
123 io.WriteString(w, "A")
124 }))
125 b := startGenApp(t, filepath.Join(dir, "b.sock"), body("B", "max-age=60"))
126 reg := openReg(t)
127 if err := reg.Upsert(registry.Route{Host: "inst.example.com", Kind: "instance", Hibernate: true,
128 Socket: filepath.Join(dir, "canon.sock"), Status: "ready"}); err != nil {
129 t.Fatal(err)
130 }
131 act := &genActivator{fakeGens: newFakeGens(true)}
132 act.switchTo("inst.example.com", GenInfo{Socket: a.sock, N: 1, Build: "v1"})
133 e, err := New(reg, act)
134 if err != nil {
135 t.Fatal(err)
136 }
137 e.EnableCache(NewCache(DefaultCacheMaxEntries, DefaultCacheMaxBodyBytes))
138
139 done := make(chan string)
140 go func() { done <- serve(t, e, "http://inst.example.com/slow").Body.String() }()
141 <-entered
142 act.switchTo("inst.example.com", GenInfo{Socket: b.sock, N: 2, Build: "v2"})
143 close(gate)
144 if got := <-done; got != "A" {
145 t.Fatalf("in-flight request got %q, want its own A", got)
146 }
147 if n := len(e.cache.byURL[cacheURLKey{Host: "inst.example.com", Method: "GET", Path: "/slow"}]); n != 0 {
148 t.Fatalf("%d entries stored for a response that finished after the switch, want 0", n)
149 }
150 if got := serve(t, e, "http://inst.example.com/slow").Body.String(); got != "B" {
151 t.Fatalf("next request = %q, want B", got)
152 }
153}
154
155// Cache-before-wake survives generations: a sleeping instance's cached page
156// still answers without a wake, because the entry's generation is still the
157// route's newest.
158func TestSleepingInstanceStillAnswersFromTheCache(t *testing.T) {
159 e, act, a, _ := genCacheFixture(t)
160 if got := serve(t, e, "http://inst.example.com/p").Body.String(); got != "A" {
161 t.Fatalf("first GET = %q, want A", got)
162 }
163 act.sleep("inst.example.com")
164 a.srv.Close()
165 before := act.served.Load()
166 if got := serve(t, e, "http://inst.example.com/p").Body.String(); got != "A" {
167 t.Fatalf("GET while asleep = %q, want the cached A", got)
168 }
169 if act.served.Load() != before {
170 t.Fatal("a cached page woke the instance")
171 }
172}
173
174// Flag off: entries are untagged and nothing about generations touches them.
175func TestGenerationsOffCachesUntagged(t *testing.T) {
176 e, act, _ := genFixture(t, false, true)
177 e.EnableCache(NewCache(DefaultCacheMaxEntries, DefaultCacheMaxBodyBytes))
178 e.cache.Put(CacheKey{Host: "inst.example.com", Method: "GET", Path: "/p"}, "", &CachedResponse{
179 StatusCode: 200, Body: []byte("cached"), StoredAt: time.Now(), TTL: time.Minute})
180 act.switchTo("inst.example.com", GenInfo{Socket: "/nonexistent.sock", N: 9, Build: "v9"})
181 if got := serve(t, e, "http://inst.example.com/p").Body.String(); got != "cached" {
182 t.Fatalf("GET = %q, want the untagged cached body", got)
183 }
184 if e.cache.genOf != nil {
185 t.Fatal("the generation guard was armed with the switch off")
186 }
187}
188