DUE TO SPAM, SIGN-UP IS DISABLED. Goto Selfserve wiki signup and request an account.
...
| New API | Mirrors | Why Needed |
|---|---|---|
sendShareAcksToTransaction() | sendOffsetsToTransaction() | Acks are stored in __share_group_state, not __consumer_offsets |
AddShareAcksToTxnRequest | AddOffsetsToTxnRequest | Transaction coordinator must track __share_group_state partitions |
TxnShareAcknowledgeRequest | TxnOffsetCommitRequest | Ack 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
ShareCoordinatorhandles ack persistence, notGroupCoordinator. - 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:
beginTransaction()sendShareAcksToTransaction(acks, groupMetadata)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 Name | Type | Description |
|---|---|---|
share-transaction-active | Gauge | Number of active share-group transactions |
share-transaction-prepare-time-ms | Histogram | Time to prepare share ack transaction |
share-transaction-commit-time-ms | Histogram | Time to commit share ack transaction |
share-transaction-abort-total | Counter | Total aborted share ack transactions |
share-transaction-timeout-total | Counter | Total 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
Abort case:
...
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 Call | Partition Added to Transaction | Storage Topic |
|---|---|---|
producer.send(record) | Data topic partition | User 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.
Abort case:
- 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
...

