Skip to content

Commit f96e832

Browse files
authored
docs(adr): compress to 10 records in the domain-modeling format (#128)
* docs(adr): compress to 10 records in the domain-modeling format Drop 0001, 0010, 0011, 0017; merge 0009 into 0003, 0014 into 0006, 0008 into 0007; record the #118 manual-control decline in 0005. Each record is now title plus one paragraph, without the Decision / rationale / Revisit-trigger sections. Numbers stay stable, with gaps. * docs(adr): renumber the surviving records 0001-0010
1 parent e4ed89b commit f96e832

30 files changed

Lines changed: 100 additions & 456 deletions

‎AGENTS.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ Every module under `src/httpware/` is named for what it does; read it. What a si
4545
`DecodeError`. A decoder's `can_decode` runs outside that wrap, so it must never raise.
4646
- `_internal/` is the cross-module private home; `_internal/observability.py:_emit_event` is the
4747
single fan-out to both a logging record and an OTel span event, and the event names it takes are
48-
a public contract (see `CONTEXT.md`).
48+
a public contract (see `CONTEXT.md`). Logic shared only by a sync/async sibling pair stays a
49+
free function in the module that owns the behaviour; `_internal/` is for sharing across
50+
otherwise unrelated modules.
4951
- `middleware/resilience/` keeps the shared logic objects (`_RetryPolicy`, `_CircuitBreakerState`,
5052
`RetryBudget`, `_backoff`, `_event_loop_guard`) written once and driven by both worlds. New
5153
resilience logic goes in a shared object, not into each world's shell.

‎docs/adr/0001-decoders-parse-in-a-single-pass.md‎

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# No OpenTelemetry tracing middleware
2+
3+
httpware ships an `otel` extra but no span-creating middleware. `opentelemetry-instrumentation-httpx`
4+
already wraps the transport and sees redirects, connection reuse, and transport-level retries that a
5+
middleware cannot, and a second span layer would double the tree. httpware only adds `retry.*`,
6+
`bulkhead.*`, `circuit.*`, and `timeout.*` events to the active span, which is why the extra depends
7+
on `opentelemetry-api` and not the SDK.

‎docs/adr/0002-no-opentelemetry-middleware.md‎

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Sync/async parity is hand-maintained, with one named exception
2+
3+
`Client`/`AsyncClient` and each resilience middleware are two hand-written classes. Unasync-style
4+
codegen was rejected because it adds a build step and a generated file that is either committed and
5+
lying or absent from tracebacks, while the duplication that hurts is logic, which is extracted into
6+
shared objects such as `_RetryPolicy` and `_CircuitBreakerState` rather than transformed. The one
7+
deliberate break is `AsyncTimeout` without a sync sibling: sync Python cannot interrupt a blocking
8+
call mid-flight, so a sync `Timeout` would promise a deadline and deliver a suggestion, and `httpx2`'s
9+
per-call timeouts are the documented sync answer. `tests/test_client_parity.py` enforces both halves.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# 4xx, including 429, counts as a circuit-breaker success
2+
3+
The default failure set is 5xx only. A 429 means the backend is up and throttling, and a 404 or 422
4+
describes the request, so tripping on either turns a healthy service into an outage for every caller
5+
of a shared breaker. Exceptions other than `NetworkError`, httpware `TimeoutError`, and `StatusError`
6+
leave circuit state untouched, so the breaker cannot trip on load-shedding it caused itself.
7+
`failure_status_codes` is the per-deployment override, not a default worth flipping.

‎docs/adr/0003-no-sync-timeout-middleware.md‎

Lines changed: 0 additions & 24 deletions
This file was deleted.

‎docs/adr/0004-4xx-does-not-trip-the-circuit-breaker.md‎

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Circuit breaker scope: time-based window, no slow-call rate, no manual control
2+
3+
Rate mode uses a rolling `window_seconds` gated by `minimum_calls`. A count-based "last N calls"
4+
window was rejected because on low traffic it reaches back past deploys and recoveries, and
5+
`minimum_calls` already supplies the sample-size guarantee. A slow-call rate is not added because
6+
`AsyncTimeout` already converts a slow call into a counted `TimeoutError`. `force_open()` /
7+
`force_closed()` in the style of Polly's `ManualControl` were declined in
8+
[#118](https://github.com/modern-python/httpware/issues/118): breaker state is per process, so
9+
pinning one replica sheds nothing fleet-wide, and the fixes that do work, a config flag read by every
10+
replica or state in Redis, reduce to a check the caller makes before sending or to I/O inside a
11+
transition.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Decoder dispatch is a frozen ordered list with broad claims
2+
3+
`decoders=` is resolved once at `__init__` and walked in order by `_DecoderResolver.resolve`. There
4+
is no per-call override, which would be a second dispatch site the pre-flight `MissingDecoderError`
5+
check cannot see, and no stdlib `json` fallback, which would return a `dict` that type-checks as the
6+
model. Each decoder claims every model its library can parse, not only its native base class,
7+
because narrow claims reject `dict`, `list[Foo]`, dataclasses, and primitives; native types still
8+
route unambiguously since each library rejects the other's, and list order settles the
9+
library-agnostic middle. `can_decode` runs outside the `DecodeError` wrap and must return False
10+
rather than raise. A defensive wrap was rejected because it would turn a broken probe into a silent
11+
misroute.

0 commit comments

Comments
 (0)