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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Commands

- `just test` — full suite in docker compose (Postgres 17). Forwards args: `just test tests/test_unit.py -k name`.
- `just test` — full suite in docker compose (Postgres 17). Forwards args: `just test tests/test_unit.py -k name`. Args are forwarded **unquoted**, so a spaced `-k` expression (`-k "a or b"`) word-splits and fails (`file or directory not found: or`) — run one keyword per invocation, or use a single substring matching all targets.
- `just lint` — `eof-fixer`, `ruff format`, `ruff check --fix`, `ty check`. `just lint-ci` is the non-mutating variant.
- `just install` — `uv lock --upgrade && uv sync --all-extras --all-groups --frozen`.
- `just build` / `just down` / `just sh` — image build, teardown, shell into the app container.
Expand Down
9 changes: 9 additions & 0 deletions architecture/relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ c. `AcknowledgementMiddleware.__aexit__` turns publisher-chain exceptions into o
When `True`, the `process_message` override fills `Response.headers` from the inbound `OutboxMessage.headers` only when the handler returned a `Response` with empty headers (explicit user-set headers win). Default False matches the FastStream-wide convention; users who want propagation flip one kwarg.

**Envelope-managed keys are stripped for a chained `OutboxResponse`.** `_maybe_propagate_inbound_headers` drops `content-type` and `correlation_id` from the propagated dict when the result is an `OutboxResponse`. That response re-encodes through `_encode_payload`, which re-derives `content-type` from the *new* body and reads `correlation_id` from the dedicated field; propagating the inbound row's values would make `_encode_payload` raise on any cross-content-type or custom-`correlation_id` relay, nacking the **successful** inbound row to retry-exhaustion (audit F5-01 / F5-02). Foreign-publisher relays (Kafka/etc.) don't re-encode through the outbox envelope, so they keep forwarding these headers verbatim — including `content-type`.

## Who retries during a foreign-broker outage

Retries come from **two tiers**, and short outages never reach the outbox one:

- **Transient blip (~10–30s):** the client library (e.g. `aiokafka`) absorbs it with its own reconnect+retry loop. The `publish` inside the handler **blocks until the broker returns**, then succeeds. From the outbox subscriber's view that is one slow *successful* publish — **no raise, no nack, no `nacked_retried` tick, no `next_attempt_at` reschedule**. At-least-once still holds (the outbox row is held until the client acks), but the retrying tier is the client, not the outbox.
- **Sustained outage / hard failure:** the client eventually raises into the handler; `AcknowledgementMiddleware` turns that into a nack and the outbox's `retry_strategy` takes over (property (c) above).

Implications: a quick `docker compose stop kafka` will **not** produce a visible outbox-level retry log line — to exercise the outbox retry tier in a demo or test, raise inside the handler instead. Operators sizing alerts on `lease_lost` / `nacked_retried` should not expect spikes during short blips. One real failure mode: a long-blocked publish (client spinning) can outrun `lease_ttl_seconds` and surface as a `lease_lost` on the terminal write — `lease_lost` correlating with broker instability is that.
36 changes: 36 additions & 0 deletions planning/releases/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Releases

How `faststream-outbox` versions and ships to PyPI, plus the per-version notes
in this directory.

## Versioning is tag-driven

The package is **published by creating a GitHub release + git tag named with
the bare semver** (e.g. `0.10.3`, no `v` prefix). CI publishes to PyPI from the
tag. `pyproject.toml` pins `version = "0"`; the real version is **derived from
the tag at build time**, so cutting a release needs **no `pyproject` edit**.

Semver (pre-1.0, `0.x`): the org leans on **patch** bumps even for behavior
changes, but a release that adds schema migrations or input validation
rejecting previously-accepted calls is honestly a **minor** bump — size it that
way.

## Release notes

One file per version, `planning/releases/<version>.md` (e.g. `0.10.3.md`).
Format mirrors the sister project `httpware`'s `planning/releases/`:

- `# <pkg> <ver> — <headline>`
- a bold one-line summary
- gap / fix sections
- **Migration** — what users must do to upgrade
- **Touched surface** — files/areas changed
- **See also** — PR links

Minor releases add a **Breaking changes** section.

## Procedure

1. Write the notes file first; get it reviewed.
2. **Only on an explicit go** (this step publishes): create the tag + GitHub
release, using the notes file as the release body.