Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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


Code Block
// 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:

  • Different storage topic: acks go to __share_group_state, not __consumer_offsets.
  • Different coordinator: the ShareCoordinator handles ack persistence, not GroupCoordinator.
  • Different semantics: acks are per-record state transitions, not per-partition offsets.

3.2 TransactionalShareAcknowledger (Standalone)

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

Code Block
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

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

 }


3.4 ShareAcknowledgements

Code Block
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

4 Proposed Changes

4.1

...

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

Image Removed

Abort case: 

Image Removed

...

Reuse of Existing 2PC Protocol

This KIP reuses Kafka's existing two-phase commit protocol. No new coordinator type or consensus protocol is introduced.

Transaction lifecycle (identical to existing):

EMPTY → ONGOING → PREPARE_COMMIT → COMPLETE_COMMIT
                → PREPARE_ABORT  → COMPLETE_ABORT

What changes is which partitions are added to the transaction:

API CallPartition Added to TransactionStorage Topic
producer.send(record)Data topic partitionUser topic
sendOffsetsToTransaction()__consumer_offsets partition for group__consumer_offsets
sendShareAcksToTransaction() (NEW)__share_group_state partition for group+topic__share_group_state

The WriteTxnMarkers request dispatches commit/abort markers to all partitions in the transaction set. If only sendShareAcksToTransaction() was called, markers go only to __share_group_state. If both sendOffsetsToTransaction() and sendShareAcksToTransaction() were called in the same transaction, markers go to both. The transaction coordinator does not distinguish between these — it just tracks partition sets.

4.2 Wire Protocol Changes

New request: AddShareAcksToTxnRequest (mirrors AddOffsetsToTxnRequest)

Code Block
{
  "apiKey": TBD,
  "type": "request",
  "name": "AddShareAcksToTxnRequest",
  "validVersions": "0",
  "fields": [
    { "name": "TransactionalId", "type": "string", "versions": "0+" },
    { "name": "ProducerId", "type": "int64", "versions": "0+" },
    { "name": "ProducerEpoch", "type": "int16", "versions": "0+" },
    { "name": "GroupId", "type": "string", "versions": "0+" },
    { "name": "Topics", "type": "[]AddShareAcksToTxnTopic", "versions": "0+",
      "fields": [
        { "name": "Name", "type": "string", "versions": "0+" },
        { "name": "Partitions", "type": "[]int32", "versions": "0+" }
      ]
    }
  ]
}

Purpose: Tells the TransactionCoordinator to add the __share_group_state partition(s) for the given {groupId, topicPartition} pair(s) to the producer's ongoing transaction. The partition is determined by ShareCoordinator.partitionFor(groupId, topicPartition), where topicPartition refers to the original data topic partition being acknowledged, and the function maps it to the corresponding __share_group_state internal partition that stores the state for that share-group + data-partition combination.

New request: TxnShareAcknowledgeRequest (mirrors TxnOffsetCommitRequest)

Code Block
{
  "apiKey": TBD,
  "type": "request",
  "name": "TxnShareAcknowledgeRequest",
  "validVersions": "0",
  "fields": [
    { "name": "GroupId", "type": "string", "versions": "0+" },
    { "name": "TransactionalId", "type": "string", "versions": "0+" },
    { "name": "ProducerId", "type": "int64", "versions": "0+" },
    { "name": "ProducerEpoch", "type": "int16", "versions": "0+" },
    { "name": "Topics", "type": "[]TxnShareAcknowledgeTopic", "versions": "0+",
      "fields": [
        { "name": "TopicId", "type": "uuid", "versions": "0+" },
        { "name": "Partitions", "type": "[]TxnShareAcknowledgePartition", "versions": "0+",
          "fields": [
            { "name": "Partition", "type": "int32", "versions": "0+" },
            { "name": "AcknowledgementBatches", "type": "[]AcknowledgementBatch", "versions": "0+",
              "fields": [
                { "name": "FirstOffset", "type": "int64", "versions": "0+" },
                { "name": "LastOffset", "type": "int64", "versions": "0+" },
                { "name": "AcknowledgeType", "type": "int8", "versions": "0+" }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Purpose: Sent to the ShareCoordinator to write acknowledgments as pending (uncommitted) within the transaction. The acks become visible only when the transaction commits.

4.3 ShareCoordinator Changes

The ShareCoordinator must handle transaction completion, mirroring GroupCoordinator.completeTransaction():


Code Block
// In ShareCoordinatorShard (NEW method):
public CoordinatorResult<Void, CoordinatorRecord> completeTransaction(
    long producerId,
    short producerEpoch,
    TransactionResult result    // COMMIT or ABORT
) {
    if (result == TransactionResult.COMMIT) {
        // Materialize pending transactional acks into share-group state
        return applyPendingAcknowledgements(producerId, producerEpoch);
    } else {
        // Discard pending transactional acks
        return discardPendingAcknowledgements(producerId, producerEpoch);
    }
}



The existing hook ShareCoordinatorShard.replayEndTransactionMarker() already exists for replaying transaction markers during log recovery. This KIP extends it to also handle live transaction completion.

4.4 State Storage

Transactional acks are written to __share_group_state using CoordinatorRuntime.scheduleTransactionalWriteOperation(). This follows the same pattern used by GroupCoordinator for transactional offset commits to __consumer_offsets.

Records in __share_group_state are written with the producer's producerId and producerEpoch, making them part of the transaction. They become readable by other consumers only after the transaction commits and the WriteTxnMarkers COMMIT marker is written.



Image Added


Abort case: 


Image Added

  • EndTxn(COMMIT) transitions the transaction to PREPARE_COMMIT in __transaction_state
  • WriteTxnMarkers dispatches commit markers to all partitions (data topics, __consumer_offsets, and now __share_group_state)
  • When all markers are confirmed, state transitions to COMPLETE_COMMIT

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

  • mirrors AddOffsetsToTxnRequest; This tells the TransactionCoordinator to add __share_group_state partitions to the producer's transaction.
  • AddShareAcksToTxn computes TopicPartition(SHARE_GROUP_STATE_TOPIC_NAME, shareCoordinator.partitionFor(shareGroupId, topicPartition)) and  adds it to the producer's TransactionMetadata.topicPartitions set via TransactionCoordinator.handleAddPartitionsToTransaction().
  • This is what causes WriteTxnMarkers to dispatch markers to __share_group_state partitions during commit.

```

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):

  • Output records get ABORT markers -> consumers with read_committed skip them
  • Share acks are discarded -> records stay ACQUIRED -> acquisition lock timeout -> records return to AVAILABLE -> re-delivered to another consumer

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

...