Skip to content

Releases: modern-python/faststream-outbox

0.13.0

Choose a tag to compare

@github-actions github-actions released this 18 Jul 08:31
13dbd9b

faststream-outbox 0.13.0 — free-threaded (3.14t) Python support

Minor release. faststream-outbox now officially supports free-threaded
(no-GIL) CPython. There is no change to the installed package's runtime
behavior or public API
— the producer, subscriber, lease/terminal-write,
timer, DLQ, and validation paths are identical to 0.12.0. The only shipped delta
is a new trove classifier; everything else is CI and docs.

Breaking changes

None — additive packaging metadata only.

Packaging

  • Added the Programming Language :: Python :: Free Threading :: 2 - Beta
    classifier. On a python3.14t interpreter, pip/uv resolve the
    free-threaded (cp314t) wheels of the compiled dependencies (asyncpg,
    sqlalchemy, pydantic-core) automatically. No dependency-floor change; GIL
    builds are unaffected.

Free-threaded support — what it means

  • The package is pure-Python asyncio (one event loop, worker tasks not OS
    threads), so nothing in it depends on the GIL. CI now runs the full suite on
    the 3.14t interpreter and, in a separate step, asserts that importing the
    outbox and its runtime dependencies keeps the GIL disabled — so a future
    dependency that silently re-enables the GIL turns the build red.
  • It does not add cross-core parallelism. The subscriber is single-loop by
    design; to use more cores, run more subscriber processes — the same lever as
    on a GIL build.
  • To keep the GIL genuinely disabled, set DISABLE_SQLALCHEMY_CEXT_RUNTIME=1.
    SQLAlchemy's Cython extensions ship 3.14t wheels but don't yet declare
    free-thread safety, so importing SQLAlchemy otherwise re-enables the GIL
    process-wide (your outbox code runs correctly either way). Foreign-broker
    relay clients you install (e.g. aiokafka) can do the same. See the
    Free-threaded Python docs.
  • 3.14t only. The compiled dependencies ship no cp313t wheels, so 3.13t
    is not a supported free-threaded target.

Migration

No action needed — no API or behavior change. Existing installs on GIL builds
(Python 3.11–3.14) are unaffected.

Touched surface

  • pyproject.toml — the new classifier.
  • .github/workflows/_checks.yml — the standalone freethreaded CI job.
  • docs/introduction/installation.md — a "Free-threaded Python" section.
  • planning/ — the design change file and two decision records.

See also

  • #151 — Free-threaded Python (3.14t) support

0.12.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 12:27
c4cf3c9

faststream-outbox 0.12.0 — architecture-deepening pass (internal refactors, no production behavior change)

Minor release. Every change to the installed package's production runtime is an
internal refactor — the producer, subscriber, lease/terminal-write, timer, DLQ, and
validation behavior are identical to 0.11.0, verified by the unchanged suite at 100%
coverage across Python 3.11–3.14. The batch came out of an architecture review that
deepened shallow modules, named duplicated rules, and removed a dead field. The one
behavior change is test-broker-only (a tightened session argument, below), which is why
this is a minor rather than a patch. Drop-in for production code.

