This page is meant as a template for writing a KIP. To create a KIP choose Tools->Copy on this page and modify with your content and replace the heading with the next KIP number and a description of your issue. Replace anything in italics with your own description.

Status

Current state: Under Discussion

Discussion thread: here 

JIRA: [KAFKA-19883]()

Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).

1 Motivation

A record's side effects (output writes) and its source acknowledgment must be committed atomically. Either both succeed, or neither does.

  1. Share consumer polls records -> broker sets them to ACQUIRED.
  2. Framework processes and acknowledges records (implicit or explicit).
  3. Checkpoint fails before sink outputs are committed.
  4. Records are permanently ACKNOWLEDGED (not redelivered) -> data loss.
  5. Note:
    1. Share records become terminal when acked: RecordState
    2. Ack path today is irreversible once ACKNOWLEDGED: SharePartition.acknowledge()

Goal: Enable exactly-once read semantics via transactional acknowledgements.

We will be following the similar existing pattern we have in Kafka Producer Transactions.

Use Cases

Frameworks:

- Apache Flink: Exactly-once checkpointing with Share Group sources
Apache Spark: Structured Streaming with Share Group consumers
Any coordinator-worker streaming framework requiring atomic acknowledgements

1.1 Background: Share Groups

Share groups (KIP-932) allow multiple consumers to read from the same partition concurrently, with the broker controlling per-record delivery via an acquisition-lock mechanism. Each record passes through a state machine on the broker:

AVAILABLE → ACQUIRED → ACKNOWLEDGED (terminal)
                    → ARCHIVED (terminal, rejected or max delivery exceeded)
                    → AVAILABLE (released for redelivery)

Today, share groups support two acknowledgment modes:

In both modes, acknowledgments are committed immediately and irrevocably. Once a record enters ACKNOWLEDGED state in the SharePartition (managed on the broker), it is never redelivered.

Relevant existing code:

1.2 Why Existing Consumer-Group Exactly-Once Doesn't Apply

With traditional consumer groups, Flink avoids this problem by replaying from a saved offset:

  1. KafkaSourceReader.snapshotState() saves offsets in Flink's state.
  2. On failure recovery, consumer.seek(savedOffset) replays from the checkpoint.
  3. Kafka offset commits (via sendOffsetsToTransaction()) are cosmetic — Flink state is the source of truth.

Share groups have no seek(). The broker controls which records are delivered. Once acknowledged, records are gone. Therefore, acknowledgment itself must become the transactional boundary, not just a cosmetic side-effect.

1.3 Existing Pattern: sendOffsetsToTransaction()

Kafka already solves the identical problem for consumer-group offsets via KafkaProducer.sendOffsetsToTransaction():

  1. The producer includes consumer-group offsets in its ongoing transaction.
  2. When the transaction commits, both output records and offset commits become visible atomically.
  3. On abort, neither is visible — the consumer re-reads from the old offset.

This KIP applies the same pattern to share-group acknowledgments. Instead of committing to __consumer_offsets, we commit to __share_group_state.

Existing code this KIP mirrors:

2. Use Cases

2.1 Consume-Transform-Produce (CTP)

An application reads from a share group, transforms records, and produces output to another Kafka topic. Both output and acknowledgments must commit atomically.

producer.beginTransaction();

ConsumerRecords<K,V> records = shareConsumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<K,V> record : records) {
    ProducerRecord<K,V> output = transform(record);
    producer.send(output);
}

// Bind share acks to this transaction (NEW API)
producer.sendShareAcksToTransaction(
    ShareAcknowledgements.fromRecords(records, AcknowledgeType.ACCEPT),
    shareConsumer.groupMetadata()
);

producer.commitTransaction();
// Output records AND share acks commit atomically

2.2 Flink / Spark Source (No Producer)

A streaming framework reads from a share group as a source. There is no Kafka producer in the pipeline — the output may go to a database, filesystem, or another system. The framework needs to commit share acks transactionally, coordinated with its own checkpointing.

TransactionalShareAcknowledger acknowledger = new TransactionalShareAcknowledger(props);
acknowledger.initTransactions();

// On checkpoint complete:
acknowledger.commitAcknowledgements(bufferedAcks, shareGroupId);
// Internally: beginTransaction → sendShareAcksToTransaction → commitTransaction

2.3 Flink End-to-End Exactly-Once (Source + Sink)

