<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The Commit Log]]></title><description><![CDATA[Why is log compaction stalling in Kafka? Learn how the dirty ratio formula and growing clean logs freeze cleaner threads, and how to properly tune them.]]></description><link>https://offsetzero.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aabef54e72a4d0dc1a64f55/fbf4f518-8927-4536-8766-90393a35c529.jpg</url><title>The Commit Log</title><link>https://offsetzero.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 07 Oct 2026 07:10:20 GMT</lastBuildDate><atom:link href="https://offsetzero.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Handling Long-Running Tasks in KafkaShareConsumer]]></title><description><![CDATA[The Legacy Pitfall
With legacy KafkaConsumer, achieving at-least-once processing for long-running tasks often leads to a costly operational failure: if processing time exceeds max.poll.interval.ms, th]]></description><link>https://offsetzero.hashnode.dev/handling-long-running-tasks-in-kafkashareconsumer</link><guid isPermaLink="true">https://offsetzero.hashnode.dev/handling-long-running-tasks-in-kafkashareconsumer</guid><category><![CDATA[Kafkashareconsumer]]></category><category><![CDATA[kip-932]]></category><category><![CDATA[kafka]]></category><category><![CDATA[kafkaqueue]]></category><dc:creator><![CDATA[shubham shirur]]></dc:creator><pubDate>Wed, 30 Sep 2026 00:51:48 GMT</pubDate><content:encoded><![CDATA[<h3>The Legacy Pitfall</h3>
<p>With legacy <code>KafkaConsumer</code>, achieving at-least-once processing for long-running tasks often leads to a costly operational failure: if processing time exceeds <code>max.poll.interval.ms</code>, the consumer is flagged as dead and ejected from the consumer group. Any offsets committed for that offset by that consumer instance are rejected by the broker, leaving the partition stuck and requiring manual intervention from the ops team to reset offsets. <strong>KIP-932</strong> eliminates this headache through <code>KafkaShareConsumer</code> and <code>AcknowledgeType.RENEW</code>. Here's how it works.</p>
<h3>Workflow Comparison: KafkaConsumer vs KafkaShareConsumer</h3>
<p>Legacy <code>KafkaConsumer</code> workflow, illustrating the flow which can lead to pitfall if <code>process(record)</code> takes longer time than that of <code>max.poll.interval.ms</code> (default 5 minutes).</p>
<img src="https://cdn.hashnode.com/uploads/covers/6aabef54e72a4d0dc1a64f55/4aad8959-8031-4415-aced-87563a3910af.png" alt="" style="display:block;margin:0 auto" />

<hr />
<p>Let's see how this pitfall can be avoided using <code>KafkaShareConsumer</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6aabef54e72a4d0dc1a64f55/272aca45-5e41-4558-8266-e083ca515fa7.png" alt="" style="display:block;margin:0 auto" />

<p>The key idea is offloading work from the main thread. By delegating message processing to background worker threads, the main thread remains free to poll the broker well before <code>max.poll.interval.ms</code> expires. However, this decoupling only works because <code>KafkaShareConsumer</code> allows fine-grained, record-level acknowledgments. Using <code>acknowledge(record, AcknowledgeType.RENEW)</code>, worker threads can extend their locks on long-running records while the main thread signals ongoing liveliness back to the broker with every <code>poll()</code> call.</p>
<p>When <code>poll()</code> is called, the broker re-delivers any records previously acknowledged with <code>AcknowledgeType.RENEW</code> so the consumer instance can maintain its active locks on them. However, because <code>poll()</code> returns these same records on subsequent calls until they are finalized with <code>ACCEPT</code>, <code>REJECT</code>, or <code>RELEASE</code>, it is must to prevent worker threads from processing them twice. Maintaining an in-memory <code>ConcurrentHashMap</code> tracks active record states, ensuring newly polled records that are already in flight get ignored while background threads finish execution.</p>
<p>Once all the <code>ConsumerRecords</code> are processed and finalized with <code>ACCEPT</code>, <code>REJECT</code>, or <code>RELEASE</code>, then <code>poll()</code> returns next batch of records.</p>
<p>The Lock Concept</p>
<p><code>share.record.lock.duration.ms</code> (default 30 seconds) is a configuration introduced with KIP-932 defining the ownership of <code>ConsumerRecords</code> during <strong>that</strong> duration of time window.<br />Lets understand with simple example, if <code>Instance1</code> and <code>Instance2</code> of <code>share-group-A</code> are subscribed to single partitioned <code>topic-A</code>, polling offset 0 to 4 and offset 5-9 respectively. As per the default value, <code>Instance1</code> and <code>Instance2</code> have exactly 30 seconds to commit those records.</p>
<p>If an instance fails to finalize processing within this duration, the broker releases the record lock, making those messages available to other instances in the share group, which can cause duplicate processing. Periodically committing with <code>ACKTYPE.RENEW</code> extends lock ownership notifying broker and keeps long-running tasks safely in flight.</p>
<h3>Putting It All Together in the Code</h3>
<p><em>Check out</em> <a href="https://github.com/shubhamsworking/OffsetZero"><em>ShareConsumerRenewAcknowledgeTypeExample</em></a> <em>on GitHub to test this locally with Apache Kafka 4.2.0</em></p>
<pre><code class="language-java">    private static final ConcurrentHashMap&lt;ConsumerRecord&lt;String, EventMessage&gt;, Future&lt;Boolean&gt;&gt; underExecutionRecords = new ConcurrentHashMap&lt;&gt;(5);

    private static final ExecutorService processExecutorService = Executors.newFixedThreadPool(5);
