[
https://issues.apache.org/jira/browse/CAMEL-25756?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen resolved CAMEL-25756.
---------------------------------
Resolution: Fixed
> camel-kafka - batching consumer loses the records of an incomplete batch when
> a poll fails (e.g. a record that cannot be deserialized,
> pollOnError=ERROR_HANDLER): they are never routed and the next batch commits
> past them
> -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-25756
> URL: https://issues.apache.org/jira/browse/CAMEL-25756
> Project: Camel
> Issue Type: Bug
> Components: camel-kafka
> Reporter: shashank
> Assignee: shashank
> Priority: Major
> Fix For: 4.24.0
>
>
> With {{batching=true}} the Kafka consumer keeps the records of several polls
> in {{KafkaRecordBatchingProcessor.exchangeList}} until the batch is complete
> ({{maxPollRecords}} records), the poll timeout expires or
> {{batchingIntervalMs}} passes (since CAMEL-20380). The processor lives in the
> record processor facade, and {{KafkaFetchRecords.startPolling()}} (main
> ddca15435112, line 333) creates a new facade every time it is entered:
> {code:java}
> final KafkaRecordProcessorFacade recordProcessorFacade =
> createRecordProcessor();
> {code}
> {{startPolling()}} is entered again, on the same connected Kafka consumer,
> after every exception of the polling loop that the poll exception strategy
> continues on (line 396 and the do-while at line 174): a failed {{poll}} with
> the default {{pollOnError=ERROR_HANDLER}}, and with {{DISCARD}} or {{RETRY}}.
> The new processor starts with an empty batch, so the records held for the
> incomplete batch are dropped. The Kafka consumer does not return them again
> (its position is already past them), and the next complete batch commits its
> highest offset per partition, which is past them. Once a later batch with
> records of the same partition is committed, they are never redelivered, even
> after a restart or a rebalance. Nothing is logged about them. (If no later
> batch of that partition is committed before the consumer stops or the
> partition is revoked, they are polled again from the committed offset:
> redelivery, not loss. With {{allowManualCommit}} the commit past them is the
> route's own {{KafkaManualCommit}} of a later batch.)
> A realistic trigger: a record that cannot be deserialized by a deserializer
> that can fail (Avro, JSON schema, a custom one; the default
> {{StringDeserializer}} does not fail). {{poll}} throws
> {{RecordDeserializationException}}, the default error handler skips that one
> record ({{SeekUtil}}, CAMEL-23994), and the good records polled before it, on
> every partition, are lost with it. Up to {{maxPollRecords - 1}} records are
> lost per failed poll. Other poll exceptions the strategy continues on (for
> example a {{KafkaException}} for a corrupt record batch) drop the held
> records the same way. An authentication or authorization error ends the
> consumer with {{ERROR_HANDLER}}; nothing is committed past the held records
> then.
> Example ({{batching=true&maxPollRecords=10}}, one partition): a poll returns
> offsets 0-2 (held, the batch is not complete); the next poll fails on the
> record at offset 3; the error handler seeks to 4; the next poll returns 4-13;
> the batch 4-13 is routed and offset 14 is committed. Offsets 0-2 are never
> routed.
> *Fix* (one class): {{KafkaFetchRecords}} creates the record processor facade
> when the Kafka consumer is connected (and reconnected) and keeps it while the
> consumer is connected, so a failed poll keeps the held records and they are
> routed and committed with the next batch. After a reconnect a new Kafka
> consumer starts from the committed offsets, so a new facade (and an empty
> batch) is still created then. The streaming facade holds no state between
> polls; it is unaffected.
> *Tests*: new {{KafkaBatchingConsumerPollErrorTest}} (2 tests, a real route
> and Kafka's {{MockConsumer}}; {{batching=true&maxPollRecords=10}}): three
> records are polled and held, a poll fails with a {{KafkaException}} (first
> test) or a {{RecordDeserializationException}} for offset 3 (second test),
> then ten more records arrive. Both fail on main ({{expected: <[a0, a1, a2,
> b0, b1, b2, b3, b4, b5, b6]> but was: <[b0, b1, b2, b3, b4, b5, b6, b7, b8,
> b9]>}}) and pass with the fix; the camel-kafka unit tests pass (222 run, 0
> failures, 1 skipped).
> *How it was found*: deterministic simulation testing with a fake Kafka world
> (the camel-kafka consumer, its fetcher loop, commit managers and rebalance
> listener real; a fake {{Consumer}} written from the KafkaConsumer Javadoc of
> kafka-clients 4.3, bound through {{kafkaClientFactory}}). The seed picked the
> configuration (streaming or batching, {{autoCommitEnable}}, manual commit
> with the sync / async / no-op commit manager, {{breakOnFirstError}},
> {{maxPollRecords}} 1 to 50, {{pollTimeoutMs}}, {{batchingIntervalMs}}),
> poison records, failed polls and commits, partial fetches, route restarts and
> suspends. The invariant "every record sent reaches the route" failed in 103
> of 1000 seeds on main, all with {{batching=true}} and a failed poll (first
> failing seed 7); with the fix, 0 of these 1000 seeds fail it with
> {{batching=true}}.
> *Affected*: main, camel-4.23.x, camel-4.22.x, camel-4.18.x and camel-4.14.x
> (same {{createRecordProcessor()}} call in {{startPolling()}} and the same
> batch kept across polls; read, not run). On camel-4.18.x and camel-4.14.x the
> error handler's seek is the one before CAMEL-23994, the held records are
> dropped the same way.
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)