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

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.

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:



Current Acknowledgment Modes:

  1. Implicit: Automatic ack on the subsequent poll().

  2. Explicit: Manual ack via acknowledge(record, type) followed by commitSync()

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

Traditional Consumer Groups rely on replayability, which Share Groups lack:

1.3 Existing Pattern: sendOffsetsToTransaction()

FeatureTraditional Consumer GroupsShare Groups (Proposal)
Commit MethodsendOffsetsToTransaction()sendShareAcksToTransaction()
Storage__consumer_offsets__share_group_state
Atomic FateRecords + Offsets commit togetherRecords + Acknowledgment commit together
On AbortConsumer re-reads from old offsetBroker reverts records to AVAILABLE

2. Use Cases

2.1 Consume-Transform-Produce (CTP)

Ensures that output records and source acknowledgments are committed as a single atomic unit within a Kafka-to-Kafka pipeline.

2.2 Source-Only Frameworks (No Kafka Producer)

For applications writing to external systems (Databases, S3) that require transactional acknowledgments coordinated with their own internal checkpoints.


2.3 End-to-End Exactly-Once (Flink/Spark/OLAP)

Integrates Share Groups into the two-phase commit (2PC) lifecycle of streaming engines.

3 Public Interfaces

A new method is added to KafkaProducer to mirror traditional offset commits:

KIP-1310: General Transaction Session#4.5Custom2PCCoordinators

// Phase 1: Prepare
kafkaSession.beginTransaction();
producer.send(records);
database.prepareTransaction(dbTxnId); 
kafkaSession.prepareTransaction();    

// Phase 2: Commit (Recovery-friendly)
TransactionSession resumed = TransactionSession.resume(txnId, pid, epoch, configs);
resumed.commitTransaction();          
database.commitTransaction(dbTxnId);

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

{
  "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)

{
  "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():


// 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.


5 Flow diagram 


CLIENT --> BROKER communication (over the Kafka wire protocol):

  Share Consumer -----> Share Group Coordinator
    ShareFetch, ShareAcknowledge, ShareGroupHeartbeat

  TransactionManager --> Transaction Coordinator
    InitProducerId, AddPartitionsToTxn, AddShareAcksToTxn(NEW), EndTxn

  TransactionManager --> Share Group Coordinator
    TxnShareAcknowledgeRequest (NEW)

  Producer ------------> Data Partition Leaders
    ProduceRequest


BROKER --> BROKER communication (internal, not client-visible):

  Transaction Coordinator --> Data Partition Leaders
    WriteTxnMarkers (COMMIT/ABORT control record)

  Transaction Coordinator --> Group Coordinator (via __consumer_offsets leader)
    WriteTxnMarkers (materializes transactional offset commits)

  Transaction Coordinator --> Share Group Coordinator (via __share_group_state leader) [NEW]
    WriteTxnMarkers (materializes transactional share acks)



Abort case: 


6. Corner Cases

Case 1: Crash BEFORE beginTransaction()

poll() -> records [0..9] ACQUIRED
<CRASH>

What happens:

Case 2: Crash AFTER send() but BEFORE commitTransaction()

poll() -> records [0..9] ACQUIRED
beginTransaction()
send(output for 0-9)       ← records written to broker, but transactional
<CRASH>

What happens:

Case 3: Crash AFTER sendShareAcksToTransaction() but BEFORE commitTransaction()

poll() -> records [0..9] ACQUIRED
beginTransaction()
send(output)
sendShareAcksToTransaction()  ← acks written to SGC as PENDING
<CRASH>

What happens:

Case 4: Crash AFTER EndTxn(COMMIT) sent but BEFORE WriteTxnMarkers completes

poll() -> records [0..9] ACQUIRED
beginTransaction()
send(output)
sendShareAcksToTransaction()
commitTransaction()            ← EndTxn sent, PREPARE_COMMIT logged
<TC crashes or broker restart>

What happens:

Case 5: ProducerFencedException (zombie detection)

Instance A: beginTransaction(), send(), ...
Instance B: initTransactions() with same transactional.id
            ← TC bumps epoch, A is now a zombie
Instance A: commitTransaction()
            ← TC rejects: ProducerFencedException

What happens:

Case 6: abortTransaction() called explicitly

poll() -> records [0..9] ACQUIRED
beginTransaction()
send(output for 0-4)
record 5 fails validation
abortTransaction()             ← explicit abort

What happens:

Case 7: Partial produce failure (network error to one partition)

beginTransaction()
send("enriched-orders-0", rec1)   ← success
send("enriched-orders-1", rec2)   ← network error, KafkaException

What happens:

The Guarantee Matrix

ScenarioOutput RecordsShare AcksRecords Re-delivered?Data Loss?Duplicates?
Happy path (commit)VisibleMaterializedNoNoNo
Crash before txnNever writtenNever sentYes (lock expiry)NoNo
Crash mid-txnAborted (invisible)DiscardedYes (lock expiry)NoNo
Crash after PREPARE_COMMITCommitted on recoveryMaterialized on recoveryNoNoNo
Zombie fencedAbortedDiscardedYes (new instance)NoNo
Explicit abortAborted (invisible)DiscardedYes (lock expiry)NoNo
TC crash after PREPARECompleted on failoverCompleted on failoverNoNoNo

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.


7. 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

8. 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.