</code></pre>
<p>The <code>underExecutionRecords</code> map acts as an in-flight state registry to track active tasks and prevent duplicate processing; when <code>SHARE_ACQUIRE_MODE_CONFIG</code> is set to <code>record_limit</code>, its size is strictly bounded by <code>max.poll.records</code>.</p>
<p>Pairing this with <code>processExecutorService</code> capped at or below <code>max.poll.records</code> ensures optimal concurrency while preventing thread starvation and maximizing efficient CPU utilization across worker threads.</p>
<pre><code class="language-java">    Properties properties = new Properties();
    properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    properties.put(ConsumerConfig.GROUP_ID_CONFIG, "offsetzero-acknowledge-type-example");
    properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
    properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, EventMessageDeserializer.class.getName());
    properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "5");
    properties.put(ConsumerConfig.SHARE_ACKNOWLEDGEMENT_MODE_CONFIG, "explicit");
    properties.put(ConsumerConfig.SHARE_ACQUIRE_MODE_CONFIG, "record_limit");

    KafkaShareConsumer&lt;String, EventMessage&gt; consumer = new KafkaShareConsumer&lt;&gt;(properties);
</code></pre>
<p>Setting <code>SHARE_ACKNOWLEDGEMENT_MODE_CONFIG</code> to <code>explicit</code> disables automatic record commits, giving direct control to manually issue lock renewals <code>RENEW</code>, acceptances <code>ACCEPT</code>, or rejections <code>REJECT</code>.</p>
<p>Setting <code>SHARE_ACQUIRE_MODE_CONFIG</code> to <code>record_limit</code> ensures that <code>poll()</code> strictly respects <code>max.poll.records</code>.</p>
<pre><code class="language-java">consumer.subscribe(List.of(TOPIC));
            while (true) {
                ConsumerRecords&lt;String, EventMessage&gt; records;
                try {
                    records = consumer.poll(Duration.ofMillis(500));
                    System.out.println("-------------------------POLLED------------------------------");
                    for (ConsumerRecord&lt;String, EventMessage&gt; record : records) {
                        if(!underExecutionRecords.containsKey(record)){
                            //assign records to different threads for concurrent processing
                            Future&lt;Boolean&gt; futureProcessor = processExecutorService.submit(() -&gt; process(record, consumer));
                            System.out.println(Instant.now()+" Adding record "+record.value().getId()+" to underExecutionRecords");
                            underExecutionRecords.put(record, futureProcessor);
                        }
                    }
                    //collect acknowledgements from childrent threads
                    handleAcknowledgements(underExecutionRecords, consumer);
                } catch (RecordDeserializationException exception) {
                    rejectDeserializationFailure(consumer, exception);
                    continue;
                }
                consumer.commitSync();
                System.out.println(Instant.now()+" Committed for batch of records " + records.count());
            }
