foley.sources.resilience#
Graceful degradation for HTTP source adapters — throttle · backoff · circuit-break (#12).
resilient() wraps the injectable Transport DI seam
with three composed policies so a flaky or rate-limited remote source degrades
predictably instead of hammering the API or crashing a run:
throttle — a token bucket honouring the source config’s declared
rate(per_min/per_day) so foley never exceeds the published limits;backoff — exponential retry on
429/5xx(and transport exceptions), honouring aRetry-Afterheader when present;circuit-break — after N consecutive failures the breaker opens and fast-fails with
SourceUnavailablefor a cool-down window (no thundering herd).
All timing goes through injected clock / sleep callables (default
time.monotonic / time.sleep) so the whole thing is deterministically testable
with a fake clock and a no-op sleep — no real network, no real waiting. Stdlib-only.
Module Attributes
Seconds in a rolling rate-limit day (the |
Functions
|
Build a resilient transport from a source |
|
Wrap a |
Classes
|
Circuit-breaker thresholds. |
|
Exponential-backoff retry policy for transient HTTP failures. |
Exceptions
Raised when a source's circuit breaker is open or its retries are exhausted. |
- class foley.sources.resilience.BreakerPolicy(fail_threshold=5, reset_timeout_s=30.0)[source]#
Bases:
objectCircuit-breaker thresholds.
- class foley.sources.resilience.RetryPolicy(max_attempts=4, retry_on=(429, 500, 502, 503, 504), base_delay_s=0.5, max_delay_s=30.0, respect_retry_after=True)[source]#
Bases:
objectExponential-backoff retry policy for transient HTTP failures.
- foley.sources.resilience.SECONDS_PER_DAY: float = 86400.0#
Seconds in a rolling rate-limit day (the
per_daywindow length).
Bases:
RuntimeErrorRaised when a source’s circuit breaker is open or its retries are exhausted.
- foley.sources.resilience.make_resilient_transport_from_config(config, *, base=None, **inject)[source]#
Build a resilient transport from a source
config(itsratedrives the throttle).- Parameters:
config (
dict) – ASOURCE_CONFIGdict (readsconfig['rate']).base – The base transport (default:
foley.sources.http.requests_transport()).**inject –
retry/breaker/clock/sleepoverrides.
- Returns:
A resilient transport wrapping
base.
- foley.sources.resilience.resilient(transport, *, rate=None, retry=None, breaker=None, clock=<built-in function monotonic>, sleep=<built-in function sleep>, resend_after_transport_error=True)[source]#
Wrap a
Transportwith throttle + backoff + circuit-break.- Parameters:
transport – The base transport callable
(method, url, *, params, headers, json).rate (
Optional[dict]) – The source config’sratedict ({'per_min':…, 'per_day':…}).retry (
Optional[RetryPolicy]) – TheRetryPolicy(default: 4 attempts on 429/5xx).breaker (
Optional[BreakerPolicy]) – TheBreakerPolicy(default: open after 5 consecutive fails).clock (
Callable[[],float]) – Monotonic time source (injected for tests).sleep (
Callable[[float],None]) – Blocking sleep (injected for tests).resend_after_transport_error (
bool) – Retry a non-GET request after a transport-level failure (timeout, reset).Falsefor a paid source: the provider may have accepted — and billed — the request before the connection failed, so re-sending would be an unauthorized second paid call. Status-coded rejections (429 / 5xx) are still retried: those are answers, not charges.
- Returns:
A transport with the same signature, plus a
.reset()method. RaisesSourceUnavailablewhen the breaker is open or retries are exhausted.