DUE TO SPAM, SIGN-UP IS DISABLED. Goto Selfserve wiki signup and request an account.
...
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.
- Share consumer polls records -> broker sets them to
ACQUIRED. - Framework processes and acknowledges records (implicit or explicit).
- Checkpoint fails before sink outputs are committed.
- Records are permanently
ACKNOWLEDGED(not redelivered) -> data loss. - Note:
- Share records become terminal when acked:
RecordState - Ack path today is irreversible once ACKNOWLEDGED:
SharePartition.acknowledge()
- Share records become terminal when acked:
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
- Consume‑Transform‑Produce (CTP): bind acks to Kafka output transaction.
- Consume‑only frameworks: transactional acks independent of producer.
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
Although in this KIP we are refering Flink Stream processing engine few places but it is valid for any stream processing engine - almost all have same pattern of offset commits or replay when something crashes.
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:
- Implicit: Records from a previous
poll()are automatically acknowledged on the nextpoll(). - Explicit: The application calls
acknowledge(record, AcknowledgeType)for each record, thencommitSync().
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:
RecordStateenum:kafka/server/src/main/java/org/apache/kafka/server/share/fetch/RecordState.javaSharePartition.acknowledge():kafka/core/src/main/java/kafka/server/share/SharePartition.javaShareConsumerinterface:kafka/clients/src/main/java/org/apache/kafka/clients/consumer/ShareConsumer.javaAcknowledgeTypeenum (ACCEPT/RELEASE/REJECT/RENEW):kafka/clients/src/main/java/org/apache/kafka/clients/consumer/AcknowledgeType.java
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:
KafkaSourceReader.snapshotState()saves offsets in Flink's state.- On failure recovery,
consumer.seek(savedOffset)replays from the checkpoint. - 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():
- The producer includes consumer-group offsets in its ongoing transaction.
- When the transaction commits, both output records and offset commits become visible atomically.
- 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:
...
In the current KIP-932 implementation, Share Group acknowledgments are immediate and irrevocable.
If a processing framework (Flink, Spark, etc.) crashes after acknowledging a record but before committing its own state, that record is lost.
Current State:
ACKNOWLEDGEDis a terminal state; there is no way to revert toAVAILABLEfor redelivery.The Risk: Permanent data loss during processing failures.
The Goal: Atomic "Read-Process-Write"
Enable Exactly-Once Semantics (EOS) by making acknowledgments part of a Kafka transaction. Either the output is produced AND the source record is acknowledged, or neither happens.
1.1 Background: Share Groups
Current State Machine:
AVAILABLE: Ready for delivery.
ACQUIRED: Locked by a consumer.
ACKNOWLEDGED / ARCHIVED: Terminal states; records are never redelivered.
AVAILABLE (Released): Returned to the pool for redelivery.
Current Acknowledgment Modes:
Implicit: Automatic ack on the subsequent
poll().Explicit: Manual ack via
acknowledge(record, type)followed bycommitSync()
1.2 Why Existing Consumer-Group Exactly-Once Doesn't Apply
Traditional Consumer Groups rely on replayability, which Share Groups lack:
Consumer Groups: Frameworks like Flink save offsets in their own state. On failure, they use
seek(offset)to replay data. In this model, Kafka offset commits are "cosmetic" (non-critical) because Flink is the source of truth.Share Groups: There is no
seek()functionality. The broker manages delivery; once a record is acknowledged, it is removed from the delivery pipeline.The Conflict: Because records cannot be replayed, the acknowledgment itself must be the transactional boundary. It must stay "pending" until the entire processing transaction is confirmed.
1.3 Existing Pattern: sendOffsetsToTransaction()
| Feature | Traditional Consumer Groups | Share Groups (Proposal) |
| Commit Method | sendOffsetsToTransaction() | sendShareAcksToTransaction() |
| Storage | __consumer_offsets | __share_group_state |
| Atomic Fate | Records + Offsets commit together | Records + Acknowledgment commit together |
| On Abort | Consumer re-reads from old offset | Broker reverts records to AVAILABLE |
...
2. Use Cases
2.1 Consume-Transform-Produce (CTP)
...
| Code Block |
|---|
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 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 |
|---|
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 Reuse of Existing 2PC Protocol
...
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.
...
- Application catches
KafkaException, callsabortTransaction() - Same as Case 6: everything rolled back, records re-delivered
- No data loss
The Guarantee Matrix
| Scenario | Output Records | Share Acks | Records Re-delivered? | Data Loss? | Duplicates? |
|---|---|---|---|---|---|
| Happy path (commit) | Visible | Materialized | No | No | No |
| Crash before txn | Never written | Never sent | Yes (lock expiry) | No | No |
| Crash mid-txn | Aborted (invisible) | Discarded | Yes (lock expiry) | No | No |
| Crash after PREPARE_COMMIT | Committed on recovery | Materialized on recovery | No | No | No |
| Zombie fenced | Aborted | Discarded | Yes (new instance) | No | No |
| Explicit abort | Aborted (invisible) | Discarded | Yes (lock expiry) | No | No |
| TC crash after PREPARE | Completed on failover | Completed on failover | No | No | No |
The fundamental property: at no point can output records be visible while share acks are uncommitted, or vice versa. They are in the same transaction and share the same COMMIT/ABORT fate.
...