</code></pre>
<p>When <code>poll()</code> retrieves a new batch of <code>ConsumerRecords</code>, the main thread first checks if <code>ConsumerRecord</code> is present in <code>underExcecutionRecords</code> in-memory state from previous <code>poll()</code>. If a record is new, it is submitted to the <code>ExecutorService</code> for concurrent background processing and registered in <code>underExecutionRecords</code>.</p>
<p>Because the main thread isn't blocked by execution logic, it immediately transitions to <code>handleAcknowledgements()</code> to check worker thread statuses and assign appropriate acknowledgment types (<code>ACCEPT</code>, <code>REJECT</code>, or <code>RENEW</code>).It's important to note that <code>KafkaShareConsumer</code> <strong>is not thread-safe</strong>. Hence, worker threads cannot invoke <code>acknowledge()</code> directly upon completing a task. All acknowledgments must be routed back and managed centrally by the main polling thread / single thread.</p>
<p>Finally, if any record triggers a <code>DeserializationException</code> due to malformed payload data, it is safely marked with <code>AcknowledgeType.REJECT</code> before committing the batch.</p>
<pre><code class="language-java">private static boolean process(ConsumerRecord&lt;String,EventMessage&gt; record, KafkaShareConsumer&lt;String,EventMessage&gt; consumer) {
        Random random = new Random();
        try{
            if(random.nextBoolean()){ // Simulating long process time of 15sec for few records
                System.out.println(Instant.now()+" Long Processing started for record id "+record.value().getId()+" by thread "+Thread.currentThread().getName());
                Thread.sleep(15000);
                System.out.println(Instant.now()+" Long Processing completed for record id "+record.value().getId()+" by thread "+Thread.currentThread().getName());
            }else{ // Simulating short process time of 1sec for few records
                Thread.sleep(1000);
                System.out.println(Instant.now()+" Quickly Processed for record id "+record.value().getId()+" by thread "+Thread.currentThread().getName());
            }
            return true;
        }catch(InterruptedException ex){
            System.out.println(Instant.now()+" Error while processing record id "+ record.value().getId()+" by thread "+Thread.currentThread().getName());
            ex.printStackTrace();
            return false;
        }
    }
</code></pre>
<p>This method simulates realistic business logic execution times by introducing random processing delays. Long-running <code>ConsumerRecord</code> take approximately 15 seconds to finish, while faster records complete in under 1 second. Once execution wraps up, <code>process()</code> returns <code>true</code> on success, or <code>false</code> if an exception occurs during execution.</p>
<pre><code class="language-java">    private static void handleAcknowledgements(
            ConcurrentHashMap&lt;ConsumerRecord&lt;String,EventMessage&gt;,Future&lt;Boolean&gt;&gt; underExecutionRecords, KafkaShareConsumer&lt;String, EventMessage&gt; consumer) {
        
        underExecutionRecords.forEach((record, futureProcessor) -&gt; {
            try {
                    System.out.println(Instant.now()+" Calling future.get() for "+record.value().getId());
                    Boolean isDone = futureProcessor.get(2, TimeUnit.SECONDS);
                    
                    if(isDone){
                        consumer.acknowledge(record, AcknowledgeType.ACCEPT);
                        System.out.println(Instant.now()+" ACCEPT ACK for "+record.value().getId());
                    }else{
                        consumer.acknowledge(record, AcknowledgeType.REJECT);
                        System.out.println(Instant.now()+" REJECT ACK for "+record.value().getId());
                    }
                        
                System.out.println(Instant.now()+" Removing record "+record.value().getId()+" from underExecutionRecords");
                underExecutionRecords.remove(record); //either ACCEPT / REJECT in both cases record won't be polled again hence remove
           
            }catch(TimeoutException e){ 
                //If TimedOut after 2 seconds, it means record is still being processed hence renew the lock 
                consumer.acknowledge(record, AcknowledgeType.RENEW);
                System.out.println(Instant.now()+" Renewed lock for record id "+record.value().getId());
            
            }catch(Exception e){
                //Release record for reattempt as interrupted by unexpected exception
                consumer.acknowledge(record, AcknowledgeType.RELEASE);
                System.out.println(Instant.now()+" Release for record id "+record.value().getId());
                e.printStackTrace();
            }
        });
        
    }
