Skip to content

Commit 31ccb70

Browse files
Release v1.24.0 (ADR-0037) (#13)
* Leave SQS and ASB poison to the native mechanism when dead-lettering is off (ADR-0037) With dead-lettering off, version-gated and poison bodies on SQS and Azure Service Bus are no longer dropped; they are left to RedrivePolicy or MaxDeliveryCount. AsbTransport gains a narrow Abandoner used only when a version-gate DLQ write fails. Pin conformance v1.8.0 and run the version_gate sections. * Default the SQS dead-letter queue to <queue>-dlq (ADR-0037) SQS queue names cannot contain a dot, so the old <queue>.dlq default could never exist on AWS. The SQS transport now defaults to <queue>-dlq and places any suffix before .fifo; an explicit dead-letter queue still wins. Other transports keep .dlq. * Release v1.24.0
1 parent 372e674 commit 31ccb70

21 files changed

Lines changed: 1733 additions & 56 deletions

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ jobs:
177177
--health-retries 10
178178
env:
179179
# The pinned conformance release (ADR-0032). Bump together with tests/conformance/.
180-
BQ_CONFORMANCE_VERSION: 1.7.0
180+
BQ_CONFORMANCE_VERSION: 1.8.0
181181
# sha256 of that release's fetch.sh: the script runs on the runner, so it is pinned rather
182182
# than trusted from a mutable tag. Bump together with BQ_CONFORMANCE_VERSION.
183183
BQ_FETCH_SH_SHA256: 8bb6ba567b288fb0ac71a70fd17041b2503df119e01998d76fdad372570bf849

‎.github/workflows/release.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
# Recommended for PyPI Trusted Publishing — configure a "pypi" environment +
1515
# a trusted publisher on PyPI (repo BabelQueue/babelqueue-python, this workflow).
1616
environment: pypi
17-
# redis_layout (conformance 1.7.0) drives the real Lua scripts: a real redis:7 is mandatory,
17+
# redis_layout (conformance 1.7.0+) drives the real Lua scripts: a real redis:7 is mandatory,
1818
# exactly as in ci.yml's `conformance` job — without it the cases are reported as failures.
1919
services:
2020
redis:
@@ -28,7 +28,7 @@ jobs:
2828
--health-retries 10
2929
env:
3030
# The pinned conformance release (ADR-0032). Keep identical to ci.yml.
31-
BQ_CONFORMANCE_VERSION: 1.7.0
31+
BQ_CONFORMANCE_VERSION: 1.8.0
3232
BQ_FETCH_SH_SHA256: 8bb6ba567b288fb0ac71a70fd17041b2503df119e01998d76fdad372570bf849
3333
permissions:
3434
id-token: write # PyPI Trusted Publishing (OIDC) — no API token needed

‎CHANGELOG.md‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,71 @@ The envelope wire format is versioned separately by `meta.schema_version`
99

1010
## [Unreleased]
1111

12+
## [1.24.0] - 2026-10-26
13+
14+
Implements conformance **1.8.0** (vendored from the `v1.8.0` release asset, verified with
15+
`fetch.sh --check`; `BQ_CONFORMANCE_VERSION` is now `1.8.0` in `ci.yml` and `release.yml`). The
16+
envelope stays at `schema_version: 1`; nothing here touches the wire (ADR-0037). The new
17+
`version_gate_pulsar`, `version_gate_artemis`, `version_gate_sqs` and `version_gate_asb` sections run
18+
and pass (17 cases each; Python has the `on_unknown_urn` option, so no row is skipped). A MINOR.
19+
20+
### Changed (behaviour)
21+
- **With the DLQ off, SQS and Azure Service Bus no longer drop a version-gated message or retry a
22+
poison body; they are left to the broker's native mechanism (ADR-0037 decision 1 rule 3 and
23+
decision 2 "Go and Python"; broker-bindings 3.5, 4.5, 5.7; owner decision 2026-10-25).**
24+
- *Version-gated message* (an unsupported `bq-schema-version`), `dead_letter` off and a strategy
25+
other than `DELETE` or `DEAD_LETTER`: it used to be acknowledged and dropped. It is now **not**
26+
acknowledged. SQS: no `DeleteMessage`; visibility expiry redelivers it (the gate rejects it
27+
again) and the queue's `RedrivePolicy` moves it to the native DLQ after `maxReceiveCount`. Azure
28+
Service Bus: an explicit native `DeadLetter` (reason `unsupported schema version`) into
29+
`$DeadLetterQueue` instead of `Complete`.
30+
- *Poison body* (invalid JSON or an identity-less object), `dead_letter` off and a strategy other
31+
than `DELETE` or `DEAD_LETTER`: it used to take the bounded `FAIL` path (re-publish an empty
32+
envelope up to `max_attempts`, then drop). On SQS and Azure Service Bus it is now settled
33+
natively **on the first delivery** exactly as above (Azure Service Bus reason `poison`): no
34+
re-publish, no retry, no ack, body unchanged, the event is logged. Every other transport
35+
(Pulsar, Artemis, AMQP, Kafka, Redis, memory) keeps its behaviour; `DELETE` (ack and drop),
36+
`dead_letter=True` and the `DEAD_LETTER` strategy (the raw record, then ack) are unchanged on
37+
SQS and Azure Service Bus too.
38+
- A deployment that relied on SQS/Azure Service Bus silently dropping these messages now needs a
39+
`RedrivePolicy` (SQS) or accepts them in `$DeadLetterQueue` (Azure Service Bus), or sets
40+
`dead_letter=True` / `on_unknown_urn=DELETE`.
41+
42+
### Fixed
43+
- **SQS: the default dead-letter queue name is now `<queue>-dlq` (FIFO `<queue>-dlq.fifo`) (ADR-0037
44+
decision 7).** The old default `<queue>.dlq` was never a valid Amazon SQS queue name (a name may hold only
45+
alphanumerics, hyphens and underscores, plus one `.fifo` suffix), so a DLQ write by default could not find a
46+
queue on real SQS. `dead_letter_suffix` now defaults to `None` ("not set"); unless `dead_letter_queue` is set
47+
the runtime asks the transport's new optional `DeadLetterNamer` capability
48+
(`dead_letter_queue_name(queue, suffix)`, exported from `babelqueue`), which `SqsTransport` implements. The
49+
runtime also hands it an explicit `dead_letter_suffix`; on a FIFO queue the `.fifo` suffix always stays last,
50+
an explicit suffix included: `orders.fifo` with `dead_letter_suffix="-x"` is `orders-x.fifo`, never
51+
`orders.fifo-x` (an SQS FIFO name must end in `.fifo`; before, that explicit suffix produced the uncreatable
52+
`orders.fifo-x`). An explicit queue name behaves as before; every other transport keeps `<queue>.dlq` (the
53+
attribute `App.dead_letter_suffix` is `None` instead of `".dlq"` until set). Deployments
54+
that relied on the SQS default must name the DLQ `<queue>-dlq`, or pass `dead_letter_suffix=".dlq"` /
55+
`dead_letter_queue=` to keep the old name. An explicitly empty `dead_letter_suffix=""` now counts as "no suffix" on SQS (`-dlq`); before, it made the DLQ the source queue itself.
56+
57+
### Added
58+
- **`babelqueue.transport.NativeSettler`**, an optional `Transport` capability
59+
(`settle_native(message, reason, description)`). With the DLQ off the runtime calls it instead of
60+
acknowledging a version-gated message (and, on the poison route, instead of the bounded retry) on
61+
a transport that offers it; the decision follows the capability, never the transport's name.
62+
`SqsTransport.settle_native` is a deliberate no-op on the wire; `AsbTransport.settle_native`
63+
dead-letters natively (falling back to `abandon_message` for a receiver without
64+
`dead_letter_message`). Transports without it are unchanged.
65+
- **`babelqueue.transport.Abandoner` and `AsbTransport.abandon`.** When the dead-letter write of a
66+
*version-gated* message fails, Azure Service Bus now `Abandon`s the peek-lock (broker-bindings 4.5,
67+
"A failed write `Abandon`s it") so the broker redelivers it at once, instead of leaving it locked until
68+
the lock lapses (up to 5 minutes). Only that branch abandons (owner decision 2026-10-25): `AsbTransport`
69+
is deliberately **not** a `Rewinder`, so a graceful-shutdown release and a failed retry re-publish keep
70+
their previous handling (a copy is re-published and the original completed), and a failed dead-letter
71+
write of a poison body is still left to its lock. Tests pin all three. The `version_gate_asb` runner
72+
asserts the `Abandon` on its `unsettled` rows.
73+
- Conformance `version_gate_*` runners (`version_gate_pulsar` / `version_gate_artemis` through the
74+
runtime, `version_gate_sqs` / `version_gate_asb` through the real transports) and their entries in
75+
the results writer.
76+
1277
## [1.23.0] - 2026-10-25
1378

