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: 13 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

`faststream-outbox` is a FastStream broker whose transport is a Postgres table (transactional
outbox pattern), Postgres-only at v0. [`CONTEXT.md`](CONTEXT.md) opens with what it does and owns
the vocabulary — read it before naming a concept in code, a test name, or an issue title.
the vocabulary. Read it before naming a concept in code, a test name, or an issue title.

## Commands

`just` (task runner) and `uv` (package manager). The [`justfile`](justfile) is the source of truth —
`just --list`, or read it. Every recipe carries its intent as a comment. The things it does not say:
`just` (task runner) and `uv` (package manager). The [`justfile`](justfile) is the source of truth:
run `just --list`, or read it. Every recipe carries its intent as a comment. The things it does not say:

- `just test [args]` forwards args **unquoted**, so a spaced `-k` expression word-splits and fails.
Run one keyword per invocation, or a substring matching all targets.
- `tests/test_unit.py` + `tests/test_fake.py` need no Postgres and run directly under `uv run
pytest`; `tests/test_integration.py` skips if `POSTGRES_DSN` is unreachable. The coverage gate is
on by default, so a partial run trips it — pass `--no-cov` while iterating.
on by default, so a partial run trips it; pass `--no-cov` while iterating.
- Nothing validates Markdown links outside `docs/`. `just docs-build` runs `mkdocs --strict` over
the site only; root Markdown, `.github/`, and `docs/agents/` are unchecked.

Expand All @@ -28,18 +28,17 @@ will **not** tell you:

- `subscriber/usecase.py` is load-bearing: **every terminal write filters on `acquired_token`**, so
a stale writer finds `rowcount == 0` and is dropped. Any new fetch or terminal path must preserve
that — `tests/test_client_contract.py::test_delete_noop_on_token_mismatch` is the claim.
that; `tests/test_client_contract.py::test_delete_noop_on_token_mismatch` is the claim.
- `client.py` and `testing.py` implement the same rules twice, in SQL and in Python, because one
runs in the database and one in the process. They cannot share an implementation, so
`tests/test_client_contract.py` couples them by behaviour — a change to either adapter's insert, fetch,
`tests/test_client_contract.py` couples them by behaviour. A change to either adapter's insert, fetch,
terminal, or DLQ semantics belongs in that suite.
- `schema.py` — the three partial indexes and the `<table>_lease_ck` CHECK are load-bearing, not
decoration.
- `message.py` — the `DLQFailureReason` `Literal` is a **public contract**; operator queries key off
- `schema.py`: the three partial indexes and the `<table>_lease_ck` CHECK are load-bearing.
- `message.py`: the `DLQFailureReason` `Literal` is a **public contract**; operator queries key off
those strings, so changing one is API-breaking.
- `metrics/` (the recorder seam) and `prometheus/` + `opentelemetry/` (native middleware) are two
seams, deliberately. Each fires for events the other physically cannot observe — `fetched` has no
`StreamMessage`, `lease_lost` fires after `consume_scope` exits. **Don't collapse them.**
seams on purpose. Each fires for events the other cannot observe: `fetched` has no
`StreamMessage`, and `lease_lost` fires after `consume_scope` exits. **Don't collapse them.**

`TestOutboxBroker` swaps in `FakeOutboxClient` and nothing else on the publish side: the real producer
and publish middlewares run, and a thin wrapper dispatches what the fake stored once the producer
Expand All @@ -57,11 +56,11 @@ what breaks it.

## Code Style

- **Never use local/inline imports** — tests included, no `if TYPE_CHECKING` exception. `ruff`
- **Never use local/inline imports**, tests included, with no `if TYPE_CHECKING` exception. `ruff`
catches them; if `# noqa: PLC0415` looks like the fix, hoist the import instead.
- Docstrings: public API documents the contract; internal helpers get a one-line contract, plus at
most 1–2 lines for a genuinely non-obvious constraint. Never narrate implementation or justify
code to a reviewer — cross-file rationale lives in an invariant test's docstring.
most one or two lines for a non-obvious constraint. Never narrate implementation or justify
code to a reviewer; cross-file rationale lives in an invariant test's docstring.
- Lint suppressions are intentional and carry their reason at the site. The recurring cluster is
everything downstream of `BrokerUsecase`'s invariance on its config type.

Expand Down
10 changes: 5 additions & 5 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ docs must agree on it. General programming vocabulary does not belong here, howe
project uses it.

**Row**:
One outbox record — the unit the transport moves. There is no `state` column: a row exists until it
One outbox record: the unit the transport moves. There is no `state` column: a row exists until it
is delivered or terminally fails.
_Avoid_: job, task, event

Expand All @@ -33,7 +33,7 @@ statement matches nothing.
_Avoid_: lease id, owner id

**Claim**:
Acquiring a lease on a row via the fetch CTE. Increments `deliveries_count` — including on an
Acquiring a lease on a row via the fetch CTE. Increments `deliveries_count`, including on an
expired-lease reclaim.
_Avoid_: fetch (that is the loop and the client method), dequeue

Expand All @@ -50,7 +50,7 @@ The bounded wait on stop for in-flight rows to reach a terminal write.
_Avoid_: graceful shutdown (that names the whole phase, not this wait)

**Outcome**:
The single disjoint result `dispatch_one` matches on — `Ack`, `Retry(delay_seconds)`, or
The single disjoint result `dispatch_one` matches on: `Ack`, `Retry(delay_seconds)`, or
`Terminal(reason)`.
_Avoid_: status, result, state

Expand All @@ -64,7 +64,7 @@ _Avoid_: schedule, delayed message

**`timer_id`**:
The dedup key for at most one *live* row per `(queue, timer_id)`. It is not a global once-ever
idempotency key — a delivered row's id is reusable.
idempotency key; a delivered row's id is reusable.
_Avoid_: idempotency key, message key

**Relay**:
Expand All @@ -73,7 +73,7 @@ _Avoid_: bridge, forwarder

**DLQ**:
The opt-in sibling audit table terminal failures are archived to. Off by default.
_Avoid_: dead-letter queue as a transport — nothing consumes it
_Avoid_: dead-letter queue as a transport (nothing consumes it)

**Recorder seam**:
The `MetricsRecorder` callable the library invokes for events outside the FastStream bus.
Expand Down
4 changes: 2 additions & 2 deletions docs/adr/0002-free-threading-is-compat-only.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Free-threading is a compatibility guarantee, not a parallelism redesign

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.
3.14t with the GIL disabled. The subscriber is not rearchitected 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
`Free Threading :: 2 - Beta` classifier, and a docs note, with no 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 was bounded by SQLAlchemy until 2.0.54, whose
Expand Down
Loading