</code></pre>
<p>Calling <code>future.get()</code> without a timeout blocks indefinitely until the worker thread returns a result. If a task exceeds <code>max.poll.interval.ms</code>, this blocking behavior recreates the exact main-thread starvation issue we set out to solve.</p>
<p>To keep the consumer responsive, always call <code>future.get(timeout, unit)</code> with a strict time cap. When <code>future.get()</code> throws a <code>TimeoutException</code>, it signals that the background thread is still actively processing the record—allowing you to catch the exception and acknowledge that record with <code>AcknowledgeType.RENEW</code>. For completed tasks, inspect the returned boolean value to acknowledge with <code>ACCEPT</code> (on <code>true</code>) or <code>REJECT</code> (on <code>false</code>). In both cases, remove record from <code>underExecutionRecords</code> as those need not be re-processed.</p>
<p>It is crucial that wait cap time in <code>future.get(capTime, TimeUnit)</code> is strictly less than <code>share.record.lock.duration.ms</code>. When iterating over a <code>underExecutionRecords</code> for the output, the total inspection time accumulates across all items. To prevent lock expiration and unwanted redeliveries to other share group instances, calculate the individual timeout cap using the following constraint:</p>
<p>(capTime x max.poll.records) &lt; <a href="http://share.record.lock.duration.ms">share.record.lock.duration.ms</a></p>
<p>Budgeting this time ensures that the main polling thread evaluates every in-flight task, issues all required <code>AcknowledgeType.RENEW</code> extensions, and flushes <code>commitSync()</code> well before the broker's default 30-second lock window expires.</p>
<hr />
<p>After <code>commitSync()</code> extends record locks for subset of <code>ConsumerRecord</code> from current <code>poll()</code> using <code>AcknowledgeType.RENEW</code>, subsequent <code>poll()</code> calls return those same in-flight records, until those are finalized. The main thread detects them in <code>underExecutionRecords</code>, skips re-submitting them to worker threads, and immediately calls <code>handleAcknowledgements()</code>. If processing completes within the <code>capTime</code> window, the record is finalized with <code>ACCEPT</code> or <code>REJECT</code>; otherwise, it remains in <code>underExecutionRecords</code> to repeat the renewal cycle on the next poll.</p>
<h3>Key Takeaways</h3>
<ol>
<li><p><strong>Single-Threaded Safety:</strong> <code>KafkaShareConsumer</code> is not thread-safe. Worker threads cannot call <code>acknowledge()</code> directly; the main thread must collect results and issue acknowledgments centrally.</p>
</li>
<li><p><strong>All-or-Nothing Commits:</strong> You must assign an <code>AcknowledgeType</code> to <strong>every</strong> record in a polled batch before invoking <code>commitSync()</code>. Partial commits are not allowed.</p>
</li>
<li><p><strong>Lock Extension Loop:</strong> Records acknowledged with <code>AcknowledgeType.RENEW</code> are re-delivered on subsequent <code>poll()</code> calls until explicitly finalized with <code>ACCEPT</code>, <code>REJECT</code>, or <code>RELEASE</code>.</p>
</li>
<li><p><strong>Broker Lock Timeouts:</strong> Failing to commit within <code>share.record.lock.duration.ms</code> triggers broker-side redelivery up to <code>group.share.delivery.count.limit</code> times (default: 5) before the message is treated as rejected.</p>
</li>
</ol>
<h3>Last Words !</h3>
<p>By combining <code>KafkaShareConsumer</code> with asynchronous worker threads and periodic <code>AcknowledgeType.RENEW</code> lock extensions, you overcome the traditional <code>max.poll.interval.ms</code> limit. This design allows you to handle long-running, unpredictable workloads reliably without sacrificing consumer group stability or risking duplicate execution.</p>
]]></content:encoded></item><item><title><![CDATA[Beyond the Partition Limit: How KIP-932 Enables Elastic Consumer Scaling]]></title><description><![CDATA[What is KIP-932?
Apache Kafka is undergoing one of its most significant architectural shifts yet with KIP-932. By introducing a new abstraction called Share Groups, KIP-932 bridges the long-standing g]]></description><link>https://offsetzero.hashnode.dev/beyond-the-partition-limit-how-kip-932-enables-elastic-consumer-scaling</link><guid isPermaLink="true">https://offsetzero.hashnode.dev/beyond-the-partition-limit-how-kip-932-enables-elastic-consumer-scaling</guid><category><![CDATA[kafka]]></category><category><![CDATA[Kafkashareconsumer]]></category><category><![CDATA[kip-932]]></category><dc:creator><![CDATA[shubham shirur]]></dc:creator><pubDate>Thu, 24 Sep 2026 19:50:55 GMT</pubDate><content:encoded><![CDATA[<h3>What is <a href="https://cwiki.apache.org/confluence/spaces/KAFKA/pages/255070434/KIP-932+Queues+for+Kafka">KIP-932</a>?</h3>
<p>Apache Kafka is undergoing one of its most significant architectural shifts yet with <a href="https://cwiki.apache.org/confluence/spaces/KAFKA/pages/255070434/KIP-932+Queues+for+Kafka"><strong>KIP-932</strong></a>. By introducing a new abstraction called <strong>Share Groups</strong>, <a href="https://cwiki.apache.org/confluence/spaces/KAFKA/pages/255070434/KIP-932+Queues+for+Kafka">KIP-932</a> bridges the long-standing gap between log-based event streaming and message queuing. Not exactly the Queue, but it implements the concept of cooperative consumption to accommodate queuing mechanism using KafkaTopics. Kafka can now manage record-level acknowledgments and dynamic message delivery. Here is a breakdown of how <a href="https://cwiki.apache.org/confluence/spaces/KAFKA/pages/255070434/KIP-932+Queues+for+Kafka">KIP-932</a> works, why it matters, and how it transforms Kafka’s capabilities.</p>
<h3>Why Does <a href="https://cwiki.apache.org/confluence/spaces/KAFKA/pages/255070434/KIP-932+Queues+for+Kafka">KIP-932</a> Matter?</h3>
<p>Operations teams frequently run into severe operational friction when using traditional Kafka consumer groups:</p>
<ol>
<li><p><strong>Session Timeouts &amp; Rebalance Storms:</strong> When a few records require mroe computation time, processing can exceed <code>max.poll.interval.ms</code>. The group coordinator assumes the consumer is dead, triggers a rebalance, and cascades delays across the entire consumer group - all because a couple of messages took longer than expected.</p>
</li>
<li><p><strong>Scaling Bottlenecks &amp; Static Tuning:</strong> For workloads with unpredictable payload spikes, teams are forced to continuously tune <code>max.poll.records</code>. Because traditional consumer groups enforce a strict 1-to-1 limit between a partition and a consumer instance, you cannot scale consumer instances to process messages from a single partition concurrently.</p>
</li>
<li><p><strong>Poison Pills &amp; Head-of-Line Blocking:</strong> In environments without strict schema governance, a single malformed or un-processable message will halt an entire partition. Because offset commits move sequentially, subsequent valid records are blocked until someone manually resets the offset in the morning after upstream team reported a problem.</p>
</li>
</ol>
<p>These are daily headaches for Kafka developers and platform engineers. <strong>KIP-932 directly solves them</strong> by introducing native point-to-point queuing semantics with record-level acknowledgments and cooperative message processing.</p>
<h3>KIP-932 into Practice</h3>
<p>Instantiating a <code>KafkaShareConsumer</code> is similar to configuring a traditional Kafka consumer, but it introduces specific properties to control message delivery and acquisition:</p>
<pre><code class="language-java">    Properties properties = new Properties();
    properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    properties.put(ConsumerConfig.GROUP_ID_CONFIG, "offsetzero-share-consumer-group");
    properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
    properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
    properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "5");
    properties.put(ConsumerConfig.SHARE_ACKNOWLEDGEMENT_MODE_CONFIG, "explicit");
    properties.put(ConsumerConfig.SHARE_ACQUIRE_MODE_CONFIG, "record_limit");

