babelqueue 1.19.0__tar.gz → 1.20.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.19.0 → babelqueue-1.20.0}/.github/workflows/ci.yml +1 -1
- {babelqueue-1.19.0 → babelqueue-1.20.0}/CHANGELOG.md +35 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/PKG-INFO +24 -1
- {babelqueue-1.19.0 → babelqueue-1.20.0}/README.md +23 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/pyproject.toml +1 -1
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/__init__.py +1 -1
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/_envelope_shape.py +3 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/app.py +9 -5
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/codec.py +107 -32
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/exceptions.py +15 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/pulsar_transport.py +10 -5
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/sqs_transport.py +5 -2
- babelqueue-1.20.0/tests/conformance/CONFORMANCE_VERSION +1 -0
- babelqueue-1.20.0/tests/conformance/fixtures/blank-ascii-job-urn-fallback.json +16 -0
- babelqueue-1.20.0/tests/conformance/fixtures/dead-letter-roundtrip.json +23 -0
- babelqueue-1.20.0/tests/conformance/fixtures/forbidden-key-job-and-urn.json +16 -0
- babelqueue-1.20.0/tests/conformance/fixtures/forbidden-key-job-nonascii-space-and-urn.json +16 -0
- babelqueue-1.20.0/tests/conformance/fixtures/meta-ext-vendor-roundtrip.json +27 -0
- babelqueue-1.20.0/tests/conformance/fixtures/urn-only-roundtrip.json +15 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/manifest.json +290 -2
- babelqueue-1.20.0/tests/test_behaviour_conformance.py +513 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_codec.py +5 -7
- babelqueue-1.20.0/tests/test_reencode_regression.py +186 -0
- babelqueue-1.19.0/tests/conformance/CONFORMANCE_VERSION +0 -1
- babelqueue-1.19.0/tests/test_behaviour_conformance.py +0 -195
- {babelqueue-1.19.0 → babelqueue-1.20.0}/.github/FUNDING.yml +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/.github/dependabot.yml +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/.github/workflows/release.yml +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/.gitignore +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/LICENSE +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/artemis_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/asb_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/celery.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/contracts.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/dead_letter.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/django/__init__.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/django/apps.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/django/management/__init__.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/django/management/commands/__init__.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/django/management/commands/babelqueue_worker.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/gdpr.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/headers.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/idempotency.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/kafka_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/otel.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/outbox.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/pika_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/py.typed +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/redis_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/redrive.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/replay.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/routing.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/schema.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/src/babelqueue/transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/data-array-rejected.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/duplicate-keys-last-wins.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/empty-data-roundtrip.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/forbidden-key-meta-attempts.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/forbidden-key-meta-max-retries.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/forbidden-key-meta-source.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/forbidden-key-meta-ts.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/forbidden-key-timestamp.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/invalid-missing-urn.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/invalid-unknown-schema-version.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/invalid-wrong-case-job.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/order-created.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/unicode-and-numbers.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/unknown-lang.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/unknown-meta-roundtrip.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/unknown-toplevel-roundtrip.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/urn-alias.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/fixtures/wrong-case-keys-roundtrip.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/conformance/schema/message-envelope.schema.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/fakes/confluent_kafka/__init__.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/fakes/confluent_kafka/admin.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/fixtures/dead-lettered.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/fixtures/order-created.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/poison-trigger-set.json +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_app.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_artemis_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_artemis_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_asb_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_asb_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_celery.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_dead_letter.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_django.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_gdpr.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_headers.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_idempotency.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_kafka_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_kafka_integration.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_kafka_integration_logic.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_kafka_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_otel.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_outbox.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_overhead.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_pika_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_pulsar_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_pulsar_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_redis_transport.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_redrive.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_replay.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_schema.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_schema_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_shutdown.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_sqs_conformance.py +0 -0
- {babelqueue-1.19.0 → babelqueue-1.20.0}/tests/test_sqs_transport.py +0 -0
|
@@ -166,7 +166,7 @@ jobs:
|
|
|
166
166
|
- uses: actions/checkout@v7
|
|
167
167
|
- name: Verify vendored conformance matches the canonical suite
|
|
168
168
|
run: |
|
|
169
|
-
git clone --depth 1 --branch v1.
|
|
169
|
+
git clone --depth 1 --branch v1.4.0 https://github.com/BabelQueue/conformance.git "$RUNNER_TEMP/conformance"
|
|
170
170
|
diff -u "$RUNNER_TEMP/conformance/CONFORMANCE_VERSION" "tests/conformance/CONFORMANCE_VERSION"
|
|
171
171
|
diff -ru "$RUNNER_TEMP/conformance/manifest.json" "tests/conformance/manifest.json"
|
|
172
172
|
diff -ru "$RUNNER_TEMP/conformance/fixtures" "tests/conformance/fixtures"
|
|
@@ -9,6 +9,41 @@ The envelope wire format is versioned separately by `meta.schema_version`
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.20.0] - 2026-10-17
|
|
13
|
+
|
|
14
|
+
Conformance suite 1.4.0, [ADR-0031](https://babelqueue.com). The envelope is unchanged
|
|
15
|
+
(`schema_version: 1`); this is a MINOR with one deliberate behaviour change (encode rejects).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `EnvelopeCodec.make(urn, data, *, queue, trace_id, meta=None)`: a producer `meta` write API. The entries
|
|
19
|
+
land in `meta` next to the core keys (recommended shape `meta.ext.<vendor>`). A collision with a core
|
|
20
|
+
meta key or a forbidden key raises the new `EnvelopeKeyError` (subclass of `BabelQueueError`, with a
|
|
21
|
+
`pointer` attribute). `_envelope_shape.CORE_META_KEYS` is the single copy of the core key list.
|
|
22
|
+
- `decode` folds the `urn` alias into `job` (ADR-0031 (c)): a `job` that is missing, empty or only ASCII
|
|
23
|
+
whitespace (`" \t\n\v\f\r"`; a U+2003 `job` is NOT blank) falls back to `urn` silently; when both carry
|
|
24
|
+
a value, `job` wins and `urn` is dropped with a warning naming `/urn`. `accepts()` / `urn()` follow the
|
|
25
|
+
same rule. The decoded envelope never keeps a `urn` key.
|
|
26
|
+
- Tests: the runner now implements the 1.4.0 fields (`expect_preserved` on `forbidden_keys`;
|
|
27
|
+
`expect_absent_after_reencode`, `apply_dead_letter`, `expect_dead_letter` on `roundtrip`, the latter via
|
|
28
|
+
`dead_letter.annotate`) plus the `extension_write` and `encode_reject` sections, with a runner
|
|
29
|
+
self-test proving violations are reported.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **Behaviour change: `EnvelopeCodec.encode` raises `EnvelopeKeyError`** for an envelope carrying a
|
|
33
|
+
forbidden key (message-envelope.md section 10) or a top-level `urn`, instead of dropping it with a
|
|
34
|
+
warning (policy K-15 (a)). Nothing is written. `decode` still warns and drops, so re-encoding a decoded
|
|
35
|
+
envelope is safe. Code that built an envelope from a raw legacy body and relied on encode to clean it
|
|
36
|
+
must decode it first (`EnvelopeCodec.encode(EnvelopeCodec.decode(raw))`).
|
|
37
|
+
- `test_all_five_contract_keys_are_covered` is a subset check (the suite adds `/urn` cases).
|
|
38
|
+
- Docs only: the note in the previous release that the Go reference's case-insensitive envelope-key read "is tightened separately" is now closed — Go core 1.14.0 reads envelope keys exactly (case-sensitive, last duplicate wins), so every SDK now decides poison on the same exact-case keys.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- Raw-body re-encode paths now decode before they encode, so the new `encode` rejection cannot strand a
|
|
42
|
+
legacy body: the runtime's raw-body RELEASE re-publish (`app.py`), and the SQS and Pulsar attempts
|
|
43
|
+
reconciliation (Kafka, Azure Service Bus and Artemis already decoded first). A body with a legacy
|
|
44
|
+
`timestamp` or only a `urn` alias is re-emitted normalised (forbidden key dropped and warned, `urn`
|
|
45
|
+
folded into `job`); unknown top-level and `meta` keys, including `meta.ext`, are preserved.
|
|
46
|
+
|
|
12
47
|
## [1.19.0] - 2026-10-12
|
|
13
48
|
|
|
14
49
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: babelqueue
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20.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
|
|
@@ -106,6 +106,29 @@ The envelope is identical to every other SDK's:
|
|
|
106
106
|
}
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
### Extension metadata, forbidden keys and the `urn` alias (1.20.0, ADR-0031)
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Producer meta: extra keys next to the core meta keys (recommended shape: meta.ext.<vendor>).
|
|
113
|
+
envelope = EnvelopeCodec.make(
|
|
114
|
+
"urn:babel:orders:created", {"order_id": 1042},
|
|
115
|
+
queue="orders", meta={"ext": {"acme": {"tier": "gold"}}},
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `meta=` is keyword-only. A key that collides with a core meta key (`id`, `queue`, `lang`,
|
|
120
|
+
`schema_version`, `created_at`) or with a forbidden key (`max_retries`, `attempts`, `source`, `ts`)
|
|
121
|
+
raises `EnvelopeKeyError` (a `BabelQueueError`; its `pointer` names the key, e.g. `/meta/ts`).
|
|
122
|
+
- Unknown top-level and `meta` keys of an inbound message survive a decode -> `attempts + 1` -> encode
|
|
123
|
+
hop (retry, redrive, dead-letter), so a vendor `meta.ext` block is never lost.
|
|
124
|
+
- `EnvelopeCodec.encode` **rejects** an envelope that carries a forbidden key (top-level `timestamp`;
|
|
125
|
+
`meta.max_retries`, `meta.attempts`, `meta.source`, `meta.ts`) or the inbound-only top-level `urn`
|
|
126
|
+
alias: it raises `EnvelopeKeyError` instead of dropping the key with a warning. `decode` is unchanged
|
|
127
|
+
(it still warns and drops), so a decoded envelope can always be re-encoded.
|
|
128
|
+
- `decode` folds `urn` into `job`: a `job` that is missing, empty or only ASCII whitespace (space, `\t`,
|
|
129
|
+
`\n`, `\v`, `\f`, `\r`) falls back to `urn`; when both carry a value, `job` wins and `urn` is dropped
|
|
130
|
+
with a warning naming `/urn`. The decoded envelope never keeps a `urn` key.
|
|
131
|
+
|
|
109
132
|
### Typed messages (optional)
|
|
110
133
|
|
|
111
134
|
```python
|
|
@@ -52,6 +52,29 @@ The envelope is identical to every other SDK's:
|
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
### Extension metadata, forbidden keys and the `urn` alias (1.20.0, ADR-0031)
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Producer meta: extra keys next to the core meta keys (recommended shape: meta.ext.<vendor>).
|
|
59
|
+
envelope = EnvelopeCodec.make(
|
|
60
|
+
"urn:babel:orders:created", {"order_id": 1042},
|
|
61
|
+
queue="orders", meta={"ext": {"acme": {"tier": "gold"}}},
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `meta=` is keyword-only. A key that collides with a core meta key (`id`, `queue`, `lang`,
|
|
66
|
+
`schema_version`, `created_at`) or with a forbidden key (`max_retries`, `attempts`, `source`, `ts`)
|
|
67
|
+
raises `EnvelopeKeyError` (a `BabelQueueError`; its `pointer` names the key, e.g. `/meta/ts`).
|
|
68
|
+
- Unknown top-level and `meta` keys of an inbound message survive a decode -> `attempts + 1` -> encode
|
|
69
|
+
hop (retry, redrive, dead-letter), so a vendor `meta.ext` block is never lost.
|
|
70
|
+
- `EnvelopeCodec.encode` **rejects** an envelope that carries a forbidden key (top-level `timestamp`;
|
|
71
|
+
`meta.max_retries`, `meta.attempts`, `meta.source`, `meta.ts`) or the inbound-only top-level `urn`
|
|
72
|
+
alias: it raises `EnvelopeKeyError` instead of dropping the key with a warning. `decode` is unchanged
|
|
73
|
+
(it still warns and drops), so a decoded envelope can always be re-encoded.
|
|
74
|
+
- `decode` folds `urn` into `job`: a `job` that is missing, empty or only ASCII whitespace (space, `\t`,
|
|
75
|
+
`\n`, `\v`, `\f`, `\r`) falls back to `urn`; when both carry a value, `job` wins and `urn` is dropped
|
|
76
|
+
with a warning naming `/urn`. The decoded envelope never keeps a `urn` key.
|
|
77
|
+
|
|
55
78
|
### Typed messages (optional)
|
|
56
79
|
|
|
57
80
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "babelqueue"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.20.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"
|
|
@@ -51,6 +51,9 @@ _META = {
|
|
|
51
51
|
"schema_version": _INTEGER,
|
|
52
52
|
"created_at": _INTEGER,
|
|
53
53
|
}
|
|
54
|
+
#: The core ``meta`` keys of message-envelope.md section 4 (the single copy; the producer ``meta``
|
|
55
|
+
#: write API of the codec rejects a collision with any of them, ADR-0031 (b)).
|
|
56
|
+
CORE_META_KEYS = frozenset(_META)
|
|
54
57
|
_DEAD_LETTER = {
|
|
55
58
|
"reason": _STRING,
|
|
56
59
|
"error": _STRING,
|
|
@@ -458,8 +458,9 @@ class BabelQueue:
|
|
|
458
458
|
self._stop.wait(self._rewind_pause)
|
|
459
459
|
|
|
460
460
|
def _release_unprocessed(self, received: ReceivedMessage) -> None:
|
|
461
|
-
"""Release a message whose handler never completed (shutdown): unchanged body
|
|
462
|
-
|
|
461
|
+
"""Release a message whose handler never completed (shutdown): unchanged body
|
|
462
|
+
(normalised only if it carries a forbidden key or a ``urn`` alias), no backoff, and the
|
|
463
|
+
runtime adds no ``attempts`` increment. A broker that counts
|
|
463
464
|
deliveries still counts it: on SQS the release bumps ``ApproximateReceiveCount``, so the
|
|
464
465
|
redelivered envelope reads one more attempt (and a ``RedrivePolicy`` sees it too)."""
|
|
465
466
|
self._release(received, received.body, 0.0)
|
|
@@ -578,12 +579,15 @@ _UNSUPPORTED_SCHEMA_ROUTE_PREFIX = "babelqueue:unsupported-schema-version:"
|
|
|
578
579
|
|
|
579
580
|
def _without_forbidden_keys(body: str) -> str:
|
|
580
581
|
"""``body`` unchanged, unless it carries a forbidden envelope key (message-envelope.md §10,
|
|
581
|
-
e.g. from a legacy producer) — then it is re-encoded without it (warned by the codec), so a
|
|
582
|
+
e.g. from a legacy producer) or a ``urn`` alias — then it is re-encoded without it (warned by the codec), so a
|
|
582
583
|
raw-body re-publish never re-emits a key the decode boundary reported as dropped."""
|
|
583
584
|
parsed = parse_envelope(body)
|
|
584
|
-
|
|
585
|
+
# ``urn`` is not a §10 key but ``encode`` rejects it and ADR-0031 (a)/(c) forbid re-emitting it.
|
|
586
|
+
if not parsed or not (has_forbidden_keys(parsed) or "urn" in parsed):
|
|
585
587
|
return body
|
|
586
|
-
|
|
588
|
+
# ``decode`` (not the raw parse) so the dropped keys — and a top-level ``urn`` alias, folded
|
|
589
|
+
# into ``job`` — are gone before ``encode``, which rejects them (ADR-0031 (c)).
|
|
590
|
+
return EnvelopeCodec.encode(EnvelopeCodec.decode(body))
|
|
587
591
|
|
|
588
592
|
|
|
589
593
|
def _handler_wants_envelope(fn: Handler) -> bool:
|
|
@@ -16,15 +16,17 @@ import time
|
|
|
16
16
|
import uuid
|
|
17
17
|
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
18
18
|
|
|
19
|
-
from .
|
|
19
|
+
from ._envelope_shape import CORE_META_KEYS
|
|
20
|
+
from .exceptions import BabelQueueError, EnvelopeKeyError
|
|
20
21
|
|
|
21
22
|
SCHEMA_VERSION = 1
|
|
22
23
|
SOURCE_LANG = "python"
|
|
23
24
|
|
|
24
25
|
#: Non-canonical keys (message-envelope.md §10), as ``(container, key)`` pairs where the
|
|
25
26
|
#: container is ``None`` for the top level or ``"meta"``. Policy K-15: decode WARNS and drops
|
|
26
|
-
#: them (old producers keep working); encode NEVER emits them
|
|
27
|
-
#:
|
|
27
|
+
#: them (old producers keep working); encode NEVER emits them and REJECTS an envelope that
|
|
28
|
+
#: carries one (ADR-0031 (c)). The top-level ``urn`` alias is not in this table: it is an inbound
|
|
29
|
+
#: alias that decode folds into ``job`` (see :meth:`EnvelopeCodec.decode`), but encode rejects it.
|
|
28
30
|
FORBIDDEN_KEYS: Tuple[Tuple[Optional[str], str], ...] = (
|
|
29
31
|
(None, "timestamp"),
|
|
30
32
|
("meta", "max_retries"),
|
|
@@ -33,6 +35,10 @@ FORBIDDEN_KEYS: Tuple[Tuple[Optional[str], str], ...] = (
|
|
|
33
35
|
("meta", "ts"),
|
|
34
36
|
)
|
|
35
37
|
|
|
38
|
+
#: The closed ASCII whitespace set that makes a ``job`` "blank" (cross-SDK parity, GR-5): never a
|
|
39
|
+
#: bare ``str.strip()``, which also strips Unicode spaces such as U+2003.
|
|
40
|
+
_ASCII_WHITESPACE = " \t\n\v\f\r"
|
|
41
|
+
|
|
36
42
|
#: Logger every forbidden-key warning is reported through (one record per dropped key).
|
|
37
43
|
logger = logging.getLogger("babelqueue.codec")
|
|
38
44
|
|
|
@@ -52,11 +58,7 @@ def strip_forbidden_keys(envelope: Dict[str, Any]) -> List[str]:
|
|
|
52
58
|
del target[key]
|
|
53
59
|
pointer = "/" + key if container is None else f"/{container}/{key}"
|
|
54
60
|
dropped.append(pointer)
|
|
55
|
-
|
|
56
|
-
"Dropped forbidden envelope key %s from the envelope "
|
|
57
|
-
"(non-canonical, message-envelope.md §10).",
|
|
58
|
-
pointer,
|
|
59
|
-
)
|
|
61
|
+
_warn_dropped(pointer)
|
|
60
62
|
return dropped
|
|
61
63
|
|
|
62
64
|
|
|
@@ -76,10 +78,42 @@ def parse_envelope(raw: Any) -> Dict[str, Any]:
|
|
|
76
78
|
|
|
77
79
|
|
|
78
80
|
def has_forbidden_keys(envelope: Mapping[str, Any]) -> bool:
|
|
79
|
-
"""Whether ``envelope`` carries any of the non-canonical keys of message-envelope.md §10.
|
|
81
|
+
"""Whether ``envelope`` carries any of the non-canonical keys of message-envelope.md §10.
|
|
82
|
+
|
|
83
|
+
The top-level ``urn`` alias is NOT included (it is not a §10 key: decode folds it into ``job``),
|
|
84
|
+
although ``encode`` also rejects it — a caller deciding whether a raw body needs normalising
|
|
85
|
+
before re-emit must check ``"urn" in envelope`` as well.
|
|
86
|
+
"""
|
|
80
87
|
return _has_forbidden_keys(envelope)
|
|
81
88
|
|
|
82
89
|
|
|
90
|
+
def _is_blank(value: Any) -> bool:
|
|
91
|
+
"""Missing, ``None``, empty or only ASCII whitespace; a falsy non-string (``0``, ``false``,
|
|
92
|
+
``[]``, ``{}``) is blank too, as in ``job or urn``. A truthy non-string is not blank."""
|
|
93
|
+
if isinstance(value, str):
|
|
94
|
+
return value.strip(_ASCII_WHITESPACE) == ""
|
|
95
|
+
return not value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _warn_dropped(pointer: str) -> None:
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Dropped forbidden envelope key %s from the envelope "
|
|
101
|
+
"(non-canonical, message-envelope.md §10).",
|
|
102
|
+
pointer,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _first_encode_violation(envelope: Mapping[str, Any]) -> Optional[str]:
|
|
107
|
+
"""RFC 6901 pointer of the first key encode refuses to write, or ``None``."""
|
|
108
|
+
for container, key in FORBIDDEN_KEYS:
|
|
109
|
+
target = envelope if container is None else envelope.get(container)
|
|
110
|
+
if isinstance(target, Mapping) and key in target:
|
|
111
|
+
return "/" + key if container is None else f"/{container}/{key}"
|
|
112
|
+
if "urn" in envelope:
|
|
113
|
+
return "/urn"
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
83
117
|
def _has_forbidden_keys(envelope: Mapping[str, Any]) -> bool:
|
|
84
118
|
for container, key in FORBIDDEN_KEYS:
|
|
85
119
|
target = envelope if container is None else envelope.get(container)
|
|
@@ -101,12 +135,18 @@ class EnvelopeCodec:
|
|
|
101
135
|
*,
|
|
102
136
|
queue: str = "default",
|
|
103
137
|
trace_id: Optional[str] = None,
|
|
138
|
+
meta: Optional[Mapping[str, Any]] = None,
|
|
104
139
|
) -> Dict[str, Any]:
|
|
105
140
|
"""Build the canonical envelope for a ``(urn, data)`` pair.
|
|
106
141
|
|
|
107
142
|
``trace_id`` is reused when given (trace continuation), otherwise a fresh
|
|
108
143
|
UUID is minted. ``attempts`` starts at 0 and is the top-level transport
|
|
109
144
|
counter (kept out of the immutable ``meta`` block).
|
|
145
|
+
|
|
146
|
+
``meta`` (ADR-0031 (b)) adds producer-supplied entries (e.g. ``{"ext": {"acme": 1}}``) to
|
|
147
|
+
the ``meta`` block next to the core keys. An entry whose key is a core ``meta`` key
|
|
148
|
+
(``id``, ``queue``, ``lang``, ``schema_version``, ``created_at``) or a forbidden ``meta``
|
|
149
|
+
key (message-envelope.md §10) raises :class:`~babelqueue.exceptions.EnvelopeKeyError`.
|
|
110
150
|
"""
|
|
111
151
|
urn = (urn or "").strip()
|
|
112
152
|
if not urn:
|
|
@@ -117,19 +157,36 @@ class EnvelopeCodec:
|
|
|
117
157
|
|
|
118
158
|
trace_id = (trace_id or "").strip() or str(uuid.uuid4())
|
|
119
159
|
|
|
120
|
-
|
|
160
|
+
extra: Dict[str, Any] = {}
|
|
161
|
+
if meta is not None:
|
|
162
|
+
if not isinstance(meta, Mapping):
|
|
163
|
+
raise BabelQueueError("The producer meta must be a mapping of string keys to values.")
|
|
164
|
+
forbidden_meta = {key for container, key in FORBIDDEN_KEYS if container == "meta"}
|
|
165
|
+
for key, value in meta.items():
|
|
166
|
+
if key in CORE_META_KEYS or key in forbidden_meta:
|
|
167
|
+
raise EnvelopeKeyError(
|
|
168
|
+
f"/meta/{key}",
|
|
169
|
+
f"The producer meta key /meta/{key} is reserved or forbidden "
|
|
170
|
+
"(core meta keys and message-envelope.md §10 keys cannot be set).",
|
|
171
|
+
)
|
|
172
|
+
extra[key] = value
|
|
173
|
+
|
|
174
|
+
meta_block: Dict[str, Any] = {
|
|
175
|
+
"id": str(uuid.uuid4()),
|
|
176
|
+
"queue": queue,
|
|
177
|
+
"lang": SOURCE_LANG,
|
|
178
|
+
"schema_version": SCHEMA_VERSION,
|
|
179
|
+
"created_at": int(time.time() * 1000),
|
|
180
|
+
}
|
|
181
|
+
meta_block.update(extra)
|
|
182
|
+
envelope: Dict[str, Any] = {
|
|
121
183
|
"job": urn,
|
|
122
184
|
"trace_id": trace_id,
|
|
123
185
|
"data": dict(data),
|
|
124
|
-
"meta":
|
|
125
|
-
"id": str(uuid.uuid4()),
|
|
126
|
-
"queue": queue,
|
|
127
|
-
"lang": SOURCE_LANG,
|
|
128
|
-
"schema_version": SCHEMA_VERSION,
|
|
129
|
-
"created_at": int(time.time() * 1000),
|
|
130
|
-
},
|
|
186
|
+
"meta": meta_block,
|
|
131
187
|
"attempts": 0,
|
|
132
188
|
}
|
|
189
|
+
return envelope
|
|
133
190
|
|
|
134
191
|
@staticmethod
|
|
135
192
|
def from_message(message: Any, queue: str = "default") -> Dict[str, Any]:
|
|
@@ -152,20 +209,20 @@ class EnvelopeCodec:
|
|
|
152
209
|
def encode(envelope: Mapping[str, Any]) -> str:
|
|
153
210
|
"""Encode the envelope as compact UTF-8 JSON (unescaped unicode/slashes).
|
|
154
211
|
|
|
155
|
-
Forbidden keys (message-envelope.md §10) are never emitted:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
from a legacy source.
|
|
212
|
+
Forbidden keys (message-envelope.md §10) are never emitted: an envelope that carries one
|
|
213
|
+
(top-level ``timestamp``; ``meta.max_retries`` / ``meta.attempts`` / ``meta.source`` /
|
|
214
|
+
``meta.ts``) or the inbound-only top-level ``urn`` alias raises
|
|
215
|
+
:class:`~babelqueue.exceptions.EnvelopeKeyError` (ADR-0031 (c)) and nothing is written.
|
|
216
|
+
Envelopes produced by :meth:`decode` are always safe to re-encode: decode already drops
|
|
217
|
+
those keys. Unknown, non-forbidden keys are kept as-is.
|
|
162
218
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
219
|
+
pointer = _first_encode_violation(envelope)
|
|
220
|
+
if pointer is not None:
|
|
221
|
+
raise EnvelopeKeyError(
|
|
222
|
+
pointer,
|
|
223
|
+
f"Refusing to encode the forbidden envelope key {pointer} "
|
|
224
|
+
"(non-canonical, message-envelope.md §10).",
|
|
225
|
+
)
|
|
169
226
|
return json.dumps(envelope, ensure_ascii=False, separators=(",", ":"))
|
|
170
227
|
|
|
171
228
|
@staticmethod
|
|
@@ -175,15 +232,33 @@ class EnvelopeCodec:
|
|
|
175
232
|
Unknown keys are kept as-is (forward compatibility). Forbidden keys
|
|
176
233
|
(message-envelope.md §10) are dropped and reported as warnings on :data:`logger`
|
|
177
234
|
(policy K-15: warn, never reject), so re-encoding never re-emits them.
|
|
235
|
+
|
|
236
|
+
The ``urn`` alias is folded into ``job`` (ADR-0031 (c)): when ``job`` is missing, empty or
|
|
237
|
+
only ASCII whitespace, the ``urn`` value becomes ``job`` (silently); when both carry a
|
|
238
|
+
value, ``job`` wins and ``urn`` is dropped with a warning naming ``/urn``. The decoded
|
|
239
|
+
envelope never keeps a ``urn`` key.
|
|
178
240
|
"""
|
|
179
241
|
decoded = parse_envelope(raw)
|
|
180
242
|
strip_forbidden_keys(decoded)
|
|
243
|
+
if "urn" in decoded:
|
|
244
|
+
if _is_blank(decoded.get("job")):
|
|
245
|
+
decoded["job"] = decoded.pop("urn")
|
|
246
|
+
else:
|
|
247
|
+
del decoded["urn"]
|
|
248
|
+
_warn_dropped("/urn")
|
|
181
249
|
return decoded
|
|
182
250
|
|
|
183
251
|
@staticmethod
|
|
184
252
|
def urn(envelope: Mapping[str, Any]) -> str:
|
|
185
|
-
"""The message URN: canonical ``job``, with ``urn`` accepted as an alias.
|
|
186
|
-
|
|
253
|
+
"""The message URN: canonical ``job``, with ``urn`` accepted as an alias.
|
|
254
|
+
|
|
255
|
+
A blank ``job`` (missing, empty or only ASCII whitespace) falls back to ``urn``.
|
|
256
|
+
"""
|
|
257
|
+
job = envelope.get("job")
|
|
258
|
+
if isinstance(job, str) and not _is_blank(job):
|
|
259
|
+
return job
|
|
260
|
+
alias = envelope.get("urn")
|
|
261
|
+
return alias if isinstance(alias, str) and not _is_blank(alias) else ""
|
|
187
262
|
|
|
188
263
|
@staticmethod
|
|
189
264
|
def accepts(envelope: Mapping[str, Any]) -> bool:
|
|
@@ -7,6 +7,21 @@ class BabelQueueError(Exception):
|
|
|
7
7
|
"""Base for every recoverable BabelQueue error (bad config, empty URN, ...)."""
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class EnvelopeKeyError(BabelQueueError):
|
|
11
|
+
"""An envelope key the codec refuses to write (ADR-0031 (b), (c)).
|
|
12
|
+
|
|
13
|
+
Raised by :meth:`~babelqueue.codec.EnvelopeCodec.make` when the ``meta`` entries collide with a
|
|
14
|
+
core ``meta`` key (``id``, ``queue``, ``lang``, ``schema_version``, ``created_at``) or carry a
|
|
15
|
+
forbidden key, and by :meth:`~babelqueue.codec.EnvelopeCodec.encode` when the envelope holds a
|
|
16
|
+
forbidden key of message-envelope.md section 10 (including the top-level ``urn`` alias).
|
|
17
|
+
``pointer`` is the RFC 6901 pointer of the first offending key (e.g. ``"/meta/ts"``).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, pointer: str, message: str) -> None:
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.pointer = pointer
|
|
23
|
+
|
|
24
|
+
|
|
10
25
|
class UnknownUrnError(BabelQueueError):
|
|
11
26
|
"""A consumed message carries a URN with no mapped handler (strategy "fail")."""
|
|
12
27
|
|
|
@@ -241,7 +241,7 @@ class PulsarTransport(Transport):
|
|
|
241
241
|
|
|
242
242
|
* Routing: a non-blank ``bq-job`` property is the URN the app routes on. It is returned
|
|
243
243
|
as ``route_urn`` (set on :attr:`ReceivedMessage.route_urn`); the body's ``job`` /
|
|
244
|
-
``urn`` is never rewritten, so the handler, retries and the dead-letter record keep the
|
|
244
|
+
``urn`` is never rewritten for routing, so the handler, retries and the dead-letter record keep the
|
|
245
245
|
producer's original body. A body with no URN gets no ``route_urn`` — it stays
|
|
246
246
|
non-conformant and is rejected whatever ``bq-job`` says.
|
|
247
247
|
* Attempts: a valid ``bq-attempts`` property replaces the body's value (the body's is
|
|
@@ -254,8 +254,9 @@ class PulsarTransport(Transport):
|
|
|
254
254
|
still rejected by ``EnvelopeCodec.accepts``.
|
|
255
255
|
* ``on_routing_mismatch(property_urn, body_urn, envelope)`` is called when the property
|
|
256
256
|
and the body name different URNs — only for an envelope the consumer accepts and after
|
|
257
|
-
attempts are reconciled (as in the Java and .NET consumers)
|
|
258
|
-
|
|
257
|
+
attempts are reconciled (as in the Java and .NET consumers). It receives the raw
|
|
258
|
+
parsed body, not the decoded envelope the handler gets, so a ``urn`` alias or a
|
|
259
|
+
forbidden key the decode would drop is still present. The hook is optional, separate from any error handling, and any
|
|
259
260
|
exception it raises is swallowed — it never affects the message flow.
|
|
260
261
|
"""
|
|
261
262
|
properties = properties or {}
|
|
@@ -282,8 +283,12 @@ class PulsarTransport(Transport):
|
|
|
282
283
|
|
|
283
284
|
out = body
|
|
284
285
|
if has_int_attempts and current != attempts:
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
# Re-encode from the *decoded* body: ``encode`` rejects forbidden keys / a ``urn`` alias,
|
|
287
|
+
# which the raw parse above keeps (ADR-0031).
|
|
288
|
+
env["attempts"] = attempts # the routing-mismatch hook below sees the reconciled value
|
|
289
|
+
reencoded = EnvelopeCodec.decode(body)
|
|
290
|
+
reencoded["attempts"] = attempts
|
|
291
|
+
out = EnvelopeCodec.encode(reencoded)
|
|
287
292
|
|
|
288
293
|
if route_urn and route_urn != body_urn and on_routing_mismatch is not None:
|
|
289
294
|
if EnvelopeCodec.accepts(env):
|
|
@@ -213,8 +213,11 @@ class SqsTransport(Transport):
|
|
|
213
213
|
native = rc - 1
|
|
214
214
|
if native <= int(env.get("attempts", 0)):
|
|
215
215
|
return body
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
# Re-encode from the *decoded* body: ``encode`` rejects forbidden keys / a ``urn`` alias,
|
|
217
|
+
# which the raw parse above keeps (ADR-0031).
|
|
218
|
+
reencoded = EnvelopeCodec.decode(body)
|
|
219
|
+
reencoded["attempts"] = native
|
|
220
|
+
return EnvelopeCodec.encode(reencoded)
|
|
218
221
|
|
|
219
222
|
# -- Transport ----------------------------------------------------------
|
|
220
223
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.4.0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": " ",
|
|
3
|
+
"urn": "urn:acme:b",
|
|
4
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
5
|
+
"data": {
|
|
6
|
+
"order_id": 1042
|
|
7
|
+
},
|
|
8
|
+
"meta": {
|
|
9
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
10
|
+
"queue": "orders",
|
|
11
|
+
"lang": "php",
|
|
12
|
+
"schema_version": 1,
|
|
13
|
+
"created_at": 1749132727000
|
|
14
|
+
},
|
|
15
|
+
"attempts": 0
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": "urn:acme:a",
|
|
3
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
4
|
+
"data": {
|
|
5
|
+
"order_id": 1042
|
|
6
|
+
},
|
|
7
|
+
"meta": {
|
|
8
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
9
|
+
"queue": "orders",
|
|
10
|
+
"lang": "php",
|
|
11
|
+
"schema_version": 1,
|
|
12
|
+
"created_at": 1749132727000,
|
|
13
|
+
"vendor_flag": true,
|
|
14
|
+
"ext": {
|
|
15
|
+
"acme": {
|
|
16
|
+
"tier": "gold",
|
|
17
|
+
"flags": [1, 2]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"extra_top": 1,
|
|
22
|
+
"attempts": 2
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": "urn:acme:a",
|
|
3
|
+
"urn": "urn:acme:b",
|
|
4
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
5
|
+
"data": {
|
|
6
|
+
"order_id": 1042
|
|
7
|
+
},
|
|
8
|
+
"meta": {
|
|
9
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
10
|
+
"queue": "orders",
|
|
11
|
+
"lang": "php",
|
|
12
|
+
"schema_version": 1,
|
|
13
|
+
"created_at": 1749132727000
|
|
14
|
+
},
|
|
15
|
+
"attempts": 0
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": " ",
|
|
3
|
+
"urn": "urn:acme:b",
|
|
4
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
5
|
+
"data": {
|
|
6
|
+
"order_id": 1042
|
|
7
|
+
},
|
|
8
|
+
"meta": {
|
|
9
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
10
|
+
"queue": "orders",
|
|
11
|
+
"lang": "php",
|
|
12
|
+
"schema_version": 1,
|
|
13
|
+
"created_at": 1749132727000
|
|
14
|
+
},
|
|
15
|
+
"attempts": 0
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"job": "urn:acme:a",
|
|
3
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
4
|
+
"data": {
|
|
5
|
+
"order_id": 1042
|
|
6
|
+
},
|
|
7
|
+
"meta": {
|
|
8
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
9
|
+
"queue": "orders",
|
|
10
|
+
"lang": "php",
|
|
11
|
+
"schema_version": 1,
|
|
12
|
+
"created_at": 1749132727000,
|
|
13
|
+
"ext": {
|
|
14
|
+
"acme": {
|
|
15
|
+
"tier": "gold",
|
|
16
|
+
"flags": [1, 2],
|
|
17
|
+
"limits": {
|
|
18
|
+
"burst": 10
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"globex": {
|
|
22
|
+
"region": "eu-west-1"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"attempts": 0
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"urn": "urn:acme:b",
|
|
3
|
+
"trace_id": "7b3f9c2a-e41d-4f88-9b2a-1c0d5e6f7a8b",
|
|
4
|
+
"data": {
|
|
5
|
+
"order_id": 1042
|
|
6
|
+
},
|
|
7
|
+
"meta": {
|
|
8
|
+
"id": "f1e2d3c4-b5a6-4789-90ab-cdef01234567",
|
|
9
|
+
"queue": "orders",
|
|
10
|
+
"lang": "php",
|
|
11
|
+
"schema_version": 1,
|
|
12
|
+
"created_at": 1749132727000
|
|
13
|
+
},
|
|
14
|
+
"attempts": 0
|
|
15
|
+
}
|