When a Flink pipeline reads from a Kafka share group and writes to a Kafka sink topic, we achieve end-to-end exactly-once by binding both sink output and source acknowledgments to the same Kafka transaction:

Checkpoint lifecycle:
  prepareCommit()              → flush sink records, pre-commit Kafka txn
                               → include share acks in the same transaction
  snapshotState()              → save txn metadata + buffered acks
  notifyCheckpointComplete()   → commitTransaction() (acks + output atomically)
  On failure                   → abortTransaction() (acks + output both rolled back)

3 Public Interfaces


New APIMirrorsWhy Needed
sendShareAcksToTransaction()sendOffsetsToTransaction()Acks are stored in __share_group_state, not __consumer_offsets
AddShareAcksToTxnRequestAddOffsetsToTxnRequestTransaction coordinator must track __share_group_state partitions
TxnShareAcknowledgeRequestTxnOffsetCommitRequestAck semantics are per‑record state, not per‑offset


3.1 KafkaProducer API Addition


// In org.apache.kafka.clients.producer.KafkaProducer:

/**
 * Sends share-group acknowledgments as part of the current transaction.
 * Mirrors sendOffsetsToTransaction() but writes to __share_group_state
 * instead of __consumer_offsets.
 *
 * @param acknowledgements Map of TopicPartition to list of acknowledgment batches
 * @param groupMetadata    The share group metadata (group ID, member ID, generation)
 * @throws IllegalStateException if no transaction is in progress
 * @throws ProducerFencedException if the producer is fenced
 */
public void sendShareAcksToTransaction(
    Map<TopicPartition, ShareAcknowledgements> acknowledgements,
    ShareGroupMetadata groupMetadata
) throws ProducerFencedException;



This mirrors sendOffsetsToTransaction(). The reason a new method is needed (instead of reusing the existing one) is that:

3.2 TransactionalShareAcknowledger (Standalone)

For frameworks that do not use a KafkaProducer in the pipeline:

public class TransactionalShareAcknowledger implements Closeable {

    public TransactionalShareAcknowledger(Properties config);

    /** Initialize the internal transactional producer. Call once. */
    public void initTransactions();

    /**
     * Atomically commit share acknowledgments.
     * Internally executes: beginTransaction → sendShareAcksToTransaction → commitTransaction.
     * This is NOT a single RPC. It orchestrates the standard 2PC protocol.
     */
    public void commitAcknowledgements(
        Map<TopicPartition, ShareAcknowledgements> acks,
        String groupId
    );

    /** Abort any in-progress transactional acknowledgment. */
    public void abortAcknowledgements();

    public void close();

 }


Clarification: commitAcknowledgements() is a convenience wrapper. It internally calls three operations in sequence:

  1. beginTransaction()
  2. sendShareAcksToTransaction(acks, groupMetadata)
  3. commitTransaction()

It does NOT introduce a new single-RPC path. It uses the standard 2PC protocol.

3.3 ShareGroupMetadata

public class ShareGroupMetadata {
    private final String groupId;
    private final String memberId;
    private final int generationId;
    // constructor, getters

 }


3.4 ShareAcknowledgements

public class ShareAcknowledgements {
    private final List<ShareAcknowledgementBatch> batches;

    public static ShareAcknowledgements fromRecords(
        ConsumerRecords<?, ?> records, AcknowledgeType type);

    // Each batch: firstOffset, lastOffset, acknowledgeType

 }


3.5 New Metrics

Metric NameTypeDescription
share-transaction-activeGaugeNumber of active share-group transactions
share-transaction-prepare-time-msHistogramTime to prepare share ack transaction
share-transaction-commit-time-msHistogramTime to commit share ack transaction
share-transaction-abort-totalCounterTotal aborted share ack transactions
share-transaction-timeout-totalCounterTotal timed-out share ack transactions

Proposed Changes

1. Two-Phase Commit Protocol

Transaction States Transitions

existing Kafka 2PC (TransactionState: ONGOING→PREPARE_COMMIT→COMPLETE_COMMIT)

- EMPTY -> ONGOING -> PREPARE -> ABORTED (abort after prepare)

- EMPTY -> ONGOING -> ABORTED (early abort)

Sequence Diagram





Abort case: 




This KIP reuses Kafka's existing two-phase commit protocol:

Transaction metadata stays in __transaction_state.

Share acks are written transactionally to __share_group_state using CoordinatorRuntime.scheduleTransactionalWriteOperation().

