From d13dfa80f469ec8f4381c76e5b85d5f4caaf5162 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 20 Jun 2026 13:35:30 +0300 Subject: [PATCH 1/2] docs(releases): add release-process README (tag-driven versioning + notes format) Co-Authored-By: Claude Opus 4.8 (1M context) --- planning/releases/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 planning/releases/README.md diff --git a/planning/releases/README.md b/planning/releases/README.md new file mode 100644 index 0000000..ac688b0 --- /dev/null +++ b/planning/releases/README.md @@ -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/.md` (e.g. `0.10.3.md`). +Format mirrors the sister project `httpware`'s `planning/releases/`: + +- `# ` +- 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. From 034c1591dcf38ce2acb35a7070f476d174b7370e Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 20 Jun 2026 13:39:57 +0300 Subject: [PATCH 2/2] docs: promote relay retry-tier + just-test -k caveat from agent memory Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- architecture/relay.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 836ee01..e52723d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/architecture/relay.md b/architecture/relay.md index 3048d0c..34c0624 100644 --- a/architecture/relay.md +++ b/architecture/relay.md @@ -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.