diff --git a/planning/releases/0.10.0.md b/planning/releases/0.10.0.md new file mode 100644 index 0000000..0fd09d9 --- /dev/null +++ b/planning/releases/0.10.0.md @@ -0,0 +1,44 @@ +# faststream-outbox 0.10.0 — pass-3 audit closure: a High fix, two new features, a hardened tail + +**Minor release.** Ships the full resolution of the 2026-06-14 **pass-3 deep audit** ([findings](../audits/2026-06-14-deep-audit-pass3-findings.md)): one **High**-severity correctness fix, two additive features, a cluster of robustness/validation fixes, and a large test-hardening + documentation sweep. Backward-compatible by default — the new constructor kwargs default to today's behavior — but **three opt-in/behavior refinements are worth a look before upgrading** (see [Behavior notes](#behavior-notes)). + +## New features + +- **`OutboxBroker(..., last_exception_renderer=...)`** — an opt-in `Callable[[BaseException], str | None]` controlling what the DLQ stores in `last_exception`. The default still renders `repr(exc)` (full forensic detail); pass `lambda exc: type(exc).__name__` to redact, or `lambda exc: None` to drop the detail — for deployments whose exceptions can embed payloads/PII/credentials. The rendered string is still length-capped at 8 KiB. Also available on the FastAPI `OutboxRouter`. (#93) +- **FastAPI `OutboxRouter` now forwards `dlq_table` + `metrics_recorder`.** Previously the router built its inner broker with no handle to inject them, so FastAPI deployments **could not** enable the dead-letter queue or the recorder-based metrics at all. Both are now constructor kwargs on `OutboxRouter`, forwarded to the inner broker. (`routers` remains unforwarded pending a design call.) (#88) +- **`drain_timeout` observability.** A `stop()` drain that exceeds `graceful_timeout` (abandoning in-flight rows to lease-expiry retry) now emits a `WARNING` **and** a `drain_timeout` recorder event — surfaced as `faststream_outbox_drain_timeout_total` (Prometheus) / `messaging.outbox.drain_timeout` (OpenTelemetry). A timed-out drain used to be silent. (#92, #96) + +## Bug fixes + +- **[High] `propagate_inbound_headers=True` no longer poisons a successful relay.** When a handler returned an `OutboxResponse` with no headers, the inbound row's envelope-managed `content-type` (and `correlation_id`) were copied onto it; if the relayed body encoded to a different content-type, `_encode_payload` raised — nacking the **successful** inbound row and walking it to retry-exhaustion. Those keys are now stripped before propagating onto a chained `OutboxResponse`; foreign-publisher relays still forward them verbatim. (#85) +- **Incomplete eager-validation fix completed (F4-01/02/06/10).** `OutboxResponse` validated `activate_*`/tz eagerly but deferred `queue` and `session` to dispatch — where a bad value masqueraded as a handler failure and burned the inbound row's retry budget. All publish entry points (the command constructor, `OutboxResponse`, and `publish_batch`'s empty branch) now share one `_validate_publish_args` and reject the same misconfigurations eagerly. (#86) +- **`fetch_unprocessed(limit=...)` rejects `limit < 1`** instead of hitting an opaque DB error (real) or silently mis-slicing (fake). (#89) +- **`ping()` is bounded by `asyncio.timeout`** so a half-dead TCP socket can't hang the liveness probe for hours. (#89) + +## Robustness / correctness + +- **`validate_schema()` now flags a non-unique `timer_id_uq`** (`pg_index.indisunique`). A same-named index with the correct partial predicate but created non-unique passes the predicate check yet breaks the producer's `ON CONFLICT` arbiter at publish time. (#92) +- **63-byte identifier guard now also runs in `OutboxClient.__init__`** — a directly-constructed or reflected over-long `Table` can no longer bypass the check that previously lived only in `make_outbox_table`. (#92) +- **`OutboxBroker.stop()` sets `running=False` before the subscriber-stop gather**, so an external cancellation mid-shutdown can't leave the broker reporting live over stopped subscribers. (#92) +- **Reconnect backoff measures healthy time from a live connection** — a slow pool checkout that then fails no longer resets the backoff, preserving exponential escalation under a connection storm. (#92) + +## Internals / tests / docs + +- DRY cleanups (no behavior change): shared index-suffix constants, one `utcnow`, one `_is_future_dated`, one `_REQUEST_UNSUPPORTED_MSG`, a terminal-tags helper. (#90) +- Test-hardening: a real SKIP-LOCKED-contention test, a real-Postgres test pinning the `OutboxResponse` transactional-commit contract, lease-token threading assertions, tightened telemetry bounds, deterministic NOTIFY-wakeup tests, and adversarial DLQ-identifier quoting. (#87, #91, #95) +- Documentation: `deliveries_count` counts claims, within-batch dispatch order is unspecified, `activate_at`/batch NOTIFY timing is worker-clock-relative, the true lease bound (`fetch_batch_size + max_workers`), recommended operational alerts, and a `last_exception` redaction guide. (#89, #93, #94) + +## Behavior notes + +Read these if you upgrade in place: + +1. **`propagate_inbound_headers=True` (relay):** a chained `OutboxResponse` no longer receives the inbound `content-type`/`correlation_id` — its own encoder/field own those. If you somehow relied on the old (broken) copy, the fix changes that; foreign-broker relays are unaffected. (#85) +2. **`OutboxResponse` / empty `publish_batch` validate eagerly:** a bad `queue`/`session` now raises at the `return OutboxResponse(...)` / call site instead of at dispatch. Misconfigurations surface earlier and louder — the intended fix, but the exception location moves. (#86) +3. **`validate_schema()` is stricter (opt-in):** it now raises on a non-unique `timer_id_uq` where 0.9.x passed. If you don't call `validate_schema()`, nothing changes. (#92) + +The producer/subscriber hot paths, the lease/terminal-write invariants, and the `dlq_table=None` path are otherwise unchanged. + +## See also + +- Audit findings + full disposition ledger: [`planning/audits/2026-06-14-deep-audit-pass3-findings.md`](../audits/2026-06-14-deep-audit-pass3-findings.md). +- PRs: [#85](https://github.com/modern-python/faststream-outbox/pull/85)–[#96](https://github.com/modern-python/faststream-outbox/pull/96).