Skip to content
Merged
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
44 changes: 44 additions & 0 deletions planning/releases/0.10.0.md
Original file line number Diff line number Diff line change
@@ -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).