KafkaShareConsumer&lt;String, EventMessage&gt; consumer = new KafkaShareConsumer&lt;&gt;(properties);
</code></pre>
<p>Key configurations to note:</p>
<ol>
<li><p><a href="https://kafka.apache.org/43/configuration/consumer-configs/#consumerconfigs_share.acknowledgement.mode"><code>SHARE_ACKNOWLEDGEMENT_MODE_CONFIG</code></a>: Defines how record processing status is committed to the broker.</p>
<ol>
<li><p><code>explicit</code>(Recommended): Gives you fine-grained control by requiring manual acknowledgments (<code>ACCEPT</code>, <code>RELEASE</code>, <code>REJECT</code>, <code>RENEW</code>) per record.</p>
</li>
<li><p><code>implicit</code>: Automatically acknowledges fetched batches on subsequent calls to <code>poll()</code>.</p>
</li>
</ol>
</li>
<li><p><a href="https://kafka.apache.org/43/configuration/consumer-configs/#consumerconfigs_share.acquire.mode"><code>SHARE_ACQUIRE_MODE_CONFIG</code></a>: Dictates how the broker locks and delivers messages to consumer instances.</p>
<ol>
<li><p><code>record_limit</code>: Acquires records up to the batch limit set by <code>MAX_POLL_RECORDS_CONFIG</code>, ensuring controlled memory footprint and predictable lock acquisition across instances.</p>
</li>
<li><p><code>batch_optimized</code>: Optimizes for raw throughput by fetching full message batches from the broker, regardless of the limit set in <code>MAX_POLL_RECORDS_CONFIG</code>.</p>
</li>
</ol>
</li>
</ol>
<pre><code class="language-java">try {
            consumer.subscribe(List.of(TOPIC));
            while (true) {
                ConsumerRecords&lt;String, EventMessage&gt; records;
                try {
                    records = consumer.poll(Duration.ofMillis(500));
                } catch (RecordDeserializationException exception) {
                    //Handle poison pills / improper records separately
                    rejectDeserializationFailure(consumer, exception);
                    continue;
                }

                for (ConsumerRecord&lt;String, EventMessage&gt; record : records) {
                    Thread.sleep(3000);
                    //Process records
                    restBackendProcess(record, consumer);
                }
                //Report all the individual acknowledgements (ACCEPT, REJECT, RELEASE, RENEW) for this batch of messages to broker
                consumer.commitSync();
                System.out.println("Committed for batch of records " + records.count());
            }
        } catch (WakeupException exception) {
            consumer.commitSync();
            System.out.println("Wakeup exception caught, consumer shutdown initiated");
        } catch (Exception exception) {
            System.err.println("Error closing consumer: " + exception.getMessage());
        } finally {
            consumer.close();
            System.out.println("Consumer closed");
        }