1479
Implements ADR-0036 (post-R1 parity) decisions 2, 4, 5 and 6. A MINOR: every change is a

‎README.md‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,13 @@ untrimmed; Azure Service Bus and Artemis also accept the integer `1`). A message
158158
anything else is not decoded: it reaches the runtime on the reserved route
159159
`babelqueue:unsupported-schema-version:<v>` (the version percent-escaped) and is **never
160160
retried** — it goes straight to the dead-letter queue (the incoming body, byte-identical,
161-
with no `dead_letter` block) when `dead_letter` is enabled, and is dropped otherwise, whatever `max_attempts` is.
162-
It does not depend on a broker `RedrivePolicy`. If the dead-letter write fails the message is
161+
with no `dead_letter` block) when `dead_letter` is enabled (or `on_unknown_urn=DEAD_LETTER`), and
162+
`on_unknown_urn=DELETE` drops it, whatever `max_attempts` is. With `dead_letter` off, **SQS and Azure
163+
Service Bus no longer drop it** (ADR-0037): it is left to the broker's native mechanism and logged
164+
(SQS: no `DeleteMessage`, so visibility expiry redelivers it — the gate rejects it again — and the
165+
queue's `RedrivePolicy` moves it to the native DLQ after `maxReceiveCount`; Azure Service Bus: an
166+
explicit native `DeadLetter` with reason `unsupported schema version` into `$DeadLetterQueue`
167+
instead of `Complete`). Kafka, Artemis and Pulsar still acknowledge and drop it. If the dead-letter write fails the message is
163168
left unacknowledged (an error is logged), is never retried and never reaches a handler; the
164169
dead-letter write is attempted again when the broker brings it back, which depends on the
165170
broker:
@@ -199,10 +204,17 @@ a warning. A **per-message delay** at publish time is
199204
standard queue; a larger value, or any per-message delay on a FIFO queue, raises
200205
`BabelQueueError` instead of being truncated or dropped).
201206

