Files
atelier/examples/bad/messaging-shared-subscription.md
Jon Chery 29ffb42898 docs(milestone): complete v0.4 — Edge + Messaging + Language-Derived Docs
---ci---
project: atelier
phase: 0
milestone: v0.4
status: complete
requirements:
  covered: [ATELIER-92, ATELIER-93, ATELIER-94, ATELIER-95, ATELIER-96, ATELIER-97, ATELIER-98, ATELIER-99, ATELIER-100, ATELIER-101, ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105, ATELIER-106, ATELIER-107, ATELIER-108, ATELIER-109, ATELIER-110, ATELIER-111, ATELIER-112, ATELIER-113, ATELIER-114, ATELIER-115, ATELIER-116, ATELIER-117]
  partial: []
---/ci---
2026-08-05 16:23:15 +00:00

188 lines
8.6 KiB
Markdown

# Bad Example: Messaging Shared Subscription
> Two consumers share one subscription; the broker dispatches each
> message to an arbitrary consumer. Per-consumer ordering breaks;
> per-consumer dedup is wrong. This is the pre-specified
> `messaging-shared-subscription` chaos anti-pattern (ATELIER-110).
> **Single-breach per D-068:** the primary principle breached is
> **Messaging P2 (Ordering is a Property, Not an Assumption)**. P3
> (Consumers are Idempotent) is noted as the compounding
> consequence — the example remains single-breach in its named
> violation.
## The Code
```python
# Two email workers share ONE subscription on the "users" topic.
# The broker round-robins: worker A gets msg 1, msg 3; worker B
# gets msg 2, msg 4. Per-consumer order is broken (P2 breach).
# Each worker has its OWN dedup store — a redelivery to the other
# worker re-processes (P3 compounding).
import json
def shared_subscription_bad(broker, send_email, worker_id: str):
# P2 VIOLATION: both workers call subscribe with the SAME
# subscription name. The broker dispatches each message to an
# arbitrary worker in the shared group. Worker A sees msg 3
# before worker B sees msg 1; per-worker order is broken.
sub = broker.subscribe(topic="users", subscription="welcome-shared")
# P3 compounding: per-worker dedup. A redelivered message may
# land on the OTHER worker, which has not seen it, so the
# per-worker dedup store does not catch it — the message is
# processed twice across the two workers.
local_dedup = DedupStore(backend=redis_for(worker_id))
for message in sub.receive():
payload = json.loads(message["body"])
if local_dedup.seen(payload["idempotencyKey"]):
sub.ack(message); continue
send_email(payload["email"], "Welcome!")
local_dedup.mark(payload["idempotencyKey"])
sub.ack(message)
```
```python
# The two workers are launched with the same subscription name.
# Worker A and Worker B both call subscribe("users", "welcome-shared").
# The broker sees one shared subscription; it round-robins.
def start_workers(broker):
# Worker A
spawn(shared_subscription_bad, broker, send_email, worker_id="A")
# Worker B — same subscription name, same topic
spawn(shared_subscription_bad, broker, send_email, worker_id="B")
# The broker dispatches: A gets msg 1, B gets msg 2, A gets msg 3,
# B gets msg 4. If worker A is slow, B may get msg 2 and msg 4
# before A acks msg 1. Per-worker order is broken (P2).
```
The scenario: a user signs up, then immediately updates their
email preference (two messages in topic order: `user.signed-up.v1`,
`user.preference-updated.v1`). The broker dispatches `signed-up` to
worker A and `preference-updated` to worker B. Worker B sends the
preference email before worker A sends the welcome email — the
user sees the preference confirmation before the welcome. Then the
broker redelivers `signed-up` (worker A's ack was slow); this time
it dispatches to worker B. Worker B's local dedup store has never
seen `signed-up` (it was processed by A), so B sends the welcome
email again. The user receives two welcome emails. Per-consumer
ordering broke (P2); per-consumer dedup did not catch the
cross-worker redelivery (P3 compounding).
## Why It Violates
### Ordering is a Property, Not an Assumption (Messaging P2, C1, C2)
- **The breach (primary):** the two workers share one subscription,
and the broker dispatches each message to an arbitrary worker in
the shared group. Per-consumer ordering is broken: worker A sees
`preference-updated` (msg 2) before it sees `signed-up` (msg 1)
if the broker round-robins them to different workers. The
workers' code assumes topic order, but the shared subscription
provides no per-consumer order guarantee — the broker's dispatch
is arbitrary. P2 requires that the ordering property be explicit
and documented; here it is assumed (topic order) but not provided
(arbitrary dispatch). The assumption is wrong.
- The P2 contract is that "it's FIFO" is a claim backed by the
broker's partitioning contract, not an assumption the consumer
makes. A shared subscription's contract is "no per-consumer
order"; the workers' code assumes the opposite. See
`domains/messaging/pubsub.md` (Shared vs Independent
Subscriptions) and `domains/messaging/first-principles.md` P2.
### Consumers are Idempotent — compounding consequence (Messaging P3, C1)
- **The compounding consequence (not the named breach):** each
worker has its OWN dedup store. A redelivered message that lands
on the *other* worker is not in that worker's dedup store, so it
is processed again. The dedup is per-worker, but the subscription
is shared — the dedup must be shared across workers to be
correct under a shared subscription. The per-worker dedup store
is wrong for a shared subscription; a shared dedup store (a
shared Redis, a shared DB) is required.
- Per D-068, the example remains single-breach in its named
violation: P2 is the primary breach (the shared subscription
breaks ordering); P3 is the compounding consequence (the
per-worker dedup is wrong *because* the subscription is shared).
If the subscription were independent, per-worker dedup would be
correct. The shared subscription is the root cause; P3 is the
downstream effect.
## The Fix
```python
# Fix 1 (default): independent subscriptions. Each consumer gets
# its own durable cursor; per-consumer order holds (P2); per-
# consumer dedup is correct (P3). This is the default per
# domains/messaging/pubsub.md.
def independent_subscriptions_good(broker, send_email, worker_id: str):
# Each worker has its OWN subscription name. The broker
# delivers every message to every subscription in topic order.
sub = broker.subscribe(
topic="users",
subscription=f"welcome-{worker_id}", # per-consumer
)
# Per-consumer dedup is now correct: a redelivery to THIS
# worker is caught by THIS worker's dedup store.
dedup = DedupStore(backend=redis_for(worker_id))
for message in sub.receive():
payload = json.loads(message["body"])
if dedup.seen(payload["idempotencyKey"]):
sub.ack(message); continue
send_email(payload["email"], "Welcome!")
dedup.mark(payload["idempotencyKey"])
sub.ack(message)
```
```python
# Fix 2 (if a shared subscription is genuinely required): the
# consumers must be stateless, the processing order-independent,
# AND the dedup store must be SHARED across workers. Document the
# ordering property as "none across consumers" (P2 — the property
# is explicit, not assumed) and use a shared dedup backend (P3).
def shared_subscription_stateless(broker, send_email):
# P2: document the ordering property. A shared subscription
# provides NO per-consumer order; processing must be order-
# independent. Do not assume topic order.
sub = broker.subscribe(topic="users", subscription="welcome-shared")
# P3: SHARED dedup. A redelivery to any worker is caught by the
# shared store.
shared_dedup = DedupStore(backend=shared_redis)
for message in sub.receive():
payload = json.loads(message["body"])
if shared_dedup.seen(payload["idempotencyKey"]):
sub.ack(message); continue
send_email(payload["email"], "Welcome!") # order-independent
shared_dedup.mark(payload["idempotencyKey"])
sub.ack(message)
```
The default is Fix 1 (independent subscriptions): per-consumer
ordering holds, per-consumer dedup is correct, and the code is
simpler. Fix 2 is the narrow opt-in for genuinely stateless,
order-independent processing — and even then, the dedup must be
shared. See `domains/messaging/pubsub.md` for the full
shared-vs-independent discussion.
## Cross-Domain Links
- `domains/messaging/pubsub.md` — the shared-vs-independent
subscriptions section; the `messaging-shared-subscription`
anti-pattern lives here (pre-specified in P4 ATELIER-110).
- `domains/messaging/first-principles.md` — P2 (Ordering is a
Property, Not an Assumption) is the primary breach; P3
(Consumers are Idempotent) is the compounding consequence.
- `domains/messaging/delivery-semantics.md` — the idempotency-key
dedup store; the per-subscription dedup key scoping
(`(subscription, idempotencyKey)`) that prevents one
subscription's dedup from masking another's redelivery.
- `review/anti-patterns.md` — the `messaging-shared-subscription`
chaos anti-pattern (messaging P2, P3, C1 — shared subscription
breaks ordering and dedup).
- `examples/good/messaging-idempotent-consumer.md` — the good-
example version of the consumer pattern: an independent consumer
with a dedup store and a DLQ routing rule.