Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions docs/adr/0001-metrics-recorders-not-unified.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
# Metrics recorders stay separate; no shared eventmetric table
# Metrics recorders stay separate, with no shared event-to-metric table

**Decision:** the two `MetricsRecorder` adapters (`metrics/prometheus.py`,
`metrics/opentelemetry.py`) keep their own hand-written `__call__` event switches. There is no
declarative event→metric table both adapters consume.

The two switches branch on the same event names, which reads as duplication, but only the dispatch
ladder is actually shared. The same event maps to a *different number of instruments* per backend —
`dispatched` is three Prometheus operations and an OpenTelemetry no-op — and the same tag is a
labeled counter in one and a span attribute in the other. A `(kind, name, label set)` table would
need a per-backend escape hatch on nearly every row, and deleting it would push the per-event bodies
back exactly where they are now.

- **A parity contract test**, feeding each event to both adapters, addresses the real worry ("add an
event, forget one adapter") — which is parity, not duplication. Declined for now: the vocabulary
is stable and additive, both adapters are independently tested, and a new event is emitted from a
call site the author is already editing.
The two `MetricsRecorder` adapters, `metrics/prometheus.py` and `metrics/opentelemetry.py`, keep
their own hand-written `__call__` event switches, and no declarative event-to-metric table is shared
between them. Only the dispatch ladder is common: the same event maps to a different number of
instruments per backend, so `dispatched` is three Prometheus operations and an OpenTelemetry no-op,
and the same tag is a labeled counter in one and a span attribute in the other. A
`(kind, name, label set)` table would need a per-backend escape hatch on nearly every row, and
deleting it would push the per-event bodies back exactly where they are now. A parity contract test
feeding each event to both adapters addresses the real worry, which is parity rather than
duplication, and is declined for now because the event vocabulary is stable and additive and both
adapters are independently tested.
43 changes: 12 additions & 31 deletions docs/adr/0002-free-threading-is-compat-only.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,14 @@
# Free-threading is a compatibility guarantee, not a parallelism redesign

**Decision:** "free-threaded support" means proving that `faststream-outbox` runs correctly on a
free-threaded CPython (3.14t) with the GIL disabled, scoped to "when SQLAlchemy's Cython extensions
are off". It does not mean rearchitecting the subscriber to use multiple CPU cores. Target 3.14t
only, not 3.13t.

The package is pure-Python asyncio — one event loop, N worker *tasks* rather than OS threads, no
`threading`, lock, or C-extension code of its own — so free-threading changes none of its runtime
semantics and the guarantee costs a CI job, a classifier, and a docs note rather than a source
change. What users need to know is in `docs/introduction/installation.md`: 3.14t support, and that
`DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` is required for a genuinely GIL-free process because
SQLAlchemy's cyextensions do not declare `Py_MOD_GIL_NOT_USED` and re-enable the GIL process-wide on
import.

- **Exploiting the parallelism** — worker loops across OS threads or multiple event loops — is a
rearchitecture of a deliberately single-loop design; the two-loop subscriber, the lease-token
invariant, and drain-on-stop all assume one loop. Outbox throughput is dominated by Postgres I/O
and lease contention rather than in-process CPU, and scaling today is "run more subscriber
processes", which already uses more cores. Real invariant-breaking risk for an unproven gain.
- **Dropping the GIL-off assertion** and making no GIL claim discards the regression guard that
caught the SQLAlchemy behaviour in the first place.
- **Deferring until SQLAlchemy ships free-thread-safe cyextensions** banks nothing: the guarantee is
true today, the workaround is SQLAlchemy's own documented switch, the upstream timeline is
open-ended, and withholding the classifier leaves 3.14t adopters with no signal.
- **Targeting 3.13t** is not available: the compiled dependencies ship `cp314t` wheels but no
`cp313t` ones, so the full graph will not install.

**Revisit trigger:** drop `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` from the `freethreaded` CI job and the
docs caveat once SQLAlchemy's Cython extensions declare `Py_MOD_GIL_NOT_USED`. This is the one part
of the decision that is scheduled work rather than a judgement call — the workaround is upstream's
to remove, and until it does, the env var is load-bearing. Re-run the job's GIL assertion without it
to confirm before removing. Tracked as #160.
Free-threaded support means proving that `faststream-outbox` runs correctly on free-threaded CPython
3.14t with the GIL disabled; it does not mean rearchitecting the subscriber to use multiple cores.
The package is pure-Python asyncio, one event loop with N worker tasks rather than OS threads, so
free-threading changes none of its runtime semantics and the guarantee costs a CI job, a
`Free Threading :: 2 - Beta` classifier, and a docs note rather than a source change. Exploiting the
parallelism was rejected because the two-loop subscriber, the lease-token invariant, and
drain-on-stop all assume one loop, throughput is dominated by Postgres I/O, and scaling today means
running more subscriber processes. The guarantee is bounded by SQLAlchemy: its Cython extensions do
not declare `Py_MOD_GIL_NOT_USED` and re-enable the GIL process-wide on import, so
`DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` is load-bearing until upstream fixes that
([#160](https://github.com/modern-python/faststream-outbox/issues/160)). 3.13t is not a target
because the compiled dependencies ship `cp314t` wheels only.
Loading