207+
**Poison bodies.** A body that does not decode, or has no identity, is never retried by the SDK
208+
(ADR-0037): `dead_letter=True` writes the ADR-0035 record to the DLQ (`<queue>-dlq` on SQS, `<queue>.dlq` elsewhere) and then deletes it,
209+
`on_unknown_urn=DELETE` deletes it, and with the DLQ off it is **not deleted and not re-sent**: the
210+
transport's `settle_native` capability leaves it to SQS on the first delivery (visibility expiry,
211+
then the queue's `RedrivePolicy`; the event is logged). Azure Service Bus does the same with a native
212+
`DeadLetter` (reason `poison`). Every other transport keeps the bounded retry up to `max_attempts`.
213+
202214
**Poison-loop risk:** a message that always fails is redelivered at once until
203215
`max_attempts` is reached. If the receive count is unavailable the SDK cannot
204216
advance attempts at all. **Configure a native `RedrivePolicy`**
205-
(`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue>.dlq`) on every SQS
217+
(`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue>-dlq`, the SQS default DLQ name: FIFO `<queue>-dlq.fifo`, because an SQS queue name cannot contain a dot; `dead_letter_queue` overrides it, and `dead_letter_suffix` replaces `-dlq`, placed before `.fifo` on a FIFO queue) on every SQS
206218
queue as the broker-side backstop, and set a non-zero `retry_backoff` if the
207219
handler's dependencies need time to recover.
208220

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "babelqueue"
7-
version = "1.23.0"
7+
version = "1.24.0"
88
description = "Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers."
99
readme = "README.md"
1010
requires-python = ">=3.9"

‎src/babelqueue/__init__.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@
3030
from .replay import HEADER_REPLAY_BYPASS, bypass_external_effects, is_replay
3131
from .routing import UnknownUrnStrategy
3232
from .transport import (
33+
Abandoner,
3334
BatchPopper,
35+
DeadLetterNamer,
3436
DeadLetterWriter,
3537
HeaderPublisher,
3638
InMemoryTransport,
39+
NativeSettler,
3740
ReceivedMessage,
3841
Redeliverer,
3942
Rewinder,
@@ -42,7 +45,7 @@
4245
Transport,
4346
)
4447

45-
__version__ = "1.23.0"
48+
__version__ = "1.24.0"
4649

4750
__all__ = [
4851
"BabelQueue",
@@ -55,9 +58,12 @@
5558
"Transport",
5659
"InMemoryTransport",
5760
"ReceivedMessage",
61+
"Abandoner",
5862
"BatchPopper",
63+
"DeadLetterNamer",
5964
"DeadLetterWriter",
6065
"HeaderPublisher",
66+
"NativeSettler",
6167
"Redeliverer",
6268
"Rewinder",
6369
"RoutedPublisher",

0 commit comments

Comments
 (0)