feat(asyncapi): one channel per queue; lift the fastapi cap - #184
Merged
Merged
Conversation
Two independent changes that both fell out of the faststream 0.7.6 bump. AsyncAPI channels, closing #181. 0.7.6 reworked the specification model around `channel_labels` (one label per channel) and a required `SubscriberSpec.address`. The subscriber kept filing every queue under a single channel keyed by the joined queue list, which forced a synthetic address: `"orders,shipments"` names nothing a consumer can subscribe to. Dropping the `name` override in favour of the base's `_channel_key` also fixes `title_`, which is public and documented but named only the operation, unlike the publisher and every built-in broker. The fastapi cap. It existed because fastapi 0.140 made `Dependant` a slotted dataclass and broke faststream's integration (ag2ai/faststream#2959). That issue closed completed on 2026-07-28 and the fix shipped no later than faststream 0.7.4, which is below the 0.7.6 floor this package already requires, so the pairing can no longer regress within the supported range. Verified against fastapi 0.141.1. `CONTEXT.md` reserves *channel* for the LISTEN/NOTIFY channel, so it now records the one place upstream's vocabulary overrides ours.
Benchmark gate✅ gate passed
Gated (fails the build): |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #181.
Two independent changes, bundled by request. Either can be dropped without touching the other: they
share no files except
pyproject.tomland the second is four lines.1. AsyncAPI: one channel per queue (#181)
faststream 0.7.6 reworked the specification model around
channel_labels(one label per channel)and a required
SubscriberSpec.address, which lands inchannels.<key>.address. The subscriberkept filing every queue under a single channel keyed by the joined queue list, so the address came
out synthetic.
For
@broker.subscriber(["orders", "shipments"]):Dropping the
nameoverride in favour of the base's_channel_keyalso fixestitle_, which is apublic, documented kwarg that named only the operation:
orders:HandleOrderIngestOrderIngestOrderIngestIngest:orders,Ingest:refundsQueues are deduped through a dict rather than a set, so channel order does not vary per process.
Why one channel per queue, and not one per subscriber
Forced by the spec, not a style preference. From the official JSON Schema
(spec-json-schemas/schemas/3.0.0.json):
addressis a string or null, never an array; 3.0.0describes it as "the 'topic name', 'routing key', 'event type', or 'path'", and
nullor absent"MUST be interpreted as unknown".
channelis a single$refand it is required. Nothinglets one operation span several.
A handler draining several queues therefore has no single-channel form that keeps every queue name.
The alternatives are to drop all but one queue, or set
address: nulland publish "unknown", bothof which lose more than the split. Address expressions do not help: they are
{param}substitutionfor dynamic segments, not lists.
The joined form was not invalid. Validated against the official schema, the old and new
documents both pass with zero errors. The old one was schema-valid and semantically false: it
asserted an address,
"orders,shipments", that no consumer can resolve and no reader can tell wasmeant as two. That is the worse failure mode, because tooling accepts it in silence. This PR is not
fixing a validation error; it is fixing a document that lies.
What the split does lose. AsyncAPI 3.0 cannot say "these operations are served by one consumer
sharing a lease and a single fetch", so two queues now read as two independent subscriptions. That
fidelity is genuinely gone and nothing standard recovers it:
operation.tagscould group them, butno tooling reads a tag as "same consumer", so it would be decoration. Every alternative loses more.
Queue is the right granularity. The spec documents
addressas the topic name or routing key,which is exactly what the
queuecolumn is: what a publisher addresses and a subscriber filters on.The outbox table is closer to a server than a channel, and is already modelled as the server URL.
The built-ins agree: Kafka keys on the topic, RabbitMQ on the queue and exchange, Redis on the
channel or stream name.
Checked against both output versions faststream supports, 2.6.0 and 3.0.0.
Scope
Document-shape only.
.nameand.get_schema()have no consumers outside AsyncAPI generation;nothing in logging, metrics, or dispatch reads them. No runtime behaviour moves.
Breaking for consumers of the generated document, specifically multi-queue and titled
subscribers. Worth a release note.
2. Lift the
fastapi <0.140capThe cap existed because fastapi 0.140 made
Dependanta slotted dataclass and broke faststream'sFastAPI integration (ag2ai/faststream#2959). That
issue closed as completed on 2026-07-28; faststream 0.7.4 shipped 2026-08-07, so the fix is at or
below 0.7.4 and therefore below the
>=0.7.6floor #182 established. The pairing can no longerregress anywhere in the supported range, which is what the cap was protecting, so the cap and its
note go.
Verified by resolving to fastapi 0.141.1 and running
tests/test_fastapi.py: 10 passed.Vocabulary
CONTEXT.mdreserves channel for theoutbox_<table>LISTEN/NOTIFY channel and lists it underavoid.
channel_labelsis upstream's name and cannot be renamed, soCONTEXT.mdnow records theexception rather than leaving the next reader to trip on it.
Verification
Run against Postgres 17 via compose, so
test_integration.pywas not skipped.eof-fixer --check,ruff format --check,ruff check --no-fix,ty checkcleanmkdocs build --strictcleanpython -m benchmarks check: gate OK