@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 433 lines 13.1 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package reverseproxy
16
17 import (
18 "context"
19 "testing"
20 "time"
21
22 "github.com/caddyserver/caddy/v2"
23 )
24
25 type recordCB struct {
26 status int
27 lat time.Duration
28 n int
29 }
30
31 func (c *recordCB) OK() bool { return true }
32
33 func (c *recordCB) RecordMetric(statusCode int, latency time.Duration) {
34 c.status = statusCode
35 c.lat = latency
36 c.n++
37 }
38
39 func TestRecordUpstreamRoundTrip(t *testing.T) {
40 h, cancel := newPassiveHandler(t, 1, time.Minute)
41 defer cancel()
42 h.HealthChecks.Passive.UnhealthyLatency = caddy.Duration(time.Millisecond)
43 u, cleanup := provisionedStaticUpstream(t, h, "127.0.0.1:9")
44 defer cleanup()
45 cb := &recordCB{}
46 u.cb = cb
47
48 repl := caddy.NewReplacer()
49 lat := 5 * time.Millisecond
50 h.recordUpstreamRoundTrip(DialInfo{Upstream: u}, repl, 200, lat)
51
52 if cb.n != 1 || cb.status != 200 {
53 t.Errorf("circuit breaker RecordMetric calls=%d status=%d, want 1 / 200", cb.n, cb.status)
54 }
55 if cb.lat != lat {
56 t.Errorf("circuit breaker latency = %v, want %v", cb.lat, lat)
57 }
58 if got := u.Host.Fails(); got != 1 {
59 t.Errorf("passive fails = %d, want 1 (unhealthy latency)", got)
60 }
61 got, _ := repl.Get("http.reverse_proxy.upstream.latency")
62 if got != lat {
63 t.Errorf("latency placeholder = %v, want %v", got, lat)
64 }
65 }
66
67 // newPassiveHandler builds a minimal Handler with passive health checks
68 // configured and a live caddy.Context so the fail-forgetter goroutine can
69 // be cancelled cleanly. The caller must call cancel() when done.
70 func newPassiveHandler(t *testing.T, maxFails int, failDuration time.Duration) (*Handler, context.CancelFunc) {
71 t.Helper()
72 caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
73 h := &Handler{
74 ctx: caddyCtx,
75 HealthChecks: &HealthChecks{
76 Passive: &PassiveHealthChecks{
77 MaxFails: maxFails,
78 FailDuration: caddy.Duration(failDuration),
79 },
80 },
81 }
82 return h, cancel
83 }
84
85 // provisionedStaticUpstream creates a static upstream, registers it in the
86 // UsagePool, and returns a cleanup func that removes it from the pool.
87 func provisionedStaticUpstream(t *testing.T, h *Handler, addr string) (*Upstream, func()) {
88 t.Helper()
89 u := &Upstream{Dial: addr}
90 h.provisionUpstream(u, false)
91 return u, func() { _, _ = hosts.Delete(addr) }
92 }
93
94 // provisionedDynamicUpstream creates a dynamic upstream, registers it in
95 // dynamicHosts, and returns a cleanup func that removes it.
96 func provisionedDynamicUpstream(t *testing.T, h *Handler, addr string) (*Upstream, func()) {
97 t.Helper()
98 u := &Upstream{Dial: addr}
99 h.provisionUpstream(u, true)
100 return u, func() {
101 dynamicHostsMu.Lock()
102 delete(dynamicHosts, addr)
103 dynamicHostsMu.Unlock()
104 }
105 }
106
107 // --- countFailure behaviour ---
108
109 // TestCountFailureNoopWhenNoHealthChecks verifies that countFailure is a no-op
110 // when HealthChecks is nil.
111 func TestCountFailureNoopWhenNoHealthChecks(t *testing.T) {
112 resetDynamicHosts()
113 h := &Handler{}
114 u := &Upstream{Dial: "10.1.0.1:80", Host: new(Host)}
115
116 h.countFailure(u)
117
118 if u.Host.Fails() != 0 {
119 t.Errorf("expected 0 fails with no HealthChecks config, got %d", u.Host.Fails())
120 }
121 }
122
123 // TestCountFailureNoopWhenZeroDuration verifies that countFailure is a no-op
124 // when FailDuration is 0 (the zero value disables passive checks).
125 func TestCountFailureNoopWhenZeroDuration(t *testing.T) {
126 resetDynamicHosts()
127 caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
128 defer cancel()
129 h := &Handler{
130 ctx: caddyCtx,
131 HealthChecks: &HealthChecks{
132 Passive: &PassiveHealthChecks{MaxFails: 1, FailDuration: 0},
133 },
134 }
135 u := &Upstream{Dial: "10.1.0.2:80", Host: new(Host)}
136
137 h.countFailure(u)
138
139 if u.Host.Fails() != 0 {
140 t.Errorf("expected 0 fails with zero FailDuration, got %d", u.Host.Fails())
141 }
142 }
143
144 // TestCountFailureIncrementsCount verifies that countFailure increments the
145 // fail count on the upstream's Host.
146 func TestCountFailureIncrementsCount(t *testing.T) {
147 resetDynamicHosts()
148 h, cancel := newPassiveHandler(t, 2, time.Minute)
149 defer cancel()
150 u := &Upstream{Dial: "10.1.0.3:80", Host: new(Host)}
151
152 h.countFailure(u)
153
154 if u.Host.Fails() != 1 {
155 t.Errorf("expected 1 fail after countFailure, got %d", u.Host.Fails())
156 }
157 }
158
159 // TestCountFailureDecrementsAfterDuration verifies that the fail count is
160 // decremented back after FailDuration elapses.
161 func TestCountFailureDecrementsAfterDuration(t *testing.T) {
162 resetDynamicHosts()
163 const failDuration = 50 * time.Millisecond
164 h, cancel := newPassiveHandler(t, 2, failDuration)
165 defer cancel()
166 u := &Upstream{Dial: "10.1.0.4:80", Host: new(Host)}
167
168 h.countFailure(u)
169 if u.Host.Fails() != 1 {
170 t.Fatalf("expected 1 fail immediately after countFailure, got %d", u.Host.Fails())
171 }
172
173 // Wait long enough for the forgetter goroutine to fire.
174 time.Sleep(3 * failDuration)
175
176 if u.Host.Fails() != 0 {
177 t.Errorf("expected fail count to return to 0 after FailDuration, got %d", u.Host.Fails())
178 }
179 }
180
181 // TestCountFailureCancelledContextForgets verifies that cancelling the handler
182 // context (simulating a config unload) also triggers the forgetter to run,
183 // decrementing the fail count.
184 func TestCountFailureCancelledContextForgets(t *testing.T) {
185 resetDynamicHosts()
186 h, cancel := newPassiveHandler(t, 2, time.Hour) // very long duration
187 u := &Upstream{Dial: "10.1.0.5:80", Host: new(Host)}
188
189 h.countFailure(u)
190 if u.Host.Fails() != 1 {
191 t.Fatalf("expected 1 fail immediately after countFailure, got %d", u.Host.Fails())
192 }
193
194 // Cancelling the context should cause the forgetter goroutine to exit and
195 // decrement the count.
196 cancel()
197 time.Sleep(50 * time.Millisecond)
198
199 if u.Host.Fails() != 0 {
200 t.Errorf("expected fail count to be decremented after context cancel, got %d", u.Host.Fails())
201 }
202 }
203
204 // --- static upstream passive health check ---
205
206 // TestStaticUpstreamHealthyWithNoFailures verifies that a static upstream with
207 // no recorded failures is considered healthy.
208 func TestStaticUpstreamHealthyWithNoFailures(t *testing.T) {
209 resetDynamicHosts()
210 h, cancel := newPassiveHandler(t, 2, time.Minute)
211 defer cancel()
212
213 u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.1:80")
214 defer cleanup()
215
216 if !u.Healthy() {
217 t.Error("upstream with no failures should be healthy")
218 }
219 }
220
221 // TestStaticUpstreamUnhealthyAtMaxFails verifies that a static upstream is
222 // marked unhealthy once its fail count reaches MaxFails.
223 func TestStaticUpstreamUnhealthyAtMaxFails(t *testing.T) {
224 resetDynamicHosts()
225 h, cancel := newPassiveHandler(t, 2, time.Minute)
226 defer cancel()
227
228 u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.2:80")
229 defer cleanup()
230
231 h.countFailure(u)
232 if !u.Healthy() {
233 t.Error("upstream should still be healthy after 1 of 2 allowed failures")
234 }
235
236 h.countFailure(u)
237 if u.Healthy() {
238 t.Error("upstream should be unhealthy after reaching MaxFails=2")
239 }
240 }
241
242 // TestStaticUpstreamRecoversAfterFailDuration verifies that a static upstream
243 // returns to healthy once its failures expire.
244 func TestStaticUpstreamRecoversAfterFailDuration(t *testing.T) {
245 resetDynamicHosts()
246 const failDuration = 50 * time.Millisecond
247 h, cancel := newPassiveHandler(t, 1, failDuration)
248 defer cancel()
249
250 u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.3:80")
251 defer cleanup()
252
253 h.countFailure(u)
254 if u.Healthy() {
255 t.Fatal("upstream should be unhealthy immediately after MaxFails failure")
256 }
257
258 time.Sleep(3 * failDuration)
259
260 if !u.Healthy() {
261 t.Errorf("upstream should recover to healthy after FailDuration, Fails=%d", u.Host.Fails())
262 }
263 }
264
265 // TestStaticUpstreamHealthPersistedAcrossReprovisioning verifies that static
266 // upstreams share a Host via the UsagePool, so a second call to provisionUpstream
267 // for the same address (as happens on config reload) sees the accumulated state.
268 func TestStaticUpstreamHealthPersistedAcrossReprovisioning(t *testing.T) {
269 resetDynamicHosts()
270 h, cancel := newPassiveHandler(t, 2, time.Minute)
271 defer cancel()
272
273 u1, cleanup1 := provisionedStaticUpstream(t, h, "10.2.0.4:80")
274 defer cleanup1()
275
276 h.countFailure(u1)
277 h.countFailure(u1)
278
279 // Simulate a second handler instance referencing the same upstream
280 // (e.g. after a config reload that keeps the same backend address).
281 u2, cleanup2 := provisionedStaticUpstream(t, h, "10.2.0.4:80")
282 defer cleanup2()
283
284 if u1.Host != u2.Host {
285 t.Fatal("expected both Upstream structs to share the same *Host via UsagePool")
286 }
287 if u2.Healthy() {
288 t.Error("re-provisioned upstream should still see the prior fail count and be unhealthy")
289 }
290 }
291
292 // --- dynamic upstream passive health check ---
293
294 // TestDynamicUpstreamHealthyWithNoFailures verifies that a freshly provisioned
295 // dynamic upstream is healthy.
296 func TestDynamicUpstreamHealthyWithNoFailures(t *testing.T) {
297 resetDynamicHosts()
298 h, cancel := newPassiveHandler(t, 2, time.Minute)
299 defer cancel()
300
301 u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.1:80")
302 defer cleanup()
303
304 if !u.Healthy() {
305 t.Error("dynamic upstream with no failures should be healthy")
306 }
307 }
308
309 // TestDynamicUpstreamUnhealthyAtMaxFails verifies that a dynamic upstream is
310 // marked unhealthy once its fail count reaches MaxFails.
311 func TestDynamicUpstreamUnhealthyAtMaxFails(t *testing.T) {
312 resetDynamicHosts()
313 h, cancel := newPassiveHandler(t, 2, time.Minute)
314 defer cancel()
315
316 u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.2:80")
317 defer cleanup()
318
319 h.countFailure(u)
320 if !u.Healthy() {
321 t.Error("dynamic upstream should still be healthy after 1 of 2 allowed failures")
322 }
323
324 h.countFailure(u)
325 if u.Healthy() {
326 t.Error("dynamic upstream should be unhealthy after reaching MaxFails=2")
327 }
328 }
329
330 // TestDynamicUpstreamFailCountPersistedBetweenRequests is the core regression
331 // test: it simulates two sequential (non-concurrent) requests to the same
332 // dynamic upstream. Before the fix, the UsagePool entry would be deleted
333 // between requests, wiping the fail count. Now it should survive.
334 func TestDynamicUpstreamFailCountPersistedBetweenRequests(t *testing.T) {
335 resetDynamicHosts()
336 h, cancel := newPassiveHandler(t, 2, time.Minute)
337 defer cancel()
338
339 // --- first request ---
340 u1 := &Upstream{Dial: "10.3.0.3:80"}
341 h.provisionUpstream(u1, true)
342 h.countFailure(u1)
343
344 if u1.Host.Fails() != 1 {
345 t.Fatalf("expected 1 fail after first request, got %d", u1.Host.Fails())
346 }
347
348 // Simulate end of first request: no delete from any pool (key difference
349 // vs. the old behaviour where hosts.Delete was deferred).
350
351 // --- second request: brand-new *Upstream struct, same dial address ---
352 u2 := &Upstream{Dial: "10.3.0.3:80"}
353 h.provisionUpstream(u2, true)
354
355 if u1.Host != u2.Host {
356 t.Fatal("expected both requests to share the same *Host pointer from dynamicHosts")
357 }
358 if u2.Host.Fails() != 1 {
359 t.Errorf("expected fail count to persist across requests, got %d", u2.Host.Fails())
360 }
361
362 // A second failure now tips it over MaxFails=2.
363 h.countFailure(u2)
364 if u2.Healthy() {
365 t.Error("upstream should be unhealthy after accumulated failures across requests")
366 }
367
368 // Cleanup.
369 dynamicHostsMu.Lock()
370 delete(dynamicHosts, "10.3.0.3:80")
371 dynamicHostsMu.Unlock()
372 }
373
374 // TestDynamicUpstreamRecoveryAfterFailDuration verifies that a dynamic
375 // upstream's fail count expires and it returns to healthy.
376 func TestDynamicUpstreamRecoveryAfterFailDuration(t *testing.T) {
377 resetDynamicHosts()
378 const failDuration = 50 * time.Millisecond
379 h, cancel := newPassiveHandler(t, 1, failDuration)
380 defer cancel()
381
382 u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.4:80")
383 defer cleanup()
384
385 h.countFailure(u)
386 if u.Healthy() {
387 t.Fatal("upstream should be unhealthy immediately after MaxFails failure")
388 }
389
390 time.Sleep(3 * failDuration)
391
392 // Re-provision (as a new request would) to get fresh *Upstream with policy set.
393 u2 := &Upstream{Dial: "10.3.0.4:80"}
394 h.provisionUpstream(u2, true)
395
396 if !u2.Healthy() {
397 t.Errorf("dynamic upstream should recover to healthy after FailDuration, Fails=%d", u2.Host.Fails())
398 }
399 }
400
401 // TestDynamicUpstreamMaxRequestsFromUnhealthyRequestCount verifies that
402 // UnhealthyRequestCount is copied into MaxRequests so Full() works correctly.
403 func TestDynamicUpstreamMaxRequestsFromUnhealthyRequestCount(t *testing.T) {
404 resetDynamicHosts()
405 caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
406 defer cancel()
407 h := &Handler{
408 ctx: caddyCtx,
409 HealthChecks: &HealthChecks{
410 Passive: &PassiveHealthChecks{
411 UnhealthyRequestCount: 3,
412 },
413 },
414 }
415
416 u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.5:80")
417 defer cleanup()
418
419 if u.MaxRequests != 3 {
420 t.Errorf("expected MaxRequests=3 from UnhealthyRequestCount, got %d", u.MaxRequests)
421 }
422
423 // Should not be full with fewer requests than the limit.
424 _ = u.Host.countRequest(2)
425 if u.Full() {
426 t.Error("upstream should not be full with 2 of 3 allowed requests")
427 }
428
429 _ = u.Host.countRequest(1)
430 if !u.Full() {
431 t.Error("upstream should be full at UnhealthyRequestCount concurrent requests")
432 }
433 }