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.
Current state: Voting
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).
Today, the Producer's sendOffsetsToTransaction(offsets, consumerGroupMetadata) allows EOS in read-process-write topologies that consume from
regular consumer groups. With KIP-932 introducing share groups, the equivalent capability is missing for share-group consumers.
This blocks share-group adoption in:
1. MirrorMaker and other Kafka-to-Kafka mirroring/forwarding pipelines.
2. Kafka Streams stateless topologies that want to use share groups for parallelism beyond partition count.
3. Atomic DLQ write in different connectors
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. This KIP is scopred for Kafka producer write AND consumes from a share group.
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 by commitSync()
Producer (clients module)
public interface Producer<K, V> {
/**
* Sends a list of share-group acknowledgements to the consumer-coordinator and marks
* them for atomic commit alongside the records produced in this transaction.
*
* The acknowledgements are staged on the broker in a TX_PENDING state until the
* transaction is committed or aborted. On commit, ACCEPT records transition to
* ACKNOWLEDGED and REJECT records transition to ARCHIVING/ARCHIVED. On abort,
* all staged records revert to ACQUIRED and remain owned by the original consumer
* until either the consumer re-acknowledges them or the acquisition lock expires.
*
* @param acknowledgements Per-partition list of acknowledgement batches. Only
* AcknowledgeType.ACCEPT (1) and AcknowledgeType.REJECT (3)
* are valid inside a transaction. RELEASE (2), RENEW (4),
* and GAP (0) are rejected with InvalidRecordStateException.
* @param groupMetadata Snapshot of the share consumer's group identity, obtained
* from ShareConsumer.shareGroupMetadata().
*
* @throws IllegalStateException if no transaction is in progress, or if the producer
* is not transactional.
* @throws ProducerFencedException if another producer with the same transactionalId
* has fenced this one.
* @throws UnsupportedVersionException if the cluster does not advertise apiKey 93
* (TxnShareAcknowledge) via ApiVersions.
* @throws GroupAuthorizationException if the configured principal cannot Write to
* the share group.
* @throws InvalidProducerEpochException if the producer's epoch is stale.
* @throws KafkaException for other non-fatal errors that may be retried by aborting
* the transaction and retrying the entire read-process-write
* loop.
*
* Threading: Returns immediately after enqueuing the request for the producer's
* background Sender thread. The broker has NOT processed it yet.
* Any error is reported when commitTransaction() or abortTransaction() is later called.
*/
void sendShareAcknowledgementsToTransaction(
Map<TopicIdPartition, List<AcknowledgementBatch>> acknowledgements,
ShareGroupMetadata groupMetadata
) throws ProducerFencedException;
} |
ShareConsumer (clients module)
public interface ShareConsumer<K, V> {
/**
* Returns an immutable snapshot of this consumer's share-group identity for use
* with Producer.sendShareAcknowledgementsToTransaction.
*
* The snapshot captures groupId, memberId, and memberEpoch atomically; if a
* rebalance changes the memberEpoch between the snapshot and the producer call,
* the broker will reject the staging request with STALE_MEMBER_EPOCH and the
* user must abort the transaction and retry the read-process-write loop.
*
* @throws UnsupportedVersionException if the cluster does not support KIP-1289.
* @throws TimeoutException if the snapshot cannot be obtained within the
* configured default.api.timeout.ms.
*
* Threading: thread-safe; safe to call concurrently with poll() and acknowledge().
*/
ShareGroupMetadata shareGroupMetadata();
} |
ShareGroupMetadata (new class in clients module, package o.a.k.clients.consumer)
/* Thread-safe, Immutable, Concurrent with poll/acknowledge */
public final class ShareGroupMetadata {
public ShareGroupMetadata(String groupId, String memberId, int memberEpoch);
public String groupId();
public String memberId();
public int memberEpoch();
@Override public boolean equals(Object other);
@Override public int hashCode();
@Override public String toString();
} |
New RPC: TxnShareAcknowledgeRequest (apiKey 93) Listeners: broker. Acknowledge type values: 0=Gap, 1=Accept, 2=Release, 3=Reject, 4=Renew. Transactional constraint: only 1 (Accept) and 3 (Reject) are valid inside a transaction. A batch containing any other value is rejected with INVALID_RECORD_STATE. |
| Field | Type | Notes |
|---|---|---|
| TransactionalId | string (nullable) | The producer's transactional.id. |
| GroupId | string, entityType=groupId | The share group ID. |
| ProducerId | int64, entityType=producerId | For fencing. |
| ProducerEpoch | int16 | For fencing. |
| MemberId | string, entityType=memberId | The share group member ID. |
| MemberEpoch | int32 | For share-group fencing. |
| Topics | []TxnShareAcknowledgeTopic | mapKey=true on TopicId. |
| Topics.TopicId | uuid, mapKey=true | |
| Topics.Partitions | []TxnShareAcknowledgePartition | mapKey=true on PartitionIndex. |
| Partitions.PartitionIndex | int32, mapKey=true | |
| Partitions.AcknowledgementBatches | []TxnShareAcknowledgeBatch | |
| Batch.FirstOffset | int64 | Inclusive. |
| Batch.LastOffset | int64 | Inclusive. |
| Batch.AcknowledgeTypes | []int8 | Per-offset ack type byte. Size 1 = uniform type for whole range. |
New RPC: TxnShareAcknowledgeResponse (apiKey 94, v0) Top-level supported errors: - GROUP_AUTHORIZATION_FAILED - TOPIC_AUTHORIZATION_FAILED - TRANSACTIONAL_ID_AUTHORIZATION_FAILED - TRANSACTIONAL_ID_NOT_FOUND - INVALID_PRODUCER_EPOCH / PRODUCER_FENCED - INVALID_PRODUCER_ID_MAPPING - INVALID_TXN_STATE - UNKNOWN_MEMBER_ID - STALE_MEMBER_EPOCH - TRANSACTION_ABORTABLE (KIP-890) - UNKNOWN_SERVER_ERROR Per-partition supported errors: - UNKNOWN_TOPIC_OR_PARTITION / UNKNOWN_TOPIC_ID - NOT_LEADER_OR_FOLLOWER (with CurrentLeader populated) - INVALID_RECORD_STATE - INVALID_REQUEST - KAFKA_STORAGE_ERROR |
| Field | Type | Notes |
|---|---|---|
| ThrottleTimeMs | int32 | |
| ErrorCode | int16 | Top-level error. |
| Responses | []TxnShareAcknowledgeTopicResponse | |
| Responses.TopicId | uuid, mapKey=true | |
| Responses.Partitions | []TxnShareAcknowledgePartitionResponse | |
| Partitions.PartitionIndex | int32 | |
| Partitions.ErrorCode | int16 | Per-partition error. |
| Partitions.ErrorMessage | string (nullable) | |
| Partitions.CurrentLeader | LeaderIdAndEpoch (tagged) | Populated on NOT_LEADER_OR_FOLLOWER. |
| NodeEndpoints | []NodeEndpoint (tagged) | Top-level: address of any new leader referenced above. |
- WriteTxnMarkers (apiKey 27): NO schema change. The broker hooks the
existing marker arrival in KafkaApis.handleWriteTxnMarkersRequest and broadcasts
to all SharePartition instances on that broker. Per-record fencing by
(producerId, producerEpoch) filters out non-participants. This means the
TransactionCoordinator does NOT need to track share-partitions as transaction
participants explicitly — the broadcast is correct because the per-record
state filter is the source of truth.
- AddPartitionsToTxn (apiKey 24): NO schema change. KIP-1289 does NOT add share
partitions as transaction participants. The transactional output-write path
continues to use AddPartitionsToTxn for the output topics; the share-group
staging path uses TxnShareAcknowledge directly (TV2-only, single round trip).
- ApiVersions (apiKey 18): NO schema change, but the broker MUST advertise
apiKey 93 only when feature-flag share.version >= 2 (see Enablement section).
- TxnOffsetCommit (apiKey 28): NO schema change. Independent path for regular
consumer groups; share groups use the new RPC.
TransactionManager (Client)AddPartitionsToTxn("output-A-0") to TxnCoord.TxnCoord records participants = {output-A-0} in __transaction_state.TransactionManager (Client) & TransactionCoordinator (Broker Core)AddOffsetsToTxn("my-group") to TxnCoord.TxnCoord computes hash("my-group") = (let's say) 7.participants = {output-A-0, __consumer_offsets-7} in __transaction_state .TransactionCoordinator (Broker Core)__transaction_state and resolves brokers via MetadataCache:output-A-0 — > Broker 3__consumer_offsets-7 ---→ Broker 4WriteTxnMarkers to Broker 3 AND Broker 4.KafkaApis & GroupCoordinator (Broker Core)COMMIT batch to output-A-0 log (makes records visible to read_committed consumers).COMMIT batch to __consumer_offsets-7 log (makes staged offsets visible to GroupCoordinator).TransactionManager (Client)AddPartitionsToTxnRequest(["output-A-0"]).TransactionMetadata.topicPartitions = {output-A-0}.TransactionManager (Client) ---→ KafkaApis (Broker Core)TxnShareAcknowledgeRequest to Broker 5 (the SharePartition leader - lets say it is broker 5).KafkaApis intercepts the request and performs two background operations:shareCoordinatorPartition = (__share_group_state, partitionFor(groupId)).txnCoordinator.handleAddPartitionsToTransaction to dynamically register __share_group_state-N in __transaction_state.__transaction_state via TransactionLogValue: topicPartitions = {output-A-0, __share_group_state-N}TransactionMetadata now tracks both topicPartitions = {output-A-0, __share_group_state-N}.SharePartitionManager stages the transactional acknowledgements, transitioning the internal InFlightState to TX_PENDING.TransactionCoordinator(Broker Core)EndTxnRequest(COMMIT).TxnCoord reads the topic partitions from state and resolves the physical leaders via MetadataCache:output-A-0 --------> Broker 3__share_group_state-N -----→ Broker 5 WriteTxnMarkers to both brokers.COMMIT control batch to the output-A-0 log.TX_PENDING here)COMMIT control batch to the __share_group_state-N log.sharePartitionManager.applyTxnMarker(COMMIT).partitionCache, finds the matching TX_PENDING flight states, and commits them to ACKNOWLEDGED.TxnCoord.TxnCoord transitions the state to COMPLETE_COMMIT.producer.commitTransaction() call returns successfully.[Note: Only TV 2 supporting - broker-side auto-registration and proper epoch fencing, do not want to maintain TV1 code. TV2 is already default option since 4.0]
no ShareCoordinatorShard changes; no snapshot replay changes; no compatibility risk.
existing handleAddPartitionsToTransaction accepts arbitrary TopicPartition; no new code path on TxnCoord side
// One-time setup
shareConsumer.subscribe(List.of("source-topic"));
producer.initTransactions(); // exactly once per producer instance
// CTP loop
while (running) {
ConsumerRecords<K, V> records = shareConsumer.poll(Duration.ofSeconds(5));
if (records.isEmpty()) continue;
try {
producer.beginTransaction();
Map<TopicIdPartition, Acknowledgements> acks = new HashMap<>();
for (ConsumerRecord<K, V> record : records) {
V output = process(record);
producer.send(new ProducerRecord<>("destination-topic", output));
acks.computeIfAbsent(
new TopicIdPartition(record.topicId(),
new TopicPartition(record.topic(), record.partition())),
k -> Acknowledgements.empty()
).add(record.offset(), AcknowledgeType.ACCEPT); // accumulate
}
// Compress acks to AcknowledgementBatch wire form and stage in the txn.
producer.sendShareAcknowledgementsToTransaction(
toBatches(acks), // Map<TopicIdPartition, List<AcknowledgementBatch>>
shareConsumer.shareGroupMetadata()
);
producer.commitTransaction(); // BLOCKS — true synchronisation point
// Records are now both produced AND acknowledged ATOMICALLY.
// Do NOT call shareConsumer.acknowledge(...) on these records.
// if called mistakenly then non-transactional acknowledge is rejected (TX_PENDING to ACKNOWLEDGED - InvalidRecordStateException); the transactional path works.
} catch (ProducerFencedException | UnsupportedVersionException fatal) {
throw fatal; // unrecoverable — kill the process
} catch (KafkaException abortable) {
producer.abortTransaction(); // BLOCKS — txn reverted; staged records revert to ACQUIRED on broker
// Records will be re-delivered on next poll(); retry naturally.
}
}
|
Here are your corner cases formatted using the same clean, un-phased, human-style layout. The wording, titles, technical specifications, and internal error codes remain exactly as provided.
__share_group_state schema unchanged in v1); records replay as ACQUIRED/AVAILABLE after broker restart and are redelivered.ShareSnapshotValue.DeliveryState deferred to a follow-up KIP.AddPartitionsToTxn returns retriable error.KafkaException → abortTransaction() → retries.TxnShareAcknowledge after producer B (epoch 2) has been initialized.InFlightState (stagedProducerId, stagedProducerEpoch) filter rejects mismatched markers; idempotent.InFlightStateTxnTest.group.share.record.lock.duration.ms (default 30s; per share group).WriteTxnMarkers(ABORT); broker reverts TX_PENDING → ACQUIRED → AVAILABLE on next lock cycle. No data loss; eventual redelivery.memberEpoch between shareGroupMetadata() snapshot and sendShareAcksToTransaction() call.memberEpoch with current SharePartition owner; mismatch → returns STALE_MEMBER_EPOCH; no staging.TxnShareAcknowledge).ApiVersions; if apiKey 93 absent on any broker → UnsupportedVersionException thrown synchronously, no bytes on wire.AddPartitionsToTxn still needs cleanup).share.version=2 finalized feature flag controls broker advertisement.AcknowledgeType.RELEASE (2), RENEW (4), or GAP (0) inside sendShareAcksToTransaction.INVALID_RECORD_STATE; no staging.ACCEPT (1) and REJECT (3) — release/renew/gap have no transactional semantics.WriteTxnMarkers dispatch.MetadataCache → marker goes to broker 7; broker 7's in-memory partitionCache does NOT contain the original TX_PENDING state.TxnShareAcknowledge arrives with stale epoch.PRODUCER_FENCED; broker never stages. Per-record fencing on the state machine prevents any stale marker from resolving newer staging.Note: We can persist the record state and get rid of case 1 in follow up KIP.
All new metrics follow the existing Kafka metric conventions (Yammer for broker JMX, KafkaMetric for client-side).
Metric names mirror the established kafka.server:type=group-coordinator-metrics,name=... and kafka.server:type=share-coordinator-metrics,name=... patterns.
Backward-compatible: no existing metrics renamed or removed.
| Module | Metric | Type | Purpose |
|---|---|---|---|
| Broker — SharePartitionManager | TxnPendingRecordsCount | Gauge | Current count of records in TX_PENDING (primary health signal) |
| Broker — SharePartitionManager | TxnShareAcknowledgeRequestLatencyMs | Histogram | p99 latency of staging requests |
| Broker — SharePartitionManager | TxnPendingLockExpiredCount | Counter | Any non-zero = abandoned txns or missing markers (critical alert) |
| Broker — TransactionCoordinator | TransactionPartitionsCount (existing) | Gauge | Reused; now includes __share_group_state-N entries |
| Producer | share-ack-txn-send-rate | Meter | EOS-call throughput |
| Producer | share-ack-txn-send-error-rate | Meter | Stage-failure rate (drives retry loops) |
| Consumer | share-group-metadata-fetch-rate | Meter | Confirms read-process-write loop is active |
| Total: 6 new + 1 reused = 7 metrics. Primary alert: TxnPendingRecordsCount > 0 sustained for > 60s. |
opt-in, online-upgradable, and zero-disruption for existing workloads - No new feature flag introduced. Uses existing kafka-features.sh machinery.
| Feature flag | Required level | Why |
|---|---|---|
| share.version | >= 2 | New level finalises KIP-1289; brokers below this advertise no apiKey <93> |
| transaction.version | >= 2 | KIP-1289 is TV2-only (inherits auto-registration and proper epoch fencing) |
Phase 1 — Software upgrade (rolling)
Upgrade brokers one at a time to the binary containing KIP-1289.
KIP-1289 code is dormant (share.version still 1).
Standard Kafka rolling-restart
Phase 2 — Soak timing
Verify cluster health
Phase 3 — Finalise feature
kafka-features.sh upgrade --feature share.version --version 2
- Online metadata propagation (update of __cluster_metadata)
- No restart, no rebalance, no leader change, no socket disruption
- Brokers begin advertising apiKey 93
Phase 4 — Client onboarding
Roll out applications calling sendShareAcknowledgementsToTransaction.
Add the alerts (esp. TxnPendingRecordsCount) and relevant metrics in dashboard & observability.
| Client | Broker, share.version=2 | Broker, share.version=1 | Old broker (no binary) |
|---|---|---|---|
| New | Works | UnsupportedVersionException (synchronous, no wire bytes) | Same |
| Old | Unaffected | Unaffected | Unaffected |
1. Drain in-flight TX_PENDING:
- Halt producers calling sendShareAcknowledgementsToTransaction
- Wait for transaction.timeout.ms (default 60s) for any abandoned txns to clear
2. kafka-features.sh downgrade --feature share.version --version 1
Broker rejects the downgrade RPC if any in-memory TX_PENDING exists, preventing data inconsistency.
Software downgrade (binary): must follow feature downgrade; standard rolling restart.
The verification strategy focuses on state machine integrity and fault tolerance under high-concurrency and failure scenarios.
Unit Tests: Validates state transitions (e.g., ACQUIRED to ACKNOWLEDGED on commit vs. AVAILABLE on abort), idempotency of operations, and transaction timeout/auto-abort logic.
Integration Tests: Focuses on end-to-end commit/abort flows, coordinator recovery, and multi-consumer behavior within a single group during network partitions.
System & Performance Tests: Benchmarks transactional vs. non-transactional modes and verifies exactly-once delivery.
Chaos Tests: Simulates broker and coordinator crashes specifically during critical phases like PREPARE_COMMIT to ensure protocol durability.
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.