← Start of tutorial

WIUNIX ACADEMY

RabbitMQ quorum queues: confirms, redelivery and idempotent effects

Page 2 of 2. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

RHEL / Rocky Linux / AlmaLinux 9 — existing installation checksReviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment8 editable values

The synthetic confirm/commit/ACK experiment

Save the full file below. It refuses an existing ledger and an existing queue. It first expects an unroutable mandatory error, then confirms one routed persistent message. A first consumer commits one unique-ID effect to SQLite and closes without ACK; the second receives it again, verifies the existing effect and ACKs. The ledger models one local transactional effect, not a real payment or distributed transaction.

python

Provisioned staging client with Pika1.3+

import getpass, os, sqlite3, ssl, uuid, time
from pathlib import Path
import pika

root = Path('/home/academy/rabbitmq-probe-20261028')
ledger = root / 'effect-ledger.sqlite'
fd = os.open(ledger, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
os.close(fd)
db = sqlite3.connect(ledger)
db.execute('PRAGMA synchronous=FULL')
db.execute('CREATE TABLE effect(message_id TEXT PRIMARY KEY, body BLOB NOT NULL)')
db.commit()
password = getpass.getpass('Staging RabbitMQ password: ')
tls = ssl.create_default_context(cafile='/etc/pki/tls/certs/academy-rabbitmq-ca.pem')
def connect():
    return pika.BlockingConnection(pika.ConnectionParameters(
        host='rabbitmq.example.com', port=5671, virtual_host='academy_lab',
        credentials=pika.PlainCredentials('academy_probe', password),
        ssl_options=pika.SSLOptions(tls, 'rabbitmq.example.com'),
        heartbeat=30, blocked_connection_timeout=10, socket_timeout=5,
        connection_attempts=1))

queue = 'academy_ack_probe_20261028'
message_id = 'academy_effect_20261028'
conn = connect()
ch = conn.channel()
try:
    ch.queue_declare(queue=queue, passive=True)
except pika.exceptions.ChannelClosedByBroker as exc:
    if exc.reply_code != 404:
        raise
    ch = conn.channel()
else:
    raise RuntimeError('Queue already exists: stop; do not reuse it')
ch.queue_declare(queue=queue, durable=True,
                 arguments={'x-queue-type':'quorum', 'x-quorum-initial-group-size':3})
ch.confirm_delivery()
props = pika.BasicProperties(delivery_mode=2, message_id=message_id,
                             content_type='text/plain')
try:
    ch.basic_publish(exchange='', routing_key='absent_' + uuid.uuid4().hex,
                     body=b'negative-route-probe', properties=props, mandatory=True)
except pika.exceptions.UnroutableError:
    print('PASS: unroutable mandatory publish reported')
else:
    raise RuntimeError('Missing negative routing result')
ch.basic_publish(exchange='', routing_key=queue, body=b'academy-synthetic-effect',
                 properties=props, mandatory=True)
print('PASS: routed publish confirmed')
conn.close()

for attempt in (1,2):
    conn = connect()
    ch = conn.channel()
    deadline = time.monotonic() + 10
    while True:
        method, properties, body = ch.basic_get(queue=queue, auto_ack=False)
        if method is not None or time.monotonic() >= deadline:
            break
        conn.process_data_events(time_limit=0.1)
        time.sleep(0.1)
    if method is None or properties.message_id != message_id or body != b'academy-synthetic-effect':
        raise RuntimeError('Unexpected delivery: preserve state and inspect')
    if attempt == 2 and not method.redelivered:
        raise RuntimeError('Expected redelivery after unacknowledged close')
    with db:
        db.execute('INSERT OR IGNORE INTO effect VALUES (?,?)',(message_id,body))
        stored = db.execute('SELECT body FROM effect WHERE message_id=?',(message_id,)).fetchone()
        if stored[0] != body:
            raise RuntimeError('Message-ID collision with different effect')
    if attempt == 1:
        print('PASS: synthetic effect committed; closing without ACK')
    else:
        ch.basic_ack(delivery_tag=method.delivery_tag)
        print('PASS: redelivery deduplicated, then ACK sent')
    conn.close()
assert db.execute('SELECT count(*) FROM effect').fetchone()[0] == 1
db.close()
print('PASS: exactly one synthetic ledger row; not an exactly-once service claim')
04

LET’S BUILD

Run once with a bounded observation window

This creates a fresh quorum queue and synthetic ledger in the dedicated staging scope. Do not rerun blindly if it times out or the connection breaks: broker acceptance may already have happened. Exit124 means the outer time boundary ended, not that no message exists. Preserve queue, ledger and ID for diagnosis. A normal completion prints the routing check, confirm, commit-before-ACK gap, redelivery and one ledger row.

bash

Provisioned staging client with Pika1.3+

timeout 60s python3 '/home/academy/rabbitmq-probe-20261028/confirm-ack-probe.py'
How do I know it worked?

All PASS lines appear once, including redelivery and one synthetic effect.

05

LET’S BUILD

Verify the actual queue membership and final counts

On the staging node, inspect the exact queue. The declared initial group size is an intention, not evidence that all replicas are online. Confirm three members with a healthy majority before calling the queue replicated. After the consumer closes, ready/unacknowledged counts should settle to zero. A sent ACK and a local print are weaker evidence than observing settled broker counts.

bash

Isolated three-node RabbitMQ staging cluster

rabbitmq-queues quorum_status --vhost 'academy_lab' 'academy_ack_probe_20261028'
rabbitmqctl list_queues -p 'academy_lab' name type messages_ready messages_unacknowledged consumers
How do I know it worked?

Actual replica membership and settled zero delivery counts are recorded.

A lost confirm means unknown, not definitely lost

A timeout after socket write can leave acceptance unknown. Keep an outbox entry pending until a confirm/return outcome is processed, retry the same logical ID when policy allows, and make the consumer idempotent. The broker does not deduplicate ordinary messages merely because message_id matches. A real consumer must commit its dedup marker and business effect atomically in the same durable database transaction; an external side effect needs its own idempotency protocol.

Classify failures without weakening safety

A TLS error requires checking trust, certificate name and clock, not insecure TLS. PRECONDITION_FAILED means declaration arguments conflict with an existing queue; preserve it. Resource alarms and missing quorum can delay or fail publishing; inspect broker health rather than assuming every timeout means loss. Consumer ACKs must use delivery tags on the same channel that delivered them. Poison-message loops need bounded retry and a deliberate dead-letter policy, not infinite requeue.

Retain evidence; do not purge to make counts look clean

No broker node was stopped and no partition was simulated, so the exercise is not failover certification. Retain queue topology, stable ID, output, ledger hash and final counts under the staging owner’s retention plan. Cleanup of the fresh queue and private root is a separate exact lifecycle action after evidence review; never purge a shared vhost or recursively delete a guessed path.

GO TO THE SOURCE

Official references

Each environment states how it was reviewed. A documentation review does not claim a hands-on test of every version.

Explore all tutorials ↗