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.
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.
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/ssl/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')
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.
Provisioned staging client with Pika1.3+
timeout 60s python3 '/home/academy/rabbitmq-probe-20261028/confirm-ack-probe.py'All PASS lines appear once, including redelivery and one synthetic effect.
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.
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 consumersActual 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.
- RabbitMQ: quorum queues
- RabbitMQ: confirms and acknowledgements
- RabbitMQ: queue tools
- Pika: blocking adapter
- RabbitMQ: reliability
- Compatibility and installation path: RabbitMQ / Debian, Ubuntu
- Compatibility and installation path: RabbitMQ / RPM
- Python: virtual environments
- Red Hat: diagnosing SELinux access denials
- Red Hat: firewalld inspection and management
- Red Hat: Python versions and application runtime
- Red Hat: Python versions and application runtime