Files
atelier/examples/bad/messaging-shared-subscription.md
T
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

8.6 KiB

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

# 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)
# 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

# 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)
# 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.

  • 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.