SummaryWe independently proved and reproduced the ACK temporal collision in Apache Pulsar Shared subscription — a race condition where a consumer crashes (or fails to ack in time) after storing a result but before the acknowledgment reaches the broker, causing the redelivered message to be processed twice. This is documented behavior (Pulsar at-least-once delivery with
The ScenarioPulsar-Specific DetailsPulsar Shared subscription differs from Kafka (offset commit) and NATS (AckWait) in two important ways:
Formal Proof (TLA+)We modeled the system as a TLA+ specification with the following actions:
Results:
Chaos Cross-ValidationWe built a pilot (
Results:
Cross-Broker ValidationWe adapted the same TLA+ model and chaos pilot for RabbitMQ, NATS JetStream, and Kafka. All results match:
Verdict: 8/8 TLC predictions matched 8/8 chaos experiments across 4 brokers. The FixThe recommended consumer-side fix is the idempotent consumer pattern: consumer = client.subscribe(
topic='persistent://public/default/test-topic',
subscription_name='test-sub',
subscription_type=SubscriptionType.Shared,
unacked_messages_timeout_ms=11000, # must be < processing time
)
while True:
msg = consumer.receive()
# Idempotent guard: check before processing
if should_skip(int(msg.data().decode())):
consumer.acknowledge(msg)
continue
store_result(int(msg.data().decode())) # INSERT into DB
time.sleep(15000) # simulate work (must be > ack timeout)
consumer.acknowledge(msg)The key insight: the DiscussionWe would love feedback from the Pulsar team and community:
|
Replies: 1 comment 1 reply
|
The main conclusion is right: a database commit and a Pulsar acknowledgment are not one atomic operation, so at-least-once delivery leaves a duplicate window. A few Pulsar-specific details are worth tightening in the model, though. First, the short Second, There is also a batch nuance. Current clients default
Finally, the sample guard needs to be atomic. This sequence is still racy under overlapping delivery: Use a unique constraint on the message/business ID and perform the claim plus side effect in one database transaction, for example So I would keep the core proof, but describe the invariant as “no duplicate committed effect under an atomic idempotency key,” not merely “check before processing.” Relevant API contracts: |
The main conclusion is right: a database commit and a Pulsar acknowledgment are not one atomic operation, so at-least-once delivery leaves a duplicate window. A few Pulsar-specific details are worth tightening in the model, though.
First, the short
ackTimeoutcreates the overlap in this experiment; it does not narrow the failure window. Pulsar disables that timeout by default. When it is enabled, an unacknowledged message is eligible for redelivery after the timeout, possibly to another consumer on a Shared subscription. Also, when a consumer terminates, its unacknowledged messages can be redelivered even with the timeout disabled. The broker does not wait indefinitely for that exact cons…