</code></pre>
<p>The consumer is subscribed to the target topic, configured to deserialize record keys as <code>String</code> and values as <a href="https://github.com/shubhamsworking/OffsetZero/blob/main/ShareConsumerAcknowledgeTypeExample/src/main/java/com/offsetzero/kafka/EventMessage.java"><code>EventMessage</code></a>. Notice how deserialization errors (poison pills) are intercepted and rejected immediately, preventing them from blocking subsequent records.</p>
<pre><code class="language-java">private static void rejectDeserializationFailure(KafkaShareConsumer&lt;String, EventMessage&gt; consumer,
                                                     RecordDeserializationException exception) {
        String topic = exception.topicPartition().topic();
        int partition = exception.topicPartition().partition();
        long offset = exception.offset();
        System.err.println("Rejecting deserialization failure record id =" + topic
                + ", partition=" + partition + ", offset=" + offset
                + ": " + exception.getMessage());
        consumer.acknowledge(topic, partition, offset, AcknowledgeType.REJECT);
    }
</code></pre>
<p>When a poison pill record fails deserialization, <code>consumer.poll()</code> throws a <code>RecordDeserializationException</code>. Because a <code>ConsumerRecord</code> object is never constructed, you cannot pass a <code>record</code> instance to <code>consumer.acknowledge()</code>.</p>
<p>KIP-932 solves this by providing an overloaded <code>acknowledge()</code> method accepting the specific <code>topic</code><strong>,</strong> <code>partition</code><strong>,</strong> <code>offset</code><strong>,</strong> and <code>AcknowledgeType</code>.</p>
<pre><code class="language-java">private static void restBackendProcess(ConsumerRecord&lt;String, EventMessage&gt; record,
                                            KafkaShareConsumer&lt;String, EventMessage&gt; consumer) {
        //Simulate REST API throttling messages, hence release to re-attempt processing
        if (ThreadLocalRandom.current().nextBoolean()) {
            System.out.println("REST backend throttled record id = " + record.value().getId() + "; releasing it for redelivery");
            consumer.acknowledge(record, AcknowledgeType.RELEASE);
            return;
        }

        //Successful processing
        System.out.println("REST backend processed record id = " +record.value().getId());
        consumer.acknowledge(record, AcknowledgeType.ACCEPT);
    }
