Repository navigation
Expand file tree
/
Copy pathreliability.mojo
More file actions
167 lines (140 loc) · 6.21 KB
/
Copy pathreliability.mojo
File metadata and controls
167 lines (140 loc) · 6.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""Reliability middleware — Retry / PostHocDeadline / RateLimit /
CircuitBreaker.
Shows how to wrap a handler with production-grade reliability
primitives:
- ``Retry[Inner]`` re-invokes the inner handler up to
``RetryPolicy.max_attempts`` times when it returns a 5xx
response. Idempotent methods (GET / HEAD / OPTIONS / TRACE /
PUT / DELETE — RFC 9110 §9.2.2) retry by default; non-idempotent
methods (POST / PATCH / CONNECT) pass through once unless
``RetryPolicy.retry_only_idempotent`` is set to ``False``.
Optional binary exponential backoff with full jitter
(``initial_backoff_ms`` > 0) spaces attempts.
- ``PostHocDeadline[Inner]`` runs the inner handler **to
completion**, measures the elapsed wall-clock time after it
returns, and replaces the response with a sanitised 504
Gateway Timeout when the budget was exceeded. The middleware
does **not** preempt the inner handler -- a runaway handler
ties up the worker for its full natural duration and the
504 only suppresses the response. The reactor-cooperative
cancel-cell flip lands in a later commit. Available at the
package root as ``PostHocDeadline``; the ``Timeout`` symbol
remains bound to ``flare.net.error.Timeout`` (the I/O timeout
error type).
- ``RateLimit[Inner]`` is a token-bucket admission gate: it admits up
to ``rate_per_sec`` requests/second with a ``burst`` depth, and
short-circuits with ``429 Too Many Requests`` once the bucket is
empty (the inner handler is never invoked on a rejected request).
- ``CircuitBreaker[Inner]`` opens after ``failure_threshold``
consecutive failures (a 5xx response or a raised exception),
fast-failing with ``503`` for ``cooldown_ms`` before letting a
single probe through (half-open); a success closes it again.
Pure construction — no live network. Run:
pixi run example-reliability
"""
from flare.http import Handler, Request, Response
from flare.http.reliability import (
CircuitBreaker,
PostHocDeadline,
RateLimit,
Retry,
RetryPolicy,
)
@fieldwise_init
struct OkHandler(Copyable, Defaultable, Handler, Movable):
"""Always returns 200 OK — fast-path for both middlewares."""
var _placeholder: UInt8
def __init__(out self):
self._placeholder = UInt8(0)
def serve(self, req: Request) raises -> Response:
var resp = Response(status=200)
resp.body = List[UInt8](String("hello").as_bytes())
resp.headers.set("Content-Length", String(len(resp.body)))
return resp^
@fieldwise_init
struct FlakyHandler(Copyable, Defaultable, Handler, Movable):
"""Always returns 503 — used to demonstrate retry exhaustion."""
var _placeholder: UInt8
def __init__(out self):
self._placeholder = UInt8(0)
def serve(self, req: Request) raises -> Response:
return Response(status=503, reason=String("Service Unavailable"))
def main() raises:
print("=== flare Example: Reliability middleware ===")
print()
# 1. Fast path through Retry: the inner returns 200 on the
# first attempt, so Retry never re-invokes it.
var fast = Retry(
OkHandler(),
RetryPolicy(
max_attempts=3,
retry_only_idempotent=True,
initial_backoff_ms=0,
backoff_multiplier=2,
max_backoff_ms=2_000,
),
)
var req = Request(method=String("GET"), url=String("/"))
var resp = fast.serve(req)
print("Retry / fast path status:", resp.status)
# 2. Retry exhausts max_attempts on a perpetually-flaky inner
# and surfaces the last 5xx. POSTs would *not* retry under
# the default policy; we flip retry_only_idempotent off to
# force a retry here just for illustration.
var flaky = Retry(
FlakyHandler(),
RetryPolicy(
max_attempts=2,
retry_only_idempotent=False,
initial_backoff_ms=0,
backoff_multiplier=2,
max_backoff_ms=2_000,
),
)
var resp2 = flaky.serve(req)
print("Retry / exhausted status:", resp2.status)
# 3. Retry with binary exponential backoff + full jitter:
# 5 ms initial, 2x multiplier, 20 ms cap. The actual sleep
# is drawn from [0, capped] before each retry. Three GETs
# against a flaky inner will sleep at most 5+10 = 15 ms total
# before surfacing the 503.
var jittered = Retry(
FlakyHandler(),
RetryPolicy(
max_attempts=3,
retry_only_idempotent=True,
initial_backoff_ms=5,
backoff_multiplier=2,
max_backoff_ms=20,
),
)
var resp_jit = jittered.serve(req)
print("Retry / jittered status:", resp_jit.status)
# 4. PostHocDeadline disabled-by-zero-budget sentinel: a budget
# of 0 ms is the explicit "no time allowed" knob — every call
# surfaces as a 504 without invoking the inner handler.
var bounded = PostHocDeadline(OkHandler(), budget_ms=0)
var resp3 = bounded.serve(req)
print("PostHocDeadline / 0ms status:", resp3.status)
# 5. PostHocDeadline with a generous budget: the inner runs and
# the 200 passes through unchanged.
var bounded_ok = PostHocDeadline(OkHandler(), budget_ms=30_000)
var resp4 = bounded_ok.serve(req)
print("PostHocDeadline / 30s status:", resp4.status)
# 6. RateLimit token bucket: rate 1/s, burst 2. Two rapid requests
# drain the bucket (200, 200); the third is rejected with 429
# without ever reaching the inner handler.
var limited = RateLimit(OkHandler(), rate_per_sec=1, burst=2)
print("RateLimit / req 1 status:", limited.serve(req).status)
print("RateLimit / req 2 status:", limited.serve(req).status)
print("RateLimit / req 3 status:", limited.serve(req).status)
# 7. CircuitBreaker: opens after 2 consecutive 5xx. The first two
# calls hit the flaky inner (503, 503) and trip the breaker; the
# third fast-fails with 503 during the cooldown without invoking
# the inner.
var breaker = CircuitBreaker(
FlakyHandler(), failure_threshold=2, cooldown_ms=60_000
)
print("CircuitBreaker / call 1 status:", breaker.serve(req).status)
print("CircuitBreaker / call 2 status:", breaker.serve(req).status)
print("CircuitBreaker / open status:", breaker.serve(req).status)