sanghyeok An created KAFKA-21248:
------------------------------------
Summary: AsyncKafkaConsumer may auto-commit offsets for records
not yet returned to the application
Key: KAFKA-21248
URL: https://issues.apache.org/jira/browse/KAFKA-21248
Project: Kafka
Issue Type: Bug
Reporter: sanghyeok An
Assignee: sanghyeok An
{{AsyncKafkaConsumer}} may include offsets for records that have not yet been
returned to application code in a periodic auto-commit. If the commit succeeds
and the application terminates before processing those records, they may be
skipped after restart, violating at-least-once processing semantics.
This can happen even when the application processes every record returned by
one {{poll()}} before calling {{poll()}} again:
{code:java}
while (running) {
ConsumerRecords<String, String> records = consumer.poll(timeout);
for (ConsumerRecord<String, String> record : records) {
process(record);
}
} {code}
{{AsyncKafkaConsumer.poll()}} submits an {{AsyncPollEvent}} to the background
thread. When buffered records are available and the required conditions are
satisfied, the application thread can collect those records before the event
has finished processing.
Collecting records advances {{SubscriptionState.position}} to the next offset
to read. This update occurs before {{poll()}} returns to the application and
does not indicate that application code has finished processing the records.
However, the background thread calls {{subscriptions.allConsumed()}} when
processing the {{AsyncPollEvent}} to determine which offsets to auto-commit. It
can therefore use a position that the application thread has already advanced
within the current {{poll()}} call.
Consider the following state for a single partition:
* {{{}enable.auto.commit=true{}}}, and the auto-commit interval has elapsed.
* The application has finished processing all records through offset 99.
* Both the committed offset stored on the broker and the current consumer
position are 100.
* Records at offsets 100 and 101 are available in the fetch buffer.
The following execution sequence is possible:
# *Application code calls {{{}consumer.poll(){}}}.*
All records from the previous {{poll()}} have already been processed. The
records at offsets 100 and 101 have not yet been delivered to application code.
# *The application thread submits an {{{}AsyncPollEvent{}}}.*
Before the background thread processes this event, the application thread can
proceed to collect buffered records.
# *The application thread collects the records at offsets 100 and 101 and
advances the position to 102.*
At this point, {{poll()}} may not have returned yet. The internal consumer
position is 102, but application code may still be waiting to receive the two
records.
# *The background thread processes the delayed {{{}AsyncPollEvent{}}}.*
Since the auto-commit interval has elapsed, it calls {{{}allConsumed(){}}}.
This method reads the current shared state and returns offset 102, which is
then included in the auto-commit request.
# *The background thread sends the commit request, and the broker stores
offset 102.*
This can complete while execution of the application thread is delayed.
Consequently, a resume position that skips these records can be stored before
they have even been returned to the application.
# *The application terminates before {{poll()}} returns.*
The records at offsets 100 and 101 have never been delivered to application
code, so {{process(record)}} has not been called for either record. However,
offset 102 has already been committed on the broker.
# *The same consumer group resumes consumption from the committed offset.*
Consumption starts at offset 102. The records at offsets 100 and 101 are
skipped without ever having been processed by application code.
In this example, the safe offset to commit when the current {{poll()}} begins
is 100. Reading the shared position when the event is processed instead
produces offset 102, which includes the records collected by the current
{{{}poll(){}}}.
The issue is a race between the position update inside {{poll()}} and the
background thread’s determination of the offsets to auto-commit. It does not
depend on how quickly application code processes returned records: the commit
can advance before those records are delivered to the application.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)