babelqueue 1.23.0__tar.gz → 1.24.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {babelqueue-1.23.0 → babelqueue-1.24.0}/.github/workflows/ci.yml +1 -1
- {babelqueue-1.23.0 → babelqueue-1.24.0}/.github/workflows/release.yml +2 -2
- {babelqueue-1.23.0 → babelqueue-1.24.0}/CHANGELOG.md +65 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/PKG-INFO +16 -4
- {babelqueue-1.23.0 → babelqueue-1.24.0}/README.md +15 -3
- {babelqueue-1.23.0 → babelqueue-1.24.0}/pyproject.toml +1 -1
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/__init__.py +7 -1
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/app.py +88 -10
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/asb_transport.py +37 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/sqs_transport.py +29 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/transport.py +53 -0
- babelqueue-1.24.0/tests/conformance/CONFORMANCE_VERSION +1 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/manifest.json +705 -1
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/schema/results.example.json +1 -1
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_app.py +114 -0
- babelqueue-1.24.0/tests/test_asb_conformance.py +177 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_asb_transport.py +148 -1
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_behaviour_conformance.py +108 -4
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_conformance.py +4 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_poison_routing.py +85 -15
- babelqueue-1.24.0/tests/test_sqs_conformance.py +155 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_sqs_transport.py +99 -13
- babelqueue-1.23.0/tests/conformance/CONFORMANCE_VERSION +0 -1
- babelqueue-1.23.0/tests/test_asb_conformance.py +0 -84
- babelqueue-1.23.0/tests/test_sqs_conformance.py +0 -80
- {babelqueue-1.23.0 → babelqueue-1.24.0}/.github/FUNDING.yml +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/.github/dependabot.yml +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/.gitignore +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/LICENSE +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/conformance-known-gaps.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/scripts/check_conformance_results.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/_envelope_shape.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/_poison_record.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/artemis_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/celery.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/codec.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/contracts.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/dead_letter.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/django/__init__.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/django/apps.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/django/management/__init__.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/exceptions.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/gdpr.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/headers.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/idempotency.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/kafka_retry.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/kafka_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/otel.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/outbox.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/pika_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/pulsar_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/py.typed +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/redis_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/redrive.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/replay.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/routing.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/src/babelqueue/schema.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/blank-ascii-job-urn-fallback.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/data-array-rejected.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/dead-letter-roundtrip.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/duplicate-keys-last-wins.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/empty-data-roundtrip.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-job-and-urn.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-job-nonascii-space-and-urn.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-meta-attempts.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-meta-source.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-meta-ts.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/forbidden-key-timestamp.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/invalid-wrong-case-job.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-array-urn-fallback.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-bool-urn-fallback.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-empty-string-urn-fallback.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-missing-job-array-urn.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-null-no-urn.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-null-urn-fallback.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-number-blank-urn.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-number-nonstring-urn.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-number-urn-fallback.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/job-type-object-urn-fallback.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/meta-ext-vendor-roundtrip.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/order-created.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-cr.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-ff.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-lf.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-space.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-tab.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-job-vt.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-cr.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-ff.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-lf.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-space.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-tab.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-blank-trace-id-vt.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-job-and-urn-both-blank.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-reemit-job-ascii-padded.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-reemit-job-unicode-padded.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-reemit-trace-id-padded.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-unicode-job-emsp.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-unicode-job-ideographic.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-unicode-job-nbsp.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-urn-only-ascii-blank.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/trim-urn-only-unicode-space.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/unknown-lang.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/unknown-meta-roundtrip.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/urn-alias.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/urn-only-roundtrip.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/fixtures/wrong-case-keys-roundtrip.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/conformance/schema/results.schema.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/fakes/confluent_kafka/__init__.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/fakes/confluent_kafka/admin.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/fixtures/order-created.json +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/redis_backend.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_artemis_conformance.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_artemis_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_celery.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_codec.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_dead_letter.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_django.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_gdpr.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_headers.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_idempotency.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_kafka_conformance.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_kafka_integration.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_kafka_integration_logic.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_kafka_retry.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_kafka_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_otel.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_outbox.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_overhead.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_pika_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_pulsar_conformance.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_pulsar_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_redis_transport.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_redrive.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_reencode_regression.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_replay.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_schema.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_schema_conformance.py +0 -0
- {babelqueue-1.23.0 → babelqueue-1.24.0}/tests/test_shutdown.py +0 -0
|
@@ -177,7 +177,7 @@ jobs:
|
|
|
177
177
|
--health-retries 10
|
|
178
178
|
env:
|
|
179
179
|
# The pinned conformance release (ADR-0032). Bump together with tests/conformance/.
|
|
180
|
-
BQ_CONFORMANCE_VERSION: 1.
|
|
180
|
+
BQ_CONFORMANCE_VERSION: 1.8.0
|
|
181
181
|
# sha256 of that release's fetch.sh: the script runs on the runner, so it is pinned rather
|
|
182
182
|
# than trusted from a mutable tag. Bump together with BQ_CONFORMANCE_VERSION.
|
|
183
183
|
BQ_FETCH_SH_SHA256: 8bb6ba567b288fb0ac71a70fd17041b2503df119e01998d76fdad372570bf849
|
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
# Recommended for PyPI Trusted Publishing — configure a "pypi" environment +
|
|
15
15
|
# a trusted publisher on PyPI (repo BabelQueue/babelqueue-python, this workflow).
|
|
16
16
|
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,
|
|
18
18
|
# exactly as in ci.yml's `conformance` job — without it the cases are reported as failures.
|
|
19
19
|
services:
|
|
20
20
|
redis:
|
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
--health-retries 10
|
|
29
29
|
env:
|
|
30
30
|
# The pinned conformance release (ADR-0032). Keep identical to ci.yml.
|
|
31
|
-
BQ_CONFORMANCE_VERSION: 1.
|
|
31
|
+
BQ_CONFORMANCE_VERSION: 1.8.0
|
|
32
32
|
BQ_FETCH_SH_SHA256: 8bb6ba567b288fb0ac71a70fd17041b2503df119e01998d76fdad372570bf849
|
|
33
33
|
permissions:
|
|
34
34
|
id-token: write # PyPI Trusted Publishing (OIDC) — no API token needed
|
|
@@ -9,6 +9,71 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
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
|
+
|
|
12
77
|
## [1.23.0] - 2026-10-25
|
|
13
78
|
|
|
14
79
|
Implements ADR-0036 (post-R1 parity) decisions 2, 4, 5 and 6. A MINOR: every change is a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: babelqueue
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.24.0
|
|
4
4
|
Summary: Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers.
|
|
5
5
|
Project-URL: Homepage, https://babelqueue.com
|
|
6
6
|
Project-URL: Source, https://github.com/BabelQueue/babelqueue-python
|
|
@@ -212,8 +212,13 @@ untrimmed; Azure Service Bus and Artemis also accept the integer `1`). A message
|
|
|
212
212
|
anything else is not decoded: it reaches the runtime on the reserved route
|
|
213
213
|
`babelqueue:unsupported-schema-version:<v>` (the version percent-escaped) and is **never
|
|
214
214
|
retried** — it goes straight to the dead-letter queue (the incoming body, byte-identical,
|
|
215
|
-
with no `dead_letter` block) when `dead_letter` is enabled
|
|
216
|
-
|
|
215
|
+
with no `dead_letter` block) when `dead_letter` is enabled (or `on_unknown_urn=DEAD_LETTER`), and
|
|
216
|
+
`on_unknown_urn=DELETE` drops it, whatever `max_attempts` is. With `dead_letter` off, **SQS and Azure
|
|
217
|
+
Service Bus no longer drop it** (ADR-0037): it is left to the broker's native mechanism and logged
|
|
218
|
+
(SQS: no `DeleteMessage`, so visibility expiry redelivers it — the gate rejects it again — and the
|
|
219
|
+
queue's `RedrivePolicy` moves it to the native DLQ after `maxReceiveCount`; Azure Service Bus: an
|
|
220
|
+
explicit native `DeadLetter` with reason `unsupported schema version` into `$DeadLetterQueue`
|
|
221
|
+
instead of `Complete`). Kafka, Artemis and Pulsar still acknowledge and drop it. If the dead-letter write fails the message is
|
|
217
222
|
left unacknowledged (an error is logged), is never retried and never reaches a handler; the
|
|
218
223
|
dead-letter write is attempted again when the broker brings it back, which depends on the
|
|
219
224
|
broker:
|
|
@@ -253,10 +258,17 @@ a warning. A **per-message delay** at publish time is
|
|
|
253
258
|
standard queue; a larger value, or any per-message delay on a FIFO queue, raises
|
|
254
259
|
`BabelQueueError` instead of being truncated or dropped).
|
|
255
260
|
|
|
261
|
+
**Poison bodies.** A body that does not decode, or has no identity, is never retried by the SDK
|
|
262
|
+
(ADR-0037): `dead_letter=True` writes the ADR-0035 record to the DLQ (`<queue>-dlq` on SQS, `<queue>.dlq` elsewhere) and then deletes it,
|
|
263
|
+
`on_unknown_urn=DELETE` deletes it, and with the DLQ off it is **not deleted and not re-sent**: the
|
|
264
|
+
transport's `settle_native` capability leaves it to SQS on the first delivery (visibility expiry,
|
|
265
|
+
then the queue's `RedrivePolicy`; the event is logged). Azure Service Bus does the same with a native
|
|
266
|
+
`DeadLetter` (reason `poison`). Every other transport keeps the bounded retry up to `max_attempts`.
|
|
267
|
+
|
|
256
268
|
**Poison-loop risk:** a message that always fails is redelivered at once until
|
|
257
269
|
`max_attempts` is reached. If the receive count is unavailable the SDK cannot
|
|
258
270
|
advance attempts at all. **Configure a native `RedrivePolicy`**
|
|
259
|
-
(`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue
|
|
271
|
+
(`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
|
|
260
272
|
queue as the broker-side backstop, and set a non-zero `retry_backoff` if the
|
|
261
273
|
handler's dependencies need time to recover.
|
|
262
274
|
|
|
@@ -158,8 +158,13 @@ untrimmed; Azure Service Bus and Artemis also accept the integer `1`). A message
|
|
|
158
158
|
anything else is not decoded: it reaches the runtime on the reserved route
|
|
159
159
|
`babelqueue:unsupported-schema-version:<v>` (the version percent-escaped) and is **never
|
|
160
160
|
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
|
|
162
|
-
|
|
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
|
|
163
168
|
left unacknowledged (an error is logged), is never retried and never reaches a handler; the
|
|
164
169
|
dead-letter write is attempted again when the broker brings it back, which depends on the
|
|
165
170
|
broker:
|
|
@@ -199,10 +204,17 @@ a warning. A **per-message delay** at publish time is
|
|
|
199
204
|
standard queue; a larger value, or any per-message delay on a FIFO queue, raises
|
|
200
205
|
`BabelQueueError` instead of being truncated or dropped).
|
|
201
206
|
|
|
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
|
+
|
|
202
214
|
**Poison-loop risk:** a message that always fails is redelivered at once until
|
|
203
215
|
`max_attempts` is reached. If the receive count is unavailable the SDK cannot
|
|
204
216
|
advance attempts at all. **Configure a native `RedrivePolicy`**
|
|
205
|
-
(`maxReceiveCount` ≥ `max_attempts`, pointing at `<queue
|
|
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
|
|
206
218
|
queue as the broker-side backstop, and set a non-zero `retry_backoff` if the
|
|
207
219
|
handler's dependencies need time to recover.
|
|
208
220
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "babelqueue"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.24.0"
|
|
8
8
|
description = "Polyglot Queues, Simplified — the Python core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -30,10 +30,13 @@ from .exceptions import BabelQueueError, DecryptError, UnknownUrnError
|
|
|
30
30
|
from .replay import HEADER_REPLAY_BYPASS, bypass_external_effects, is_replay
|
|
31
31
|
from .routing import UnknownUrnStrategy
|
|
32
32
|
from .transport import (
|
|
33
|
+
Abandoner,
|
|
33
34
|
BatchPopper,
|
|
35
|
+
DeadLetterNamer,
|
|
34
36
|
DeadLetterWriter,
|
|
35
37
|
HeaderPublisher,
|
|
36
38
|
InMemoryTransport,
|
|
39
|
+
NativeSettler,
|
|
37
40
|
ReceivedMessage,
|
|
38
41
|
Redeliverer,
|
|
39
42
|
Rewinder,
|
|
@@ -42,7 +45,7 @@ from .transport import (
|
|
|
42
45
|
Transport,
|
|
43
46
|
)
|
|
44
47
|
|
|
45
|
-
__version__ = "1.
|
|
48
|
+
__version__ = "1.24.0"
|
|
46
49
|
|
|
47
50
|
__all__ = [
|
|
48
51
|
"BabelQueue",
|
|
@@ -55,9 +58,12 @@ __all__ = [
|
|
|
55
58
|
"Transport",
|
|
56
59
|
"InMemoryTransport",
|
|
57
60
|
"ReceivedMessage",
|
|
61
|
+
"Abandoner",
|
|
58
62
|
"BatchPopper",
|
|
63
|
+
"DeadLetterNamer",
|
|
59
64
|
"DeadLetterWriter",
|
|
60
65
|
"HeaderPublisher",
|
|
66
|
+
"NativeSettler",
|
|
61
67
|
"Redeliverer",
|
|
62
68
|
"Rewinder",
|
|
63
69
|
"RoutedPublisher",
|
|
@@ -43,8 +43,11 @@ from .replay import HEADER_REPLAY_BYPASS, _replay_scope
|
|
|
43
43
|
from .routing import UnknownUrnStrategy
|
|
44
44
|
from .transport import (
|
|
45
45
|
NON_NUMERIC_ATTEMPTS_ROUTE,
|
|
46
|
+
Abandoner,
|
|
47
|
+
DeadLetterNamer,
|
|
46
48
|
DeadLetterWriter,
|
|
47
49
|
HeaderPublisher,
|
|
50
|
+
NativeSettler,
|
|
48
51
|
PoisonRoute,
|
|
49
52
|
ReceivedMessage,
|
|
50
53
|
Redeliverer,
|
|
@@ -109,7 +112,7 @@ class BabelQueue:
|
|
|
109
112
|
max_attempts: int = 3,
|
|
110
113
|
dead_letter: bool = False,
|
|
111
114
|
dead_letter_queue: Optional[str] = None,
|
|
112
|
-
dead_letter_suffix: str =
|
|
115
|
+
dead_letter_suffix: Optional[str] = None,
|
|
113
116
|
retry_backoff: float = 0.0,
|
|
114
117
|
retry_backoff_multiplier: float = 1.0,
|
|
115
118
|
retry_backoff_max: float = 0.0,
|
|
@@ -143,6 +146,8 @@ class BabelQueue:
|
|
|
143
146
|
self.max_attempts = max_attempts
|
|
144
147
|
self.dead_letter_enabled = bool(dead_letter)
|
|
145
148
|
self.dead_letter_queue = dead_letter_queue
|
|
149
|
+
#: ``None`` = not set: the transport's :class:`~babelqueue.transport.DeadLetterNamer` default
|
|
150
|
+
#: (SQS: ``<queue>-dlq``) or else ``.dlq``. An explicit value is handed to the namer.
|
|
146
151
|
self.dead_letter_suffix = dead_letter_suffix
|
|
147
152
|
self.retry_backoff = float(retry_backoff)
|
|
148
153
|
if retry_jitter not in RETRY_JITTERS:
|
|
@@ -383,7 +388,10 @@ class BabelQueue:
|
|
|
383
388
|
received.queue,
|
|
384
389
|
exc_info=exc,
|
|
385
390
|
)
|
|
386
|
-
|
|
391
|
+
if received.route_urn.startswith(_UNSUPPORTED_SCHEMA_ROUTE_PREFIX):
|
|
392
|
+
self._abandon_unsettled(received)
|
|
393
|
+
else:
|
|
394
|
+
self._rewind_unsettled(received)
|
|
387
395
|
return
|
|
388
396
|
self._retry_or_dead_letter(received, envelope, exc)
|
|
389
397
|
else:
|
|
@@ -528,6 +536,49 @@ class BabelQueue:
|
|
|
528
536
|
exc_info=True,
|
|
529
537
|
)
|
|
530
538
|
|
|
539
|
+
def _settle_native(self, received: ReceivedMessage, reason: str, description: str) -> bool:
|
|
540
|
+
"""Hand ``received`` to the transport's :class:`~babelqueue.transport.NativeSettler`
|
|
541
|
+
capability and report whether it did: ``False`` means the transport has none and the caller
|
|
542
|
+
keeps its previous settle. The event is logged and never retried by the SDK; a failing call
|
|
543
|
+
is logged too and leaves the message unacknowledged (the broker redelivers it)."""
|
|
544
|
+
if not isinstance(self.transport, NativeSettler):
|
|
545
|
+
return False
|
|
546
|
+
logger.warning(
|
|
547
|
+
"Left to the broker's native mechanism (not acknowledged, not retried): queue %r, "
|
|
548
|
+
"route %r, reason %s.",
|
|
549
|
+
received.queue, received.route_urn, reason,
|
|
550
|
+
)
|
|
551
|
+
try:
|
|
552
|
+
self.transport.settle_native(received, reason, description)
|
|
553
|
+
except Exception: # noqa: BLE001 - reported, never kills the loop
|
|
554
|
+
logger.error(
|
|
555
|
+
"Native settle of a message on queue %r failed; it stays unacknowledged and will "
|
|
556
|
+
"be redelivered by the broker.",
|
|
557
|
+
received.queue,
|
|
558
|
+
exc_info=True,
|
|
559
|
+
)
|
|
560
|
+
return True
|
|
561
|
+
|
|
562
|
+
def _abandon_unsettled(self, received: ReceivedMessage) -> None:
|
|
563
|
+
"""A version-gated delivery whose dead-letter write failed: a :class:`Rewinder` transport is
|
|
564
|
+
rewound (:meth:`_rewind_unsettled`); an :class:`~babelqueue.transport.Abandoner` transport
|
|
565
|
+
(Azure Service Bus) abandons its lock so the broker redelivers it at once, after the same
|
|
566
|
+
short pause. Any other transport leaves it to its lock or visibility timeout."""
|
|
567
|
+
if isinstance(self.transport, Rewinder) or not isinstance(self.transport, Abandoner):
|
|
568
|
+
self._rewind_unsettled(received)
|
|
569
|
+
return
|
|
570
|
+
try:
|
|
571
|
+
self.transport.abandon(received)
|
|
572
|
+
except Exception: # noqa: BLE001 - reported, never kills the loop
|
|
573
|
+
logger.error(
|
|
574
|
+
"Failed to abandon an unsettled version-gated message (route %r) on queue %r.",
|
|
575
|
+
received.route_urn,
|
|
576
|
+
received.queue,
|
|
577
|
+
exc_info=True,
|
|
578
|
+
)
|
|
579
|
+
return
|
|
580
|
+
self._stop.wait(self._rewind_pause)
|
|
581
|
+
|
|
531
582
|
def _rewind_unsettled(self, received: ReceivedMessage) -> None:
|
|
532
583
|
"""Make a deliberately unsettled message come back (cumulative-offset brokers such as Kafka
|
|
533
584
|
would otherwise lose it when a later record on the partition is committed), after a short
|
|
@@ -717,10 +768,15 @@ class BabelQueue:
|
|
|
717
768
|
* with the DLQ on (``dead_letter`` or the DEAD_LETTER strategy) it is written to the DLQ as
|
|
718
769
|
the ADR-0035 record with ``reason: "poison"`` (original body preserved) and acknowledged
|
|
719
770
|
after the write, with no retry;
|
|
720
|
-
* with the DLQ off
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
771
|
+
* with the DLQ off, on a transport with a native mechanism (SQS, Azure Service Bus: the
|
|
772
|
+
:class:`~babelqueue.transport.NativeSettler` capability) it is settled natively on the first
|
|
773
|
+
delivery -- not acknowledged, not re-published, not retried, body unchanged (ADR-0037
|
|
774
|
+
decision 2);
|
|
775
|
+
* with the DLQ off on any other transport it takes the bounded FAIL path (``RELEASE`` is
|
|
776
|
+
replaced by it, a poison body would otherwise be released forever): re-published with
|
|
777
|
+
``attempts + 1`` up to ``max_attempts``, then dropped. The count is the larger of the
|
|
778
|
+
transport's own delivery count and the ``attempts`` of the bare body a previous retry left
|
|
779
|
+
behind.
|
|
724
780
|
|
|
725
781
|
A failed DLQ write propagates: :meth:`dispatch` leaves the delivery unacknowledged."""
|
|
726
782
|
if self.on_unknown_urn == UnknownUrnStrategy.DELETE:
|
|
@@ -734,6 +790,11 @@ class BabelQueue:
|
|
|
734
790
|
if self.dead_letter_enabled or self.on_unknown_urn == UnknownUrnStrategy.DEAD_LETTER:
|
|
735
791
|
self._settle_transport_poison(received, cause, attempts)
|
|
736
792
|
return
|
|
793
|
+
# DLQ off, strategy neither delete nor a writable dead_letter: a transport with a native
|
|
794
|
+
# mechanism (SQS, Azure Service Bus) settles the poison body natively on the first delivery
|
|
795
|
+
# (ADR-0037 decision 2, "Go and Python") instead of the bounded retry below.
|
|
796
|
+
if self._settle_native(received, "poison", "the body does not decode into a valid envelope: " + cause):
|
|
797
|
+
return
|
|
737
798
|
self._retry_or_dead_letter(
|
|
738
799
|
received,
|
|
739
800
|
{"attempts": attempts},
|
|
@@ -779,11 +840,20 @@ class BabelQueue:
|
|
|
779
840
|
)
|
|
780
841
|
|
|
781
842
|
def _dead_letter_target(self, received: ReceivedMessage) -> str:
|
|
782
|
-
"""The queue a dead-lettered delivery is written to
|
|
783
|
-
|
|
843
|
+
"""The queue a dead-lettered delivery is written to: the configured ``dead_letter_queue``, else
|
|
844
|
+
the transport's :class:`~babelqueue.transport.DeadLetterNamer` name (SQS: ``<queue>-dlq``, FIFO
|
|
845
|
+
``<queue>-dlq.fifo``, an explicit ``dead_letter_suffix`` placed before ``.fifo``; ADR-0037
|
|
846
|
+
decision 7), else ``<queue><dead_letter_suffix>`` (default ``.dlq``)."""
|
|
847
|
+
if self.dead_letter_queue:
|
|
848
|
+
return self.dead_letter_queue
|
|
849
|
+
if isinstance(self.transport, DeadLetterNamer):
|
|
850
|
+
return self.transport.dead_letter_queue_name(received.queue, self.dead_letter_suffix)
|
|
851
|
+
return received.queue + (".dlq" if self.dead_letter_suffix is None else self.dead_letter_suffix)
|
|
784
852
|
|
|
785
853
|
def _settle_version_gated(self, received: ReceivedMessage, always: bool = False) -> None:
|
|
786
|
-
"""Dead-letter a version-gated delivery directly (no retry)
|
|
854
|
+
"""Dead-letter a version-gated delivery directly (no retry). With no DLQ set it is left to the
|
|
855
|
+
transport's native mechanism when the transport has the :class:`~babelqueue.transport.NativeSettler`
|
|
856
|
+
capability (SQS, Azure Service Bus; ADR-0037 decision 1 rule 3) and dropped otherwise.
|
|
787
857
|
|
|
788
858
|
``always`` writes the DLQ record in every configuration, ignoring ``dead_letter_enabled``.
|
|
789
859
|
A Kafka poison record reaches it with the DLQ already on (:meth:`_route_poison`). The Redis
|
|
@@ -798,7 +868,15 @@ class BabelQueue:
|
|
|
798
868
|
if always or self.dead_letter_enabled or self.on_unknown_urn == UnknownUrnStrategy.DEAD_LETTER:
|
|
799
869
|
self._write_dead_letter(received, self._dead_letter_target(received), received.body)
|
|
800
870
|
return
|
|
801
|
-
# No DLQ to hold it
|
|
871
|
+
# No DLQ to hold it (broker-bindings §5.7 rule 3). A transport with a native mechanism (SQS,
|
|
872
|
+
# Azure Service Bus) leaves it to that mechanism instead of dropping it (ADR-0037 decision 1).
|
|
873
|
+
if self._settle_native(
|
|
874
|
+
received,
|
|
875
|
+
"unsupported schema version",
|
|
876
|
+
"the message declares a schema version this consumer does not support",
|
|
877
|
+
):
|
|
878
|
+
return
|
|
879
|
+
# Any other transport: the drop is deliberate, but it must not be silent.
|
|
802
880
|
logger.warning(
|
|
803
881
|
"Dropping an undeliverable message on queue %r (route %r, %d body characters): it can "
|
|
804
882
|
"never be handled and no dead-letter queue is configured.",
|
|
@@ -12,6 +12,10 @@ Consuming uses the PeekLock reservation model (``receive_messages`` -> process -
|
|
|
12
12
|
``DeliveryCount`` (1-based), reconciled onto the envelope as ``attempts = DeliveryCount - 1``.
|
|
13
13
|
A message left un-acked has its lock expire and is redelivered (at-least-once).
|
|
14
14
|
|
|
15
|
+
A version-gated message (``bq-schema-version`` the consumer does not support) or a poison body is, with
|
|
16
|
+
dead-lettering off, not completed: :meth:`AsbTransport.settle_native` dead-letters it natively into
|
|
17
|
+
``$DeadLetterQueue`` (ADR-0037).
|
|
18
|
+
|
|
15
19
|
This implements §4 of the broker-bindings contract. The envelope is unchanged
|
|
16
20
|
(``schema_version`` stays 1); Azure Service Bus is purely additive.
|
|
17
21
|
|
|
@@ -269,6 +273,39 @@ class AsbTransport(Transport):
|
|
|
269
273
|
return
|
|
270
274
|
self._receiver(message.queue).complete_message(message.handle)
|
|
271
275
|
|
|
276
|
+
def abandon(self, message: ReceivedMessage) -> None:
|
|
277
|
+
""":class:`~babelqueue.transport.Abandoner`: abandon the peek-lock on ``message`` so the
|
|
278
|
+
broker redelivers it right away (``AbandonMessage``) instead of when the lock lapses.
|
|
279
|
+
Nothing is completed or dead-lettered and the body is untouched. The runtime calls it only
|
|
280
|
+
when the dead-letter write of a version-gated message failed (broker-bindings 4.5, "A failed
|
|
281
|
+
write ``Abandon``s it"). This transport is deliberately **not** a
|
|
282
|
+
:class:`~babelqueue.transport.Rewinder`, so a graceful-shutdown release and a failed retry
|
|
283
|
+
re-publish keep their previous handling (copy re-publish and ``Complete``). A message
|
|
284
|
+
without a handle, or a receiver without ``abandon_message`` (a test fake), leaves the lock to
|
|
285
|
+
lapse."""
|
|
286
|
+
if message.handle is None:
|
|
287
|
+
return
|
|
288
|
+
abandon = getattr(self._receiver(message.queue), "abandon_message", None)
|
|
289
|
+
if abandon is not None:
|
|
290
|
+
abandon(message.handle)
|
|
291
|
+
|
|
292
|
+
def settle_native(self, message: ReceivedMessage, reason: str, description: str) -> None:
|
|
293
|
+
"""Dead-letter ``message`` natively (:class:`~babelqueue.transport.NativeSettler`, ADR-0037
|
|
294
|
+
decisions 1 and 2): a version-gated delivery or a poison body that the runtime may neither
|
|
295
|
+
process nor drop, with dead-lettering off, is moved by an explicit ``DeadLetter`` (reason
|
|
296
|
+
``"unsupported schema version"`` or ``"poison"``, plus the description) into the entity's
|
|
297
|
+
``$DeadLetterQueue``. Nothing is completed, re-published or retried, and the body is
|
|
298
|
+
unchanged. A receiver without ``dead_letter_message`` abandons the lock instead (also a
|
|
299
|
+
conformant native settle: ``MaxDeliveryCount`` takes over)."""
|
|
300
|
+
if message.handle is None:
|
|
301
|
+
return
|
|
302
|
+
receiver = self._receiver(message.queue)
|
|
303
|
+
dead_letter = getattr(receiver, "dead_letter_message", None)
|
|
304
|
+
if dead_letter is not None:
|
|
305
|
+
dead_letter(message.handle, reason=reason, error_description=description)
|
|
306
|
+
return
|
|
307
|
+
self.abandon(message)
|
|
308
|
+
|
|
272
309
|
def close(self) -> None: # pragma: no cover - resource cleanup
|
|
273
310
|
for resource in (*self._senders.values(), *self._receivers.values(), self._client):
|
|
274
311
|
try:
|
|
@@ -25,6 +25,10 @@ contract keys win a collision, and the merge is bounded by SQS's 10-attribute li
|
|
|
25
25
|
non-``bq-`` attributes back onto :attr:`~babelqueue.transport.ReceivedMessage.headers`. The frozen
|
|
26
26
|
envelope is untouched (GR-1).
|
|
27
27
|
|
|
28
|
+
A version-gated message (``bq-schema-version`` the consumer does not support) or a poison body is, with
|
|
29
|
+
dead-lettering off, not deleted: :meth:`SqsTransport.settle_native` leaves it to SQS (visibility
|
|
30
|
+
expiry, then the queue's ``RedrivePolicy``; ADR-0037).
|
|
31
|
+
|
|
28
32
|
This implements §3 of the broker-bindings contract. The envelope is unchanged
|
|
29
33
|
(``schema_version`` stays 1); SQS is purely additive.
|
|
30
34
|
|
|
@@ -365,6 +369,31 @@ class SqsTransport(Transport):
|
|
|
365
369
|
QueueUrl=self._resolve_url(message.queue), ReceiptHandle=message.handle
|
|
366
370
|
)
|
|
367
371
|
|
|
372
|
+
def dead_letter_queue_name(self, queue: str, suffix: Optional[str] = None) -> str:
|
|
373
|
+
""":class:`~babelqueue.transport.DeadLetterNamer`: the BabelQueue DLQ of an SQS queue is
|
|
374
|
+
``<queue>-dlq`` by default and, for a FIFO queue, ``<queue>-dlq.fifo``. An explicit ``suffix``
|
|
375
|
+
(``dead_letter_suffix``; ``None``/empty = none) replaces ``-dlq`` and is placed before
|
|
376
|
+
``.fifo`` as well: an SQS FIFO queue name must end in ``.fifo``, so ``orders.fifo`` + ``-x`` is
|
|
377
|
+
``orders-x.fifo``, never ``orders.fifo-x``. An SQS queue name may hold only alphanumerics,
|
|
378
|
+
hyphens and underscores (plus that one ``.fifo`` suffix), so the runtime-wide ``<queue>.dlq``
|
|
379
|
+
cannot be created on SQS (ADR-0037 decision 7). An explicit ``dead_letter_queue`` overrides it."""
|
|
380
|
+
suffix = suffix or "-dlq"
|
|
381
|
+
if queue.endswith(".fifo"):
|
|
382
|
+
return queue[: -len(".fifo")] + suffix + ".fifo"
|
|
383
|
+
return queue + suffix
|
|
384
|
+
|
|
385
|
+
def settle_native(self, message: ReceivedMessage, reason: str, description: str) -> None:
|
|
386
|
+
"""Leave ``message`` to SQS itself (:class:`~babelqueue.transport.NativeSettler`, ADR-0037
|
|
387
|
+
decisions 1 and 2): a version-gated delivery or a poison body that the runtime may neither
|
|
388
|
+
process nor drop, with dead-lettering off. Deliberately does nothing on the wire: no
|
|
389
|
+
``DeleteMessage``, no ``ChangeMessageVisibility``, no re-send. The message becomes visible
|
|
390
|
+
again when its visibility timeout lapses (the gate or the poison check rejects it again) and
|
|
391
|
+
the queue's ``RedrivePolicy`` moves it to the native DLQ once ``maxReceiveCount`` is reached;
|
|
392
|
+
without a ``RedrivePolicy`` it is redelivered until the retention period ends. The runtime
|
|
393
|
+
has already logged the event."""
|
|
394
|
+
if message.handle:
|
|
395
|
+
self._uncounted_handles.discard(message.handle)
|
|
396
|
+
|
|
368
397
|
def redeliver(self, message: ReceivedMessage, body: str, delay: float) -> None:
|
|
369
398
|
"""Release ``message`` for redelivery after ``delay`` seconds (broker-bindings.md §3.5;
|
|
370
399
|
:class:`~babelqueue.transport.Redeliverer`).
|
|
@@ -229,6 +229,59 @@ class Rewinder(Protocol):
|
|
|
229
229
|
...
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
@runtime_checkable
|
|
233
|
+
class DeadLetterNamer(Protocol):
|
|
234
|
+
"""Optional :class:`Transport` capability: the name of the BabelQueue dead-letter queue for a
|
|
235
|
+
source queue, for a broker whose queue-name rules the runtime-wide ``<queue><suffix>`` shape cannot
|
|
236
|
+
satisfy. The runtime asks it whenever ``dead_letter_queue`` is not set, passing ``dead_letter_suffix``
|
|
237
|
+
(``None`` when not set). Amazon SQS implements it (a queue name cannot contain a dot and a FIFO name
|
|
238
|
+
must end in ``.fifo``): the default is ``<queue>-dlq``, FIFO ``<queue>-dlq.fifo``, and an explicit
|
|
239
|
+
suffix goes before ``.fifo`` too (``orders.fifo`` + ``-x`` -> ``orders-x.fifo``), never after it
|
|
240
|
+
(ADR-0037 decision 7). A transport without it keeps ``<queue><suffix>`` with the default ``.dlq``."""
|
|
241
|
+
|
|
242
|
+
def dead_letter_queue_name(self, queue: str, suffix: Optional[str] = None) -> str:
|
|
243
|
+
"""The dead-letter queue name for ``queue`` (``suffix`` is the explicit one, or ``None``)."""
|
|
244
|
+
...
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@runtime_checkable
|
|
248
|
+
class Abandoner(Protocol):
|
|
249
|
+
"""Optional :class:`Transport` capability: hand an unsettled *version-gated* delivery back to the
|
|
250
|
+
broker at once (Azure Service Bus ``Abandon``; broker-bindings.md section 4.5 "A failed write
|
|
251
|
+
``Abandon``s it"; ADR-0037 decision 1 rule 5).
|
|
252
|
+
|
|
253
|
+
The runtime calls :meth:`abandon` only when the dead-letter write of a version-gated delivery
|
|
254
|
+
failed, instead of leaving it locked until the lock lapses. It is deliberately narrower than
|
|
255
|
+
:class:`Rewinder`: a transport that offers only this capability keeps its previous graceful
|
|
256
|
+
shutdown release and retry re-publish failure handling (copy re-publish and ack)."""
|
|
257
|
+
|
|
258
|
+
def abandon(self, message: ReceivedMessage) -> None:
|
|
259
|
+
"""Release the broker lock on ``message`` so the broker redelivers it right away."""
|
|
260
|
+
...
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@runtime_checkable
|
|
264
|
+
class NativeSettler(Protocol):
|
|
265
|
+
"""Optional :class:`Transport` capability: leave a delivery the runtime can neither process nor
|
|
266
|
+
safely drop to the broker's own mechanism, without acknowledging or deleting it and without
|
|
267
|
+
re-publishing or retrying it (ADR-0037 decisions 1 and 2).
|
|
268
|
+
|
|
269
|
+
The runtime calls :meth:`settle_native` instead of acknowledging when dead-lettering is off (and
|
|
270
|
+
the strategy is neither ``delete`` nor a writable ``dead_letter``) for a version-gated delivery
|
|
271
|
+
(broker-bindings.md section 5.7 rule 3) and for a poison body (error-handling.md section 7).
|
|
272
|
+
SQS implements it as "do nothing" (no ``DeleteMessage``: visibility expiry redelivers the message
|
|
273
|
+
and the queue's ``RedrivePolicy`` moves it to the native DLQ after ``maxReceiveCount``); Azure
|
|
274
|
+
Service Bus implements it with an explicit native ``DeadLetter(reason, description)`` into
|
|
275
|
+
``$DeadLetterQueue``. The body is never altered. A transport that does not implement it keeps
|
|
276
|
+
the previous settle (acknowledge and drop), so the behaviour follows the capability, never the
|
|
277
|
+
transport's name."""
|
|
278
|
+
|
|
279
|
+
def settle_native(self, message: ReceivedMessage, reason: str, description: str) -> None:
|
|
280
|
+
"""Leave ``message`` to the broker's native mechanism; ``reason`` and ``description`` say
|
|
281
|
+
why (e.g. ``"unsupported schema version"``, ``"poison"``)."""
|
|
282
|
+
...
|
|
283
|
+
|
|
284
|
+
|
|
232
285
|
#: Reserved ``route_urn`` of a record whose envelope carries a non-numeric ``attempts`` (a foreign
|
|
233
286
|
#: producer). It sits under the version-gated prefix, so the runtime dead-letters it directly, raw
|
|
234
287
|
#: and never retried (retrying needs a numeric attempt counter); ``redrive`` restores it to the DLQ
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.8.0
|