Failure Handling

existing TransactionCoordinator recovery

2. Wire Protocol Details

TxnShareAcknowledgeRequest

```
ShareAcknowledgeTransactionalRequest => GroupId TransactionId [Topics]
  GroupId => STRING
  TransactionId => STRING
  Topics => TopicId [Partitions]
    Partitions => Partition [AcknowledgementBatches]
      AcknowledgementBatches => FirstOffset LastOffset AcknowledgeType
```

ShareBeginTransactionRequest

Use existing InitProducerIdRequest

SharePrepareTransactionRequest

 Use existing EndTxnRequest

ShareCommitTransactionRequest / ShareAbortTransactionRequest

Handled by existing WriteTxnMarkersRequest (inter-broker) and  EndTxnRequest(ABORT)

AddShareAcksToTxnRequest

```

AddShareAcksToTxnRequest => TransactionalId ProducerId ProducerEpoch GroupId [Topics]

  TransactionalId => STRING

  ProducerId => INT64

  ProducerEpoch => INT16

  GroupId => STRING

  Topics => TopicName [Partitions]

    Partitions => INT32

```

Consume-Transform-Produce Pattern

This is the exactly-once guarantee: either both output AND acks commit, or neither does: 

```

KafkaShareConsumer<K, V> consumer = new KafkaShareConsumer<>(props);  // NOT transactional
KafkaProducer<K, V> producer = new KafkaProducer<>(props);            // transactional.id

producer.initTransactions();

while (true) {
    ConsumerRecords<K, V> records = consumer.poll(...);  // ← just reads, not transactional
    
    producer.beginTransaction();
    for (var record : records) {
        producer.send(new ProducerRecord<>("output", transform(record)));
    }
    producer.sendShareAcksToTransaction(buildAcks(records), "my-share-group");  //  transactional write
    producer.commitTransaction();
    // Atomically: output records committed + share acks committed
    // If abort: output rolled back + acks discarded → records return to AVAILABLE
}

```

In the existing consumer group CTP pattern, the same thing happens (in the above code instead of KafkaShareConsumer, use KafkaConsumer and Consumer offsets to __consumer_offsets).


If abortTransaction() is called (or the transaction times out):

no-producer (Flink/Spark) use case 

```

// Flink ShareGroupSource connector — on checkpoint complete

class FlinkShareGroupSourceReader implements SourceReader<...> {

    private TransactionalShareAcknowledger acknowledger;

    private List<ShareAcknowledgement> pendingAcks;

    void open() {

        acknowledger = new TransactionalShareAcknowledger(config);

        acknowledger.initTransactions();

    }

    void snapshotState(long checkpointId) {

        // Save pending acks to checkpoint state

    }

    void notifyCheckpointComplete(long checkpointId) {

        // Atomically commit acks for this checkpoint

        acknowledger.commitAcknowledgements(pendingAcks, shareGroupId);

    }

}

```

Compatibility, Deprecation, and Migration Plan

Impact on Existing Users

Migration Path

1. Phase 1: Add `transactional` acknowledgement mode (backward compatible)
2. Phase 2: Streaming frameworks (Flink, Spark) implement transactional sources
3. Phase 3: Documentation and best practices for exactly-once semantics

Deprecation

- No deprecation of existing modes planned
- `transactional` mode recommended for exactly-once use cases

Test Plan

Unit Tests

- Transaction state machine transitions
- Idempotency of all transaction operations
- Timeout handling and auto-abort
- Record state transitions (ACQUIRED → ACKNOWLEDGED on commit, ACQUIRED → AVAILABLE on abort/timeout)

Integration Tests

- End-to-end transaction commit/abort flows
- Multi-consumer transactions within same group
- Coordinator failure and recovery
- Network partition scenarios

System Tests

- Flink checkpoint integration with transactional Share Groups
- Exactly-once delivery verification under failures
- Performance benchmarks comparing transactional vs non-transactional modes
- Stress testing with concurrent transactions

Chaos Tests

- Broker failure during PREPARE state
- Coordinator crash before/after prepare
- Consumer crash during transaction
- Network partitions between coordinator and brokers

Include exactly‑once recovery tests similar to Flink e2e:

Rejected Alternatives

If there are alternative ways of accomplishing the same thing, what were they? The purpose of this section is to motivate why the design is the way it is and not some other way.