Federico Mariani created CAMEL-25029:
----------------------------------------

             Summary: camel-kafka - Add a consumer for Kafka share groups 
(KIP-932, Queues for Kafka)
                 Key: CAMEL-25029
                 URL: https://issues.apache.org/jira/browse/CAMEL-25029
             Project: Camel
          Issue Type: New Feature
          Components: camel-kafka
            Reporter: Federico Mariani


h3. Motivation

Kafka share groups (KIP-932, "Queues for Kafka") let consumers work through the 
records of a topic like a queue:
* several consumers can read the same partition, so the number of consumers is 
no longer capped by the number of partitions
* the application acknowledges each record: {{ACCEPT}}, {{RELEASE}} 
(redeliver), {{REJECT}} (discard) or {{RENEW}} (extend the acquisition lock)
* the broker handles redelivery and counts deliveries (exposed as 
{{ConsumerRecord.deliveryCount()}}); a failing record does not block its 
partition, and records are archived after {{group.share.delivery.count.limit}} 
deliveries
* no offsets, no seeking, no topic patterns, no ordering guarantees, no 
transactions

The client API is available in the kafka-clients version Camel already uses 
(4.3.1: {{KafkaShareConsumer}}, {{AcknowledgeType}}), so this needs no 
dependency upgrade.

Typical uses:
* work queues for slow, independent tasks (LLM/embedding calls, OCR, 
rate-limited APIs) that need to scale beyond the partition count
* replacing a JMS/AMQP work queue with Kafka, without a second broker
* consuming the same topic in two ways: an ordered consumer group for 
projections, and a share group of workers for unordered side effects

h3. Proposal

A new consumer-only scheme, {{kafka-share:topic}}, in the camel-kafka module, 
with its own endpoint, configuration and consumer.

It would *not* be an option on {{kafka:}}, because about 14 of the 34 existing 
consumer options do not apply to a share consumer ({{seekTo}}, 
{{autoOffsetReset}}, {{autoCommitEnable}}, {{autoCommitIntervalMs}}, 
{{allowManualCommit}}, {{commitTimeoutMs}}, {{partitionAssignor}}, 
{{groupProtocol}}, {{groupRemoteAssignor}}, {{groupInstanceId}}, 
{{topicIsPattern}}, {{isolationLevel}}, {{batching}}, {{batchingIntervalMs}}). 
The current consumer is built around offsets, commit managers and rebalance 
listeners, none of which exist here. A separate scheme keeps the catalog and 
tooling accurate. Brokers, security, serdes, {{KafkaClientFactory}}, the header 
filter strategy and the health check infrastructure would be shared within the 
module. Producing stays on {{kafka:}}.

Acknowledgement is derived from the exchange outcome (explicit acknowledgement 
mode):
|| Exchange outcome || Acknowledgement ||
| completed, or failed with {{handled(true)}} | {{ACCEPT}} |
| failed, or rolled back (transacted route) | configurable, default {{RELEASE}} 
|
| header set by the route | {{ACCEPT}} / {{RELEASE}} / {{REJECT}} |
| still inflight when the lock is about to expire (optional) | {{RENEW}} |

The default {{RELEASE}} lets the broker redeliver, possibly to another 
instance; the broker's delivery count limit prevents endless redelivery of 
poison messages.

Headers: delivery count, topic, partition, offset, key, timestamp.

h3. Examples

_Option and header names are illustrative._

*1. Work queue that scales past the partition count*
{code:java}
from("kafka-share:support-tickets?brokers={{kafka.brokers}}&groupId=ticket-triage&consumersCount=20")
    .to("langchain4j-chat:triage")
    .to("kafka:tickets-triaged?brokers={{kafka.brokers}}");
{code}
20 concurrent consumers on a topic with 3 partitions; a slow ticket does not 
hold up the others.

*2. Broker-driven redelivery with a delivery-count cutoff*
{code:java}
errorHandler(noErrorHandler()); // no local retries: the broker redelivers, 
possibly to another pod

onException(InvalidInvoiceException.class)
    .handled(true)                                        // handled -> ACCEPT
    .to("kafka:invoices-invalid?brokers={{kafka.brokers}}");

from("kafka-share:invoices?brokers={{kafka.brokers}}&groupId=invoice-ocr")
    
.filter(header(KafkaConstants.SHARE_DELIVERY_COUNT).isGreaterThanOrEqualTo(4))
        .setHeader(KafkaConstants.SHARE_ACKNOWLEDGE, constant("REJECT"))
        .to("kafka:invoices-dlq?brokers={{kafka.brokers}}")
        .stop()
    .end()
    .to("http://ocr-service/extract";)
    .to("kafka:invoices-extracted?brokers={{kafka.brokers}}");
{code}

h3. Notes and follow-ups
* *Documentation - {{consumersCount}}:* each consumer thread owns one 
{{KafkaShareConsumer}} (the client is not thread-safe). Unlike classic 
consumers, consumers beyond the partition count are not idle: all of them 
receive records.
* *Documentation - delivery semantics:* delivery is at-least-once. A record can 
be processed again if its acquisition lock expires during processing, if the 
JVM stops after the route's side effects but before the acknowledgement reaches 
the broker, if committing the acknowledgements fails, or if the route releases 
a record after a partial side effect. Pair with the Idempotent Consumer where 
duplicates matter.
* *Manual acknowledgement:* should use the planned acknowledgement API, the 
share-group equivalent of {{KafkaManualCommit}}. Acknowledgements made from 
another thread (async hand-off) have to be applied on the polling thread.
* *Follow-up - virtual threads:* split polling from processing, i.e. a few 
pollers, each record of a poll processed on a virtual thread, acknowledgements 
queued back to the poller. This gives high concurrency for I/O-bound routes 
without one Kafka client per concurrent exchange.
* Health check criteria, since there is no partition assignment to report.

h3. References
* KIP-932: 
https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to