</code></pre>
<p>The <code>restBackendProcess</code> method demonstrates dynamic message acknowledgment by simulating a REST API call that occasionally encounters rate limits. When a downstream service returns a throttling error, the consumer calls <code>consumer.acknowledge(record, AcknowledgeType.RELEASE)</code>—releasing the message back to the broker so it can be re-attempted later or processed by another available worker. Conversely, upon successful execution, it issues an <code>AcknowledgeType.ACCEPT</code> to finalize record processing without blocking the rest of the partition.</p>
<h3>Try it yourself:</h3>
<p>To experiment with <code>KafkaShareConsumer</code>, clone the demo repository at <a href="https://github.com/shubhamsworking/OffsetZero"><strong>shubhamsworking/OffsetZero</strong></a>, start a local Kafka cluster, and test these scenarios:</p>
<ol>
<li><p><code>SimpleShareKafkaConsumer</code><strong>:</strong> Run multiple instances and see how it demonstrates multi-consumer concurrency on a <strong>single-partition</strong> topic using <a href="https://github.com/shubhamsworking/OffsetZero/blob/main/scripts/produce-events.sh"><code>produce-events.sh</code></a>.</p>
</li>
<li><p><code>ShareConsumerAcknowledgeTypeExample</code><strong>:</strong> Demonstrates <strong>record-level acknowledgments</strong> on a multi-partition topic using <a href="https://github.com/shubhamsworking/OffsetZero/blob/main/scripts/produce-json-events.sh"><code>produce-json-events.sh</code></a>. Highlights how KIP-932 handles poison pills, record releases/retries, and backend throttling.</p>
</li>
</ol>
<h3>Good to know</h3>
<p>Unlike traditional Kafka consumer groups where offset and partition management are heavily driven by the client, <strong>Share Groups rely on the Group Coordinator on the broker as the central point of management</strong>. Because of this server-centric architecture, client-side overrides don't apply to key queue behaviors.</p>
<ol>
<li><p>Key server-side and group configurations to keep in mind:</p>
<ol>
<li><p><strong>Starting offset to read (</strong><code>share.auto.offset.reset</code><strong>):</strong> Setting this in client-side code will not work. Group offsets must be managed centrally on the broker using the <code>kafka-configs.sh</code> CLI tool.</p>
<pre><code class="language-shell">kafka-configs.sh \
    --bootstrap-server localhost:9092 \
    --entity-type groups \
    --entity-name offsetzero-share-consumer-group \
    --add-config share.auto.offset.reset=earliest \
    --alter
</code></pre>
</li>
<li><p><strong>Record Lock Duration (</strong><code>share.record.lock.duration.ms</code><strong>):</strong> Defines how long a broker locks a record to a consumer instance after it is fetched (defaults to 30s). This lock mechanism allows multiple concurrent consumers—or multiple threads within a single consumer—to process records in parallel without duplicate delivery.</p>
</li>
<li><p><strong>Delivery Count Limit (</strong><code>share.delivery.record.limit</code><strong>):</strong> Controls redelivery tolerance (defaults to 5). If a record is continuously released or times out without an explicit <code>ACCEPT</code>, the record is rejected once this retry limit is reached, preventing infinite retries.</p>
</li>
<li><p><strong>Lock Extension (</strong><code>share.renew.acknowledge.enable</code><strong>):</strong> If a worker needs more time for a long-running task, issuing a <code>RENEW</code> acknowledgment extends the record lock. Brokers can enable or enforce this behavior using the <code>share.renew.acknowledge.enable</code> config.</p>
</li>
</ol>
</li>
<li><p>Management operations for Share Groups - such as describing group status or resetting offsets are performed using the dedicated <code>kafka-share-groups.sh</code> CLI tool rather than the traditional <code>kafka-consumer-groups.sh</code>.</p>
</li>
</ol>
<h3>Final Thoughts</h3>
<p>With good <a href="https://kafka.apache.org/42/operations/monitoring/#share-group-monitoring">JMX monitoring metrices</a> for share groups, Apache Kafka’s Share Groups redefine how we think about work-queue patterns in event-driven systems. By combining record-level acknowledgments, automated handling for poison pills via <code>ARCHIVED</code> states, and centralized broker configuration, KIP-932 gives platform engineers the best of both worlds: high-throughput stream processing and queue mechanism.</p>
]]></content:encoded></item></channel></rss>