This is an automated email from the ASF dual-hosted git repository.
apoorvmittal10 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git
The following commit(s) were added to refs/heads/trunk by this push:
new b77567478f0 MINOR: Honor higher start offset value in share write
state. (#23074)
b77567478f0 is described below
commit b77567478f0d51d135e3d1e33419d9c7cb5fee69
Author: Sushant Mahajan <[email protected]>
AuthorDate: Tue Aug 4 20:20:25 2026 +0530
MINOR: Honor higher start offset value in share write state. (#23074)
In share coordinator, there was no restriction on the start offset value
causing out of order requests to regress the value. This might result in
redelivery of some records when the SharePartition leadership changes.
This PR remedies the issue by always taking the max of current view and
incoming write state request's start offset value and uses the new value
to combine and prune the state batches. A new test has been added to
verify the behavior.
Reviewers: Apoorv Mittal <[email protected]>, Andrew Schofield
<[email protected]>
---
.../coordinator/share/ShareCoordinatorShard.java | 18 ++--
.../share/ShareCoordinatorShardTest.java | 113 ++++++++++++++++++++-
2 files changed, 118 insertions(+), 13 deletions(-)
diff --git
a/share-coordinator/src/main/java/org/apache/kafka/coordinator/share/ShareCoordinatorShard.java
b/share-coordinator/src/main/java/org/apache/kafka/coordinator/share/ShareCoordinatorShard.java
index 1f5296eeb9c..2acf749cccb 100644
---
a/share-coordinator/src/main/java/org/apache/kafka/coordinator/share/ShareCoordinatorShard.java
+++
b/share-coordinator/src/main/java/org/apache/kafka/coordinator/share/ShareCoordinatorShard.java
@@ -311,6 +311,9 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
* This method as called by the ShareCoordinatorService will be provided
with
* the request data which covers only a single key i.e.
group1:topic1:partition1. The implementation
* below was done keeping this in mind.
+ * <p>
+ * If the value of startOffset is lower than that of the current state,
the current state
+ * version will be used.
*
* @param request - WriteShareGroupStateRequestData for a single key
* @return CoordinatorResult(records, response)
@@ -655,13 +658,10 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
long timestamp = time.milliseconds();
int updatesPerSnapshotLimit =
config.shareCoordinatorSnapshotUpdateRecordsPerSnapshot();
ShareGroupOffset currentState = shareStateMap.get(key); // This method
assumes containsKey is true.
-
int newLeaderEpoch = updateLeaderEpoch ? partitionData.leaderEpoch() :
currentState.leaderEpoch();
+ long newStartOffset = Math.max(partitionData.startOffset(),
currentState.startOffset());
if (snapshotUpdateCount.getOrDefault(key, 0) >=
updatesPerSnapshotLimit) {
- // shareStateMap will have the entry as containsKey is true
- long newStartOffset = partitionData.startOffset() == -1 ?
currentState.startOffset() : partitionData.startOffset();
-
// Since the number of update records for this share part key
exceeds snapshotUpdateRecordsPerSnapshot
// or state epoch has incremented, we should be creating a share
snapshot record.
// The incoming partition data could have overlapping state
batches, we must merge them.
@@ -685,10 +685,10 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
key.groupId(), key.topicId(), partitionData.partition(),
new ShareGroupOffset.Builder()
.setSnapshotEpoch(currentState.snapshotEpoch()) // Use
same snapshotEpoch as last share snapshot.
- .setStartOffset(partitionData.startOffset())
+ .setStartOffset(newStartOffset) // Prevents start offset
from regressing.
.setDeliveryCompleteCount(partitionData.deliveryCompleteCount())
.setLeaderEpoch(newLeaderEpoch)
- .setStateBatches(mergeBatches(List.of(), partitionData))
+ .setStateBatches(mergeBatches(List.of(), partitionData,
newStartOffset))
.build());
}
}
@@ -719,12 +719,6 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
);
}
- private List<PersisterStateBatch> mergeBatches(
- List<PersisterStateBatch> soFar,
- WriteShareGroupStateRequestData.PartitionData partitionData) {
- return mergeBatches(soFar, partitionData, partitionData.startOffset());
- }
-
private List<PersisterStateBatch> mergeBatches(
List<PersisterStateBatch> soFar,
WriteShareGroupStateRequestData.PartitionData partitionData,
diff --git
a/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/ShareCoordinatorShardTest.java
b/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/ShareCoordinatorShardTest.java
index 23558216dc5..95ba59dc38d 100644
---
a/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/ShareCoordinatorShardTest.java
+++
b/share-coordinator/src/test/java/org/apache/kafka/coordinator/share/ShareCoordinatorShardTest.java
@@ -163,6 +163,7 @@ class ShareCoordinatorShardTest {
shard.replay(0L, 0L, (short) 0, result.records().get(0));
+ int readStateEpoch =
result.response().results().get(0).partitions().get(0).stateEpoch();
WriteShareGroupStateRequestData request = new
WriteShareGroupStateRequestData()
.setGroupId(GROUP_ID)
.setTopics(List.of(new
WriteShareGroupStateRequestData.WriteStateData()
@@ -171,7 +172,7 @@ class ShareCoordinatorShardTest {
.setPartition(PARTITION)
.setStartOffset(0)
.setDeliveryCompleteCount(0)
- .setStateEpoch(0)
+ .setStateEpoch(readStateEpoch)
.setLeaderEpoch(leaderEpoch)
.setStateBatches(List.of(new
WriteShareGroupStateRequestData.StateBatch()
.setFirstOffset(0)
@@ -608,6 +609,115 @@ class ShareCoordinatorShardTest {
assertEquals(1, shard.getStateEpochMapValue(SHARE_PARTITION_KEY));
}
+ /**
+ * The test validates that if a previous write state request with higher
start offset
+ * had been applied to the state machine, then a subsequent write request
+ * on the same share partition key with lower start offset will have the
start offset value
+ * superseded by the previous higher value.
+ * <p>
+ * For example,
+ * req1: {
+ * startOffset: 5,
+ * stateEpoch: 5,
+ * leaderEpoch: 3,
+ * stateBatches: [(11, 20, 1, ARCHIVED)]
+ * }
+ * <p>
+ * req2: {
+ * startOffset: 2,
+ * stateEpoch: 5,
+ * leaderEpoch: 3,
+ * stateBatches: [(0, 20, 1, AVAILABLE)]
+ * }
+ * <p>
+ * Then, final state would be
+ * result: {
+ * startOffset: 5,
+ * stateEpoch: 5,
+ * leaderEpoch: 3,
+ * stateBatches: [(5, 10, 1, AVAILABLE), (11, 20, 1, ARCHIVED)]
+ * }
+ *
+ */
+ @Test
+ public void testWriteStateDiscardsStartOffsetRegression() {
+ initSharePartition(shard, SHARE_PARTITION_KEY, 5);
+ writeAndReplayRecord(shard, 3);
+ int request1StartOffset = 5;
+ int request2StartOffset = 2;
+
+ WriteShareGroupStateRequestData request1 = new
WriteShareGroupStateRequestData()
+ .setGroupId(GROUP_ID)
+ .setTopics(List.of(new
WriteShareGroupStateRequestData.WriteStateData()
+ .setTopicId(TOPIC_ID)
+ .setPartitions(List.of(new
WriteShareGroupStateRequestData.PartitionData()
+ .setPartition(PARTITION)
+ .setStartOffset(request1StartOffset)
+ .setDeliveryCompleteCount(10)
+ .setStateEpoch(5)
+ .setLeaderEpoch(3)
+ .setStateBatches(List.of(new
WriteShareGroupStateRequestData.StateBatch()
+ .setFirstOffset(11)
+ .setLastOffset(20)
+ .setDeliveryCount((short) 1)
+ .setDeliveryState((byte) 4)))))));
+
+ WriteShareGroupStateRequestData request2 = new
WriteShareGroupStateRequestData()
+ .setGroupId(GROUP_ID)
+ .setTopics(List.of(new
WriteShareGroupStateRequestData.WriteStateData()
+ .setTopicId(TOPIC_ID)
+ .setPartitions(List.of(new
WriteShareGroupStateRequestData.PartitionData()
+ .setPartition(PARTITION)
+ .setStartOffset(request2StartOffset) // Lower start
offset in second request
+ .setDeliveryCompleteCount(10)
+ .setStateEpoch(5)
+ .setLeaderEpoch(3)
+ .setStateBatches(List.of(new
WriteShareGroupStateRequestData.StateBatch()
+ .setFirstOffset(0) // First offset lower than in
request1
+ .setLastOffset(20)
+ .setDeliveryCount((short) 1)
+ .setDeliveryState((byte) 0)))))));
+
+ CoordinatorResult<WriteShareGroupStateResponseData, CoordinatorRecord>
result = shard.writeState(request1);
+ shard.replay(0L, 0L, (short) 0, result.records().get(0));
+
+ WriteShareGroupStateResponseData expectedData =
WriteShareGroupStateResponse.toResponseData(TOPIC_ID, PARTITION);
+ List<CoordinatorRecord> expectedRecords =
List.of(ShareCoordinatorRecordHelpers.newShareUpdateRecord(
+ GROUP_ID, TOPIC_ID, PARTITION,
ShareGroupOffset.fromRequest(request1.topics().get(0).partitions().get(0),
TIME.milliseconds())
+ ));
+
+ assertEquals(expectedData, result.response());
+ assertEquals(expectedRecords, result.records());
+
+ // Verify request2
+ CoordinatorResult<WriteShareGroupStateResponseData, CoordinatorRecord>
result2 = shard.writeState(request2);
+ shard.replay(0L, 0L, (short) 0, result2.records().get(0));
+
+ WriteShareGroupStateResponseData expectedData2 =
WriteShareGroupStateResponse.toResponseData(TOPIC_ID, PARTITION);
+ List<CoordinatorRecord> expectedRecords2 =
List.of(ShareCoordinatorRecordHelpers.newShareUpdateRecord(
+ GROUP_ID, TOPIC_ID, PARTITION,
ShareGroupOffset.fromRequest(request2.topics().get(0).partitions().get(0),
TIME.milliseconds()).builderSupplier()
+ .setStartOffset(request1StartOffset)
+ .setStateBatches(List.of(new
PersisterStateBatch(request1StartOffset, 20, (byte) 0, (short) 1)))
+ .build()
+ ));
+
+ assertEquals(expectedData2, result2.response());
+ assertEquals(expectedRecords2, result2.records());
+
+ ShareGroupOffset expectedFinalValue = new ShareGroupOffset.Builder()
+ .setStartOffset(request1StartOffset)
+ .setStateEpoch(5)
+ .setLeaderEpoch(3)
+ .setDeliveryCompleteCount(10)
+ .setStateBatches(List.of(
+ new PersisterStateBatch(5, 10, (byte) 0, (short) 1),
+ new PersisterStateBatch(11, 20, (byte) 4, (short) 1)
+ ))
+ .build();
+
+ assertEquals(expectedFinalValue,
shard.getShareStateMapValue(SHARE_PARTITION_KEY));
+ }
+
@Test
public void testReadFailsOnUninitializedPartition() {
ReadShareGroupStateRequestData request = new
ReadShareGroupStateRequestData()
@@ -1950,6 +2060,7 @@ class ShareCoordinatorShardTest {
private void initSharePartition(ShareCoordinatorShard shard,
SharePartitionKey key) {
initSharePartition(shard, key, 0);
}
+
private void initSharePartition(ShareCoordinatorShard shard,
SharePartitionKey key, int stateEpoch) {
shard.replay(0L, 0L, (short) 0, CoordinatorRecord.record(
new ShareSnapshotKey()