Breaking changes

  • TestOutboxBroker: cancel_timer / fetch_unprocessed now require session. These
    two methods moved behind the AbstractOutboxClient seam (see refactors below). Under the
    test broker they previously accepted an optional session (an internal patch
    defaulted it to None); they now require session=..., matching the real broker's
    signature and the documented public contract (the argument is still ignored under the
    test broker). Production is unaffectedbroker.cancel_timer / broker.fetch_unprocessed
    always required session. Only test code that called these on a TestOutboxBroker
    without session needs a one-token edit. (#141)

Internal refactors (no behavior change)

Each deepens the codebase — more behavior behind smaller, single-owner seams — without
changing observable production behavior.

  • Schema-drift validator split out of client.py. The ~285 LOC of Alembic-diff +
    pg_catalog probes moved to a new schema_validation.py; the transport client roughly
    halved (796 → 471 LOC) and no longer imports the optional alembic dependency.
    broker.validate_schema() / client.validate_schema() are unchanged.
    (#139)
  • Dead to_delete intent flag removed. OutboxInnerMessage.to_delete was set on every
    ack/terminal path but read by zero production code (dispatch branched on other fields).
    Removed; tests now assert on the fields the dispatcher actually reads.
    (#140)
  • cancel_timer / fetch_unprocessed routed through the client seam. Both built inline
    SQL on the broker's table handle, bypassing the OutboxClient that already owns the
    table. They are now methods on AbstractOutboxClient (real + fake); the broker delegates,
    and the test-broker patch machinery for them is deleted. (Carries the test-broker session
    change above.) (#141)
  • Envelope-managed header keys named once. content-type and correlation_id were bare
    string literals across envelope.py (write), parser.py (read), and the relay
    header-strip. Defined once in message.py as CONTENT_TYPE_HEADER /
    CORRELATION_ID_HEADER / ENVELOPE_MANAGED_HEADERS.
    (#143)
  • Lease-token guard named; contract coverage filled. The NULL = NULL token guard the
    fake repeated inline is now one _lease_token_matches helper. The cross-adapter contract
    suite gained the missing mark_pending unleased-row scenario (the delete path already had
    it) and an empty-queues symmetry check.
    (#144)
  • Terminal intent deepened to a canonical Outcome sum type. OutboxInnerMessage now
    records one OutcomeAck | Retry(delay_seconds) | Terminal(reason) — instead of
    three loosely-coupled fields. Success is an explicit Ack(); "reason and delay both set"
    is now unrepresentable. terminal_failure_reason / pending_delay_seconds / state_set
    remain as read-only property views, so readers and the DLQFailureReason public
    contract are unchanged. (#145)

Design records (no code change)

  • The conn: AsyncConnection | None union on the client seam is deliberate. The review
    flagged it as a "type lie"; on inspection it is a documented widening (the fake genuinely
    has no connection, the subscriber genuinely produces None on the fake path). Recorded as
    an accepted decision with a revisit trigger.
    (#142)
  • The two observability seams overlap on the consume/publish series. architecture/metrics.md
    now documents that the recorder adapters and the native middleware emit the same-named
    consume/publish metrics — one Prometheus registry raises Duplicated timeseries, live OTel
    meters on both double-count — and why the library can't guard it at construction (separate
    registries / middleware-as-source-of-truth stays an operator concern).
    (#146)

Migration

  • Production code: none. No public-API change, no schema change, no config change.
  • Test suites using TestOutboxBroker: if you call .cancel_timer(...) or
    .fetch_unprocessed(...) on the test broker without a session, add session=... (any
    session — it is ignored under the test broker). Everywhere else, no change.

Touched surface

  • Package code (internal only): faststream_outbox/schema_validation.py (new),
    client.py, broker.py, message.py, envelope.py, parser/parser.py,
    subscriber/usecase.py, testing.py.
  • Tests: tests/test_client_contract.py (+2 scenarios), tests/test_unit.py,
    tests/test_fake.py.
  • Living docs / decisions: CLAUDE.md, architecture/dlq.md, architecture/test-broker.md,
    architecture/metrics.md, docs/usage/testing.md, planning/decisions/2026-07-17-conn-union-is-deliberate.md.

See also

  • #139 — split schema-drift validator.
  • #140 — remove dead to_delete flag.
  • #141 — table ops behind the client seam.
  • #142conn union deliberate (decision).
  • #143 — name envelope header keys.
  • #144 — name lease-token guard + contract coverage.
  • #145 — canonical Outcome sum type.
  • #146 — two-seam overlap hazard (docs).

0.11.0

Choose a tag to compare

@github-actions github-actions released this 16 Jul 15:08
0240751

faststream-outbox 0.11.0 — a performance pass: write-path, terminal-path, and autovacuum tooling

Minor release. A throughput-focused release built from a performance review
of the transactional-outbox transport: the producer stops emitting redundant
NOTIFYs, the subscriber gains an opt-in batched terminal flush, and a new
outbox_autovacuum_ddl helper (plus an opt-in validate_schema probe) makes the
recommended table hygiene one function call. A reproducible benchmark harness
underpins all of it. Backward-compatible by default — every new knob defaults
to today's behavior, so an in-place upgrade changes nothing until you opt in.

Performance

  • Producer NOTIFY dedup — one pg_notify per (transaction, queue), not per
    publish.
    broker.publish / publish_batch ran a SELECT pg_notify(channel, queue) on every call, so N publishes to one queue in one transaction emitted N
    NOTIFY statements — N−1 of them pure round-trip waste, since Postgres already
    coalesces identical (channel, payload) NOTIFYs per transaction at delivery. A
    transaction-scoped memo (a WeakKeyDictionary keyed on the innermost active
    transaction) now emits exactly one pg_notify per distinct queue per
    transaction. Behavior-preserving — the subscriber still gets the same
    "≥1 wake per queue per commit" it did before, fired inline at the first publish;
    delivery, ordering, and the transactional producer contract are unchanged. On a
    bulk publish (5000 rows, one queue, one transaction) the write path measured
    ~1.9× throughput / ~46% less wall-clock on loopback Postgres, and the
    absolute saving grows with database round-trip latency. Single-publish-per-
    transaction workloads are unaffected (one row still emits one NOTIFY). Default-on,
    no knob. (#135)

New features

  • Opt-in batched terminal flush — terminal_flush_batch_size (default 1).
    The subscriber deletes/DLQs each terminally-handled row individually by default
    (terminal_flush_batch_size=1, identical to prior behavior). Set it higher to
    batch terminal writes into one statement per N rows, cutting the terminal-path
    statement count under high throughput. This widens the at-least-once
    redelivery window
    — a crash between handler success and the batched flush
    redelivers up to N−1 already-processed rows on recovery — so it is opt-in and
    off by default; only raise it when your handlers are idempotent. Available on the
    broker subscriber, OutboxRouter, and the FastAPI router. (#131, #132)

  • outbox_autovacuum_ddl(...) — the recommended per-table autovacuum settings
    as one call.
    A new top-level export that renders the
    ALTER TABLE … SET (autovacuum_*) statement for an outbox table — drop it into
    an Alembic migration (op.execute(outbox_autovacuum_ddl("outbox"))) or run it via
    psql. It sets autovacuum_vacuum_scale_factor = 0 plus size-independent dead-tuple
    / insert thresholds (eligibility), breaking the stale-reltuples death-spiral that
    makes a high-churn queue table bloat. Signature:

    outbox_autovacuum_ddl(
        table_name="outbox", *, schema=None,
        vacuum_threshold=1000, insert_threshold=1000,
        vacuum_cost_delay=None, vacuum_cost_limit=None,
    ) -> str

    vacuum_cost_delay / vacuum_cost_limit are the vacuum throughput knobs
    (off by default → cluster default emitted). A churn measurement showed throughput
    — not eligibility — is the binding constraint under heavy sustained load, so these
    let vacuum keep pace; vacuum_cost_delay=0 runs it unthrottled (fast, but
    I/O-heavy on a shared cluster — opt in deliberately). (#133, #134)

  • validate_schema(check_autovacuum=True) — opt-in autovacuum probe. The
    schema validator gains a check_autovacuum flag (default False) that asserts the
    live table carries the structural scale_factor = 0 + threshold settings, so a CI
    gate can catch a table created without the recommended hygiene. The situational
    throughput knobs (cost_delay / cost_limit) are not enforced — they are
    tuning, not a structural requirement. Default False keeps validate_schema()
    identical to 0.10.x. (#133)

Bug fixes

  • validate_schema() now reflects a table in a named schema correctly. The
    validator's Alembic reflection omitted include_schemas=True, so a table created
    under a non-default schema (make_outbox_table(..., schema="app")) failed
    validation spuriously. Reflection now filters by the target schema, and an
    explicit-default-schema table (MetaData(schema="public")) is normalized against
    the connection's default_schema_name so it validates as unqualified. (#133)

Internals / tooling

  • Benchmark harness for the outbox transport (benchmarks/, just bench /
    just bench-check).
    A reproducible producer/consumer workload sweep that records
    per-message DB counters via pg_stat_statements (split by leading SQL keyword) and
    gates the deterministic, machine-independent counts against
    benchmarks/baseline.json in CI. This is contributor tooling — it drove and now
    locks the wins above (the producer NOTIFY dedup shows as select_calls 5000 → 1;
    the batched terminal flush as fewer delete_calls). Not part of the installed
    wheel. (#130)
  • Honest autovacuum docs. The operations and architecture docs were rewritten to
    scope the benefit precisely: the shipped scale_factor=0 + threshold settings
    control vacuum eligibility (necessary, not sufficient); under heavy churn,
    throughput (cost_delay/cost_limit) is the binding lever. Framed as standard
    queue-table hygiene and insurance, not a measured silver bullet. (#134)

Breaking changes

None. Every new parameter defaults to prior behavior:
terminal_flush_batch_size=1 (per-message flush), check_autovacuum=False, and the
outbox_autovacuum_ddl cost knobs default to None (cluster default). The
NOTIFY dedup is behavior-preserving. An in-place upgrade requires no code or
schema change.

Migration

  • No action required to upgrade. Optional adoption:
    • Set terminal_flush_batch_size=N on your subscriber(s) only if your handlers
      are idempotent
      — it trades a wider redelivery window for terminal-path
      throughput.
    • Apply outbox_autovacuum_ddl("<table>") in an Alembic migration to adopt the
      recommended autovacuum settings; add vacuum_cost_delay/vacuum_cost_limit if
      you run a high-churn table and vacuum can't keep pace.
    • Add check_autovacuum=True to a validate_schema() CI gate to enforce the above.

Touched surface

  • faststream_outbox/publisher/producer.py — per-(transaction, queue) NOTIFY
    dedup memo. (#135)
  • faststream_outbox/subscriber/ + registrator.py, router.py,
    fastapi/router.pyterminal_flush_batch_size plumbed through the subscriber
    config, factory, and all three registration entry points. (#131)
  • faststream_outbox/autovacuum.py (new) + __init__.py export — outbox_autovacuum_ddl. (#133, #134)
  • faststream_outbox/client.py, broker.py, testing.pyvalidate_schema(check_autovacuum=...) + named-schema reflection fix. (#133)
  • benchmarks/ (new) + Justfile (bench / bench-check) + CI bench job. (#130)
  • docs/operations/, architecture/ — batching, autovacuum (throughput-scoped), and producer-NOTIFY docs. (#132, #134, #135)

See also

0.10.5

Choose a tag to compare

@github-actions github-actions released this 01 Jul 20:44
4b22431

faststream-outbox 0.10.5 — release pipeline on PyPI Trusted Publishing

No library changes. The package is identical to 0.10.4; this release exercises the new publish path end-to-end.

CI

  • Releases now authenticate to PyPI via Trusted Publishing (OIDC) instead of a long-lived PYPI_TOKEN secret. uv publish auto-detects the GitHub Actions id-token; the release job runs under a pypi environment that scopes the trusted publisher (#123).

Downstream

No action required. Nothing about the installed package changes.

0.10.4 — internal refactors + richer PyPI metadata, no behavior change

Choose a tag to compare

@lesnik512 lesnik512 released this 23 Jun 19:15
52b8ba1

faststream-outbox 0.10.4 — internal refactors + richer PyPI metadata, no behavior change

Patch release. Every change to the installed package is an internal refactor
the runtime behaves identically to 0.10.3 (same producer, subscriber, lease/terminal-write,
timer, DLQ, and validation behavior). The one user-visible change is richer PyPI package
metadata
. Drop-in upgrade.

Internal refactors (no behavior change)

These deepen the codebase without changing any observable behavior; they exist to remove
duplication and concentrate logic, verified by the unchanged test suite (and a new
cross-adapter contract suite).

  • Outbox rules de-duplicated between the real and fake clients. The genuinely-shared
    pure bits are now single-sourced — _scheduling.py (activate-args resolution +
    validation, shared by the real and fake publish paths) and _DLQ_PROJECTION in
    schema.py (one declarative outbox→DLQ column map both the real DLQ CTE and the fake
    build from). The rules that must run server-side in Postgres (eligibility, lease cutoff,
    retry timing) stay as two implementations but are now co-verified by a single
    parametrized contract suite run against both the real client and the in-memory fake.
    (#109)
  • Lease-lost telemetry has one home. The byte-identical detect→log→emit block that the
    terminal and retry flush paths each carried is consolidated into a single
    _emit_lease_lost(row, *, phase) helper. (#110)
  • OutboxSubscriberConfig validates itself. Subscriber-knob validation moved from the
    factory into OutboxSubscriberConfig.__post_init__, so every construction path is
    guarded, not just @broker.subscriber. Behavior is preserved exactly; warning
    attribution (the warning points at your @subscriber call site) is now computed via a
    manual stacklevel walk so it's robust across CPython 3.13/3.14 and the FastAPI-router
    path. (#111)

Packaging

  • Richer PyPI metadata — keywords, classifiers, and project URLs. Better
    discoverability and project links on the PyPI page; no install-time or runtime effect.
    (#108)

Migration

None. Drop-in upgrade from 0.10.x — no public-API change, no schema change, no config
change. If you don't construct OutboxSubscriberConfig directly (the normal case), nothing
about your code changes; if you do, it now validates on construction (rejecting the same
impossible values the factory always rejected).

Touched surface

  • Package code (internal only): faststream_outbox/_scheduling.py (new),
    publisher/producer.py, broker.py, schema.py, client.py, testing.py,
    subscriber/usecase.py, subscriber/config.py, subscriber/factory.py.
  • Packaging: pyproject.toml (metadata only).
  • Tests: tests/test_client_contract.py (new, cross-adapter contract), plus updates
    to tests/test_unit.py, tests/test_fake.py, tests/test_integration.py.
  • Living docs: CLAUDE.md, architecture/dlq.md, architecture/test-broker.md.

See also

  • #108 — PyPI metadata.
  • #109 — rules de-duplication + client contract suite.
  • #110 — lease-lost telemetry consolidation.
  • #111 — self-validating subscriber config.

0.10.3 — schema validation matches the lease CHECK by predicate, not name

Choose a tag to compare

@lesnik512 lesnik512 released this 17 Jun 05:30
7015ade

faststream-outbox 0.10.3 — schema validation matches the lease CHECK by predicate, not name

Patch release. One bug fix to validate_schema()'s CHECK-constraint probe,
correcting the 0.10.2 approach. No public-API change. The only change to the
installed package is how the probe identifies the lease CHECK; everything else is
identical to 0.10.2.

Fixes

  • validate_schema() no longer false-fails when the lease CHECK was created
    under a literal name while the MetaData carries a ck naming_convention.

    0.10.2 made the probe read the expected name off the Table object, which
    under a ck convention resolves to the doubled ck_<table>_<table>_lease_ck.
    But a hand-written migration —
    op.create_check_constraint('<table>_lease_ck', ...) — creates the literal
    name verbatim, because Alembic op functions don't apply target_metadata's
    convention. So the probe demanded a name the migration never produces and
    raised a spurious

    Outbox schema mismatch: missing CHECK constraint 'ck_<table>_<table>_lease_ck' …

    on a valid schema. The live constraint name is simply not predictable from
    the package side — it depends on how the migration was authored
    (MetaData.create_all / convention-aware autogenerate → doubled name; a
    hand-written op.create_check_constraint → literal name).

    _validate_check_constraints_sync now matches the lease CHECK by predicate,
    not name
    : it normalizes every live CHECK's definition and passes if one
    enforces (acquired_token IS NULL) = (acquired_at IS NULL) under any name.
    When none does (including a drifted predicate — that just means the correct one
    is absent), it reports

    missing CHECK constraint enforcing 'acquired_token is null = acquired_at is null' (the lease invariant; name it e.g. <table>_lease_ck)

    This is strictly more correct: a CHECK's name is irrelevant to whether it
    enforces the invariant. _resolve_check_constraint_name (added in 0.10.2) is
    removed.

Compatibility

validate_schema() is opt-in (you call it from a health check / CI gate; it
is never run by broker.start()). If you don't call it, nothing changes.

If you do: any schema whose lease CHECK enforces the correct predicate now
validates regardless of the constraint's name — with no convention, with a ck
convention and the doubled name, or with a ck convention and a literal-named
hand-written migration (the case 0.10.2 broke). Schemas that genuinely lack the
invariant still fail, now with a predicate-describing message instead of a
guessed name.

The "wrong predicate" drift case now reports as "missing CHECK constraint
enforcing …" rather than "has wrong predicate" — a drifted CHECK doesn't enforce
the invariant, so it reads as the correct one being absent. The remediation
pointer (#fixing-drift-autogenerate-cant-see) still fires.

No other behavior change — producers, subscribers, the lease / terminal-write
paths, timers, the index/uniqueness probes, and the dlq_table=None path are all
identical to 0.10.2.

Docs

  • docs/operations/alembic.md — replaced the "introspect the rendered name"
    recipe with "the CHECK name doesn't matter; match the predicate." The literal
    op.create_check_constraint('outbox_lease_ck', ...) recipe is now correct even
    under a convention.
  • architecture/dlq.md — recorded the match-by-predicate invariant and why
    name-prediction was reverted.

Touched surface

  • faststream_outbox/client.py_validate_check_constraints_sync matches by
    predicate; _resolve_check_constraint_name and the now-unused CheckConstraint
    import removed. Only package code change.
  • docs/operations/alembic.md, architecture/dlq.md — recipe + invariant.
  • tests/test_unit.py, tests/test_integration.py — regression coverage
    (predicate matched under convention-doubled name, under literal name, missing
    describes the predicate, drifted-reads-as-missing, and an end-to-end Postgres
    pass for convention metadata + a literally-named constraint).

See also

  • Corrects the 0.10.2 approach
    (#102).

0.10.2 — schema validation honors naming_convention

Choose a tag to compare

@lesnik512 lesnik512 released this 16 Jun 19:34
b95225a

faststream-outbox 0.10.2 — schema validation honors naming_convention

Patch release. One bug fix to validate_schema()'s CHECK-constraint probe.
No public-API change. The only change to the installed package is which
constraint name the probe looks up; everything else is identical to 0.10.1.

Fixes

  • validate_schema() no longer false-fails under a SQLAlchemy ck
    naming_convention.
    A MetaData carrying a naming_convention with a ck
    key re-templates the package's explicitly-named lease CheckConstraint — the
    given name becomes the %(constraint_name)s token, so the live constraint is
    named e.g. ck_<table>_<table>_lease_ck, not <table>_lease_ck. The probe
    hard-coded the literal <table>_lease_ck, never found the re-templated name,
    and raised a spurious

    Outbox schema mismatch: missing CHECK constraint '<table>_lease_ck' …

    on a perfectly valid schema. _validate_check_constraints_sync now reads the
    expected name off the Table object (identifying the lease constraint by
    its normalized predicate and using its convention-resolved .name), so the
    expectation always matches what SQLAlchemy / Alembic emit from your metadata —
    convention or not. The explicitly-named indexes were never affected: the
    ix / uq convention keys only re-template auto-named indexes.

Compatibility

validate_schema() is opt-in (you call it from a health check / CI gate; it
is never run by broker.start()). If you don't call it, nothing changes.

If you do, and you use no naming_convention, behavior is identical to
0.10.1 — the probe still resolves to the literal <table>_lease_ck. If you use a
ck convention, validation that previously raised spuriously now passes,
provided the constraint in your DB carries the convention-rendered name (which it
does when the schema was created via MetaData.create_all or a
convention-aware Alembic migration). Hand-written migrations must create the
constraint under that same rendered name — see the updated
naming_convention guidance
in the Alembic docs.

No other behavior change — producers, subscribers, the lease / terminal-write
paths, timers, the index/uniqueness probes, and the dlq_table=None path are
all identical to 0.10.1.

Docs

  • docs/operations/alembic.md — replaced the misleading
    "wrap each name in op.f('outbox_lease_ck')" caveat with an
    introspect-the-rendered-name recipe (the probe now expects the
    convention-resolved name, not the literal).
  • architecture/dlq.md — recorded the convention-awareness invariant in the
    validate_schema() mechanics section.

Touched surface

  • faststream_outbox/client.py — new _resolve_check_constraint_name +
    _validate_check_constraints_sync reads the expected CHECK name off the
    Table. Only package code change.
  • docs/operations/alembic.md, architecture/dlq.md — caveat fix + invariant.
  • tests/test_unit.py, tests/test_integration.py — regression coverage
    (convention-resolved name honored, missing-under-convention, literal-name
    fallback, and an end-to-end Postgres pass under a ck convention).

See also

0.10.1 — actionable schema-drift errors

Choose a tag to compare

@lesnik512 lesnik512 released this 16 Jun 16:53
b784b80

Patch release. One opt-in diagnostic refinement plus docs/branding. No public-API change and no breaking change beyond 0.10.0's. The only change to the installed package is validate_schema()'s error text.

Diagnostics

  • validate_schema() now tells you how to fix Alembic-blind drift (#99). A missing/altered <table>_lease_ck CHECK and a drifted partial-index predicate cannot be remediated by alembic revision --autogenerate (no check-constraint comparator; the index comparator ignores postgresql_where). When one of those probes fires, the raised RuntimeError now appends a pointer to the hand-written-migration recipe. Autogenerate-fixable drift (columns, plain indexes, DLQ) gets no pointer.

Compatibility

validate_schema() is opt-in; if you don't call it, nothing changes. If you do, the error gains a trailing pointer block but the "Outbox schema mismatch: " prefix and per-error strings are unchanged, so existing greps / match= assertions still hold. No other behavior change.

Docs

  • New "Fixing drift autogenerate can't see" section in docs/operations/alembic.md + cross-link (#99).
  • Org logo, favicon, brand palette (#100).
  • architecture/ deep-dive refresh for 0.10.0 surfaces (#98).

Full notes: planning/releases/0.10.1.md. PRs: #99, #100, #98.

0.10.0 — pass-3 audit closure: a High fix, two features, a hardened tail

Choose a tag to compare

@lesnik512 lesnik512 released this 14 Jun 14:44
f0f4d6c

Minor release. Full resolution of the 2026-06-14 pass-3 deep audit — one High correctness fix, two additive features, a cluster of robustness/validation fixes, and a large test-hardening + docs sweep. Backward-compatible by default; three opt-in/behavior refinements noted below.

New features

  • OutboxBroker(..., last_exception_renderer=...) — opt-in Callable[[BaseException], str | None] to redact or drop the DLQ last_exception (default keeps repr). For deployments whose exceptions can embed payloads/PII/credentials. Also on the FastAPI OutboxRouter. (#93)
  • FastAPI OutboxRouter forwards dlq_table + metrics_recorder — DLQ archival and the recorder-metrics seam were previously unreachable for FastAPI users. (#88)
  • drain_timeout observability — a stop() drain exceeding graceful_timeout now emits a WARNING + a metric (faststream_outbox_drain_timeout_total / messaging.outbox.drain_timeout). (#92, #96)

Bug fixes

  • [High] propagate_inbound_headers=True no longer poisons a successful relay: a chained OutboxResponse no longer inherits the inbound content-type/correlation_id, which previously raised in _encode_payload and nacked the successful inbound row to retry-exhaustion. (#85)
  • Completed the eager-validation fix: OutboxResponse and empty publish_batch now reject a bad queue/session at the call site (one shared _validate_publish_args) instead of masquerading as a handler failure at dispatch. (#86)
  • fetch_unprocessed(limit<1) rejected; ping() bounded by a timeout. (#89)

Robustness / correctness

  • validate_schema() flags a non-unique timer_id_uq (indisunique). (#92)
  • 63-byte identifier guard also enforced in OutboxClient.__init__. (#92)
  • OutboxBroker.stop() sets running=False before the subscriber-stop gather; reconnect backoff measures healthy time from a live connection. (#92)

Behavior notes (read before upgrading in place)

  1. propagate_inbound_headers=True: a chained OutboxResponse no longer receives the inbound content-type/correlation_id (foreign-broker relays unaffected). (#85)
  2. OutboxResponse / empty publish_batch validate eagerly — a bad queue/session raises earlier (at the call site). (#86)
  3. validate_schema() is stricter (opt-in): raises on a non-unique timer_id_uq. No-op if you don't call it. (#92)

Hot paths, lease/terminal-write invariants, and the dlq_table=None path are otherwise unchanged.

Full notes: planning/releases/0.10.0.md. Disposition ledger: planning/audits/2026-06-14-deep-audit-pass3-findings.md. PRs: #85#97.

0.9.1 — suspected-findings fixes + diagnostics cleanup

Choose a tag to compare

@lesnik512 lesnik512 released this 13 Jun 10:24
04a93af

faststream-outbox 0.9.1 — suspected-findings fixes + diagnostics cleanup

Patch release. Closes the five "suspected" findings from the 2026-06-12 audit (4 fixed, 1 already resolved in 0.9.0) plus three diagnostics / test-broker cleanups. No new features and no new breaking changes beyond 0.9.0's. One opt-in behavior change worth a look before upgrading: validate_schema() now catches partial-index drift it previously missed (see Behavior change).

Robustness / correctness

  • S1 — bounded the LISTEN-connection teardown close. On reconnect/shutdown, a graceful close() of the raw LISTEN connection ran unbounded — on the same half-dead socket the health probe may have just flagged, it could block on the kernel keepalive and wedge the fetch loop. It's now wait_for(close, _LISTEN_CLOSE_TIMEOUT) with an immediate terminate() fallback, best-effort so teardown never raises.
  • S2 — validate_schema() detects partial-index predicate drift. Alembic's autogenerate diff compares index columns + uniqueness but ignores postgresql_where, so a {table}_timer_id_uq built with the wrong predicate (WHERE timer_id IS NULL) — or as a plain non-partial UNIQUE (queue, timer_id) — passed validation, then broke the producer's ON CONFLICT arbiter inference at publish time. validate_schema() now probes the live partial-index predicates (pg_get_expr(indpred, …)) for all three indexes and flags both a wrong predicate and a present-but-non-partial index.
  • S4 — import faststream_outbox is resilient to upstream module moves. The faststream...try_it_out import (best-effort ASGI registry wiring) sat outside the try/except meant to tolerate upstream rename/removal, so a module move would have broken import faststream_outbox entirely. The import is now inside the guard.
  • S3 — already resolved in 0.9.0 (recorded for completeness). The processed_total lease-lost double-count was closed by 0.9.0's P17: outcome metrics emit only after a successful flush, so a lease-lost row emits lease_lost instead of a paired acked/nacked.

Diagnostics / test broker

  • S5 — sync-mode batch publish mirrors production ordering. TestOutboxBroker (sync mode) used to dispatch each handler mid-feed — a handler could observe a half-inserted batch (impossible in production) — and emit published after the handlers ran. It now inserts the whole batch → emits published → then dispatches, matching the production order (atomic batch INSERTpublished → subscriber fetch).
  • P27 — subscriber-misconfig warnings point at your call site. The UserWarnings from @broker.subscriber(...) / @router.subscriber(...) misconfigurations used a static stacklevel that was wrong through the FastAPI-router path (it landed on a faststream-internal frame). They now use skip_file_prefixes (3.12+) and are attributed to your decorator line on both the direct and router paths.
  • P29 / P30 — test-broker internals. The four fake publish paths were deduplicated behind two shared helpers (no behavior change). And loop-mode TestOutboxBroker now wakes the fetch loop on feed/publish via the subscriber's _notify_event, mirroring production NOTIFY — so loop-mode tests no longer need a tight min_fetch_interval to be responsive.

Behavior change: validate_schema() is stricter

validate_schema() is opt-in (you call it from a health check / startup hook — it isn't run by broker.start()). If you call it and your outbox table's partial indexes were hand-built or migrated with a predicate that differs from what make_outbox_table declares — most importantly a timer_id_uq that isn't … WHERE timer_id IS NOT NULL — it will now raise where 0.9.0 passed. That's the point: such an index silently breaks publish(..., timer_id=…)'s ON CONFLICT at runtime, and this surfaces it at validation time. The fix is to recreate the index with the correct predicate (regenerate your Alembic migration). If you don't call validate_schema(), nothing changes.

No other behavior change. Producers, subscribers, the lease/terminal-write paths, and the dlq_table=None path are all unchanged.

Touched surface

  • faststream_outbox/subscriber/usecase.py — bounded LISTEN close (S1).
  • faststream_outbox/client.py — partial-index predicate probe in validate_schema (S2).
  • faststream_outbox/__init__.py — guarded try_it_out import (S4).
  • faststream_outbox/subscriber/factory.pyskip_file_prefixes warning attribution (P27).
  • faststream_outbox/testing.py — sync-batch ordering (S5), fake-publish dedup (P29), loop-mode NOTIFY (P30).

See also