Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Retries and idempotent consumers

After this lesson: I can design a consumer that survives retries.

60 min lesson6 commandsKafka lab: topics, producers, consumers, partitions and offsets in the browser

Why a payment can arrive twice

At-least-once delivery means a message is never lost, but it may arrive more than once. Networks drop replies, so a sender that does not hear back sends again.

If your consumer adds a payment each time it sees one, a retry takes the money twice. You will learn how to build a consumer that survives retries.

By the end of this lesson

Note

I can design a consumer that survives retries.

Key words

WordWhat it means
At-least-once deliveryno message is lost, but some repeat
Retrysending again after no answer
Duplicatea message that arrives a second time
Idempotentdoing it twice has the same effect as once
Offsetthe place number of a message in the log
Commitsaving the offset you have finished

The cells students fill in are left blank in this preview.

A lost reply makes the sender try again

A producer sends a message and waits for an acknowledgement, a short reply that says it arrived. If the reply is lost, the producer cannot tell whether the message arrived. The safe choice is to send it again.

That is a retry. The log now holds the message twice. Both copies are real messages as far as the broker can tell.

Three delivery guarantees

NameWhat the sender doesWhat can go wrong
At most onceNever retriesA message can be lost
At least onceRetries until acknowledgedA message can repeat
Exactly onceRetries, and the receiver ignores repeatsNeeds extra work in the receiver

The cells students fill in are left blank in this preview.

Make a topic of one partition

Command
curl -s -X POST -H "Content-Type: application/json" -d '{"name":"payments","partitions":1,"replicationFactor":1,"configs":{}}' http://kafka-ui:8080/api/clusters/lab/topics -o /dev/null -w "%{http_code}\n"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Send payment p17

Command
curl -s -X POST -H "Content-Type: application/json" -d '{"partition":0,"key":"p17","value":"paidada","headers":{},"keySerde":"String","valueSerde":"String"}' http://kafka-ui:8080/api/clusters/lab/topics/payments/messages -o /dev/null -w "%{http_code}\n"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Send p17 again, as a retry would

Command
curl -s -X POST -H "Content-Type: application/json" -d '{"partition":0,"key":"p17","value":"paidada","headers":{},"keySerde":"String","valueSerde":"String"}' http://kafka-ui:8080/api/clusters/lab/topics/payments/messages -o /dev/null -w "%{http_code}\n"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Send a different payment, p18

Command
curl -s -X POST -H "Content-Type: application/json" -d '{"partition":0,"key":"p18","value":"paidbo","headers":{},"keySerde":"String","valueSerde":"String"}' http://kafka-ui:8080/api/clusters/lab/topics/payments/messages -o /dev/null -w "%{http_code}\n"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Look at the offset of the next message

Command
curl -s http://kafka-ui:8080/api/clusters/lab/topics/payments | grep -o '"partition":[0-9]*\|"offsetMax":[0-9]*'

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Read the whole topic

Command
curl -s -N "http://kafka-ui:8080/api/clusters/lab/topics/payments/messages/v2?seekType=BEGINNING&limit=10" | grep -o '"partition":[0-9]*,"offset":[0-9]*\|"value":"[a-z]*"'

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A crash before a commit repeats work

A consumer reads messages in order and saves its place by committing an offset. After a restart it carries on from the last committed offset.

If it handles a message and crashes before it commits, the restart reads that message again. Committing before handling avoids the repeat, but a crash then loses the message. Most systems choose the repeat.

Worked example: a crash between handling and commit

StepOffset handledCommitted offsetBalance
Handle offset 0 (amount 5)005
Handle offset 1 (amount 3)108
Crash, then restart at 00 again013
Handle offset 1 again1 again016

The cells students fill in are left blank in this preview.

Watch a restart add the payments twice

Code: python
# A consumer adds up payments. It crashes before it saves its place.
log = [5, 3, 4]
balance = 0
committed = 0
for offset in range(committed, 2):      # reads offsets 0 and 1, then crashes
    balance = balance + log[offset]
print("Before the crash: balance", balance, "committed", committed)
for offset in range(committed, len(log)):   # the restart begins at the saved place
    balance = balance + log[offset]
print("After the restart: balance", balance, "but the true total is 12")
# Add committed = 2 above the restart loop and run again.

A common mistake

Warning

Many people think the log removes a repeated message. A plain log keeps every message that is written, as the lab shows. Your consumer must spot a repeat itself, by an id inside the message, not by its offset.

An idempotent consumer skips repeated messages

An action is idempotent when doing it twice has the same effect as doing it once. Setting a value to 7 is idempotent. Adding 5 is not.

To make adding safe, give each message an id and keep a record of the ids you have handled. If an id is already there, skip the message. Save the record and the result in the same step, so a crash cannot split them.

Skip an id that was already handled

Code: python
# Each payment has an id. Remember the ids you have handled.
seen = {}
total = 0
for message in [["p17", 5], ["p17", 5], ["p18", 3]]:
    if message[0] in seen:
        print(message[0], "already handled, skipped")
    else:
        seen[message[0]] = True
        total = total + message[1]
        print(message[0], "added, total", total)
# Change the second p17 to p19 and run again.

About this lab and its source: Kafka lab: topics, producers, consumers, partitions and offsets in the browser

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.