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 cde0d5687d2 KAFKA-20743: Restrict sentinel leader and state epoch in
share coord. (#22685)
cde0d5687d2 is described below
commit cde0d5687d29e7b0bced4f5b57f234b2dd63f8a0
Author: Sushant Mahajan <[email protected]>
AuthorDate: Fri Jun 26 21:10:34 2026 +0530
KAFKA-20743: Restrict sentinel leader and state epoch in share coord.
(#22685)
* currently in ShareCoordinatorShard the writeState RPC allows negative
leader and state epoch values, readState allows negative
leaderEpoch and initState allows negative stateEpoch.
* though these epoch's could never be injected by the client and are
server controlled, still we need to harden them.
* the initState call always has the correct stateEpoch (viz groupEpoch)
by virtue of
being called from the group coordinator (both in initializing and
altering offsets).
* readState is called from
SharePartition and will always be preceded by initializeState call,
which sets the stateEpoch (non negative). Furthermore, SharePartition
also creates new leaderEpoch per leadership change.
* writeState will always be preceded by readState which will have
correctly set the leaderEpoch by virtue of SharePartition. And since
initializeState correctly sets stateEpoch - writes should contain valid
values for both the epochs.
* in light of this ShareCoordinatorShard has been tightened.
* new tests have been added to verify the change.
Reviewers: Andrew Schofield <[email protected]>, Apoorv Mittal
<[email protected]>
---
.../coordinator/share/ShareCoordinatorShard.java | 43 +++++--
.../share/ShareCoordinatorShardTest.java | 123 ++++++++++++++++++++-
2 files changed, 151 insertions(+), 15 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 705ba35a35f..1f5296eeb9c 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
@@ -89,6 +89,8 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
public static final Exception NULL_TOPIC_ID = new Exception("The topic id
cannot be null.");
public static final Exception NEGATIVE_PARTITION_ID = new Exception("The
partition id cannot be a negative number.");
+ public static final Exception NEGATIVE_LEADER_EPOCH = new Exception("The
leader epoch cannot be a negative number.");
+ public static final Exception NEGATIVE_STATE_EPOCH = new Exception("The
state epoch cannot be a negative number.");
public static final Exception WRITE_UNINITIALIZED_SHARE_PARTITION = new
Exception("Write operation on uninitialized share partition not allowed.");
public static final Exception READ_UNINITIALIZED_SHARE_PARTITION = new
Exception("Read operation on uninitialized share partition not allowed.");
@@ -386,8 +388,7 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
);
// Optimization in case leaderEpoch update is not required.
- if (leaderEpoch == -1 ||
- (leaderEpochMap.get(key) != null && leaderEpochMap.get(key) ==
leaderEpoch)) {
+ if (leaderEpochMap.get(key) != null && leaderEpochMap.get(key) ==
leaderEpoch) {
return new CoordinatorResult<>(List.of(), responseData);
}
@@ -613,7 +614,8 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
/**
* Remove share partitions corresponding to the input topic ids, if
present.
- * @param deletedTopicIds The topic ids which have been deleted
+ *
+ * @param deletedTopicIds The topic ids which have been deleted
* @return A result containing relevant coordinator records and void
response
*/
public CoordinatorResult<Void, CoordinatorRecord>
maybeCleanupShareState(Set<Uuid> deletedTopicIds) {
@@ -654,10 +656,7 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
int updatesPerSnapshotLimit =
config.shareCoordinatorSnapshotUpdateRecordsPerSnapshot();
ShareGroupOffset currentState = shareStateMap.get(key); // This method
assumes containsKey is true.
- int newLeaderEpoch = currentState.leaderEpoch();
- if (updateLeaderEpoch) {
- newLeaderEpoch = partitionData.leaderEpoch() != -1 ?
partitionData.leaderEpoch() : newLeaderEpoch;
- }
+ int newLeaderEpoch = updateLeaderEpoch ? partitionData.leaderEpoch() :
currentState.leaderEpoch();
if (snapshotUpdateCount.getOrDefault(key, 0) >=
updatesPerSnapshotLimit) {
// shareStateMap will have the entry as containsKey is true
@@ -747,6 +746,8 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
Uuid topicId = topicData.topicId();
int partitionId = partitionData.partition();
+ int leaderEpoch = partitionData.leaderEpoch();
+ int stateEpoch = partitionData.stateEpoch();
if (topicId == null) {
return
Optional.of(getWriteErrorCoordinatorResult(Errors.INVALID_REQUEST,
NULL_TOPIC_ID, null, partitionId));
@@ -756,17 +757,25 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
return
Optional.of(getWriteErrorCoordinatorResult(Errors.INVALID_REQUEST,
NEGATIVE_PARTITION_ID, topicId, partitionId));
}
+ if (leaderEpoch < 0) {
+ return
Optional.of(getWriteErrorCoordinatorResult(Errors.INVALID_REQUEST,
NEGATIVE_LEADER_EPOCH, topicId, partitionId));
+ }
+
+ if (stateEpoch < 0) {
+ return
Optional.of(getWriteErrorCoordinatorResult(Errors.INVALID_REQUEST,
NEGATIVE_STATE_EPOCH, topicId, partitionId));
+ }
+
SharePartitionKey mapKey = SharePartitionKey.getInstance(groupId,
topicId, partitionId);
if (!shareStateMap.containsKey(mapKey)) {
return
Optional.of(getWriteErrorCoordinatorResult(Errors.INVALID_REQUEST,
WRITE_UNINITIALIZED_SHARE_PARTITION, topicId, partitionId));
}
- if (partitionData.leaderEpoch() != -1 &&
leaderEpochMap.containsKey(mapKey) && leaderEpochMap.get(mapKey) >
partitionData.leaderEpoch()) {
+ if (leaderEpochMap.containsKey(mapKey) && leaderEpochMap.get(mapKey) >
partitionData.leaderEpoch()) {
log.error("Write request leader epoch is smaller than last
recorded current: {}, requested: {}.", leaderEpochMap.get(mapKey),
partitionData.leaderEpoch());
return
Optional.of(getWriteErrorCoordinatorResult(Errors.FENCED_LEADER_EPOCH, null,
topicId, partitionId));
}
- if (partitionData.stateEpoch() != -1 &&
stateEpochMap.containsKey(mapKey) && stateEpochMap.get(mapKey) >
partitionData.stateEpoch()) {
+ if (stateEpochMap.containsKey(mapKey) && stateEpochMap.get(mapKey) >
partitionData.stateEpoch()) {
log.info("Write request state epoch is smaller than last recorded
current: {}, requested: {}.", stateEpochMap.get(mapKey),
partitionData.stateEpoch());
return
Optional.of(getWriteErrorCoordinatorResult(Errors.FENCED_STATE_EPOCH, null,
topicId, partitionId));
}
@@ -789,6 +798,7 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
String groupId = request.groupId();
ReadShareGroupStateRequestData.ReadStateData topicData =
request.topics().get(0);
ReadShareGroupStateRequestData.PartitionData partitionData =
topicData.partitions().get(0);
+ int leaderEpoch = partitionData.leaderEpoch();
Uuid topicId = topicData.topicId();
int partitionId = partitionData.partition();
@@ -805,6 +815,12 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
topicId, partitionId, Errors.INVALID_REQUEST,
NEGATIVE_PARTITION_ID.getMessage()));
}
+ if (leaderEpoch < 0) {
+ log.error("Request leader epoch is negative.");
+ return Optional.of(ReadShareGroupStateResponse.toErrorResponseData(
+ topicId, partitionId, Errors.INVALID_REQUEST,
NEGATIVE_LEADER_EPOCH.getMessage()));
+ }
+
SharePartitionKey mapKey = SharePartitionKey.getInstance(groupId,
topicId, partitionId);
if (!shareStateMap.containsKey(mapKey)) {
@@ -813,7 +829,7 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
topicId, partitionId, Errors.INVALID_REQUEST,
READ_UNINITIALIZED_SHARE_PARTITION.getMessage()));
}
- if (partitionData.leaderEpoch() != -1 &&
leaderEpochMap.containsKey(mapKey) && leaderEpochMap.get(mapKey) >
partitionData.leaderEpoch()) {
+ if (leaderEpochMap.containsKey(mapKey) && leaderEpochMap.get(mapKey) >
partitionData.leaderEpoch()) {
log.error("Read request leader epoch is smaller than last recorded
current: {}, requested: {}.", leaderEpochMap.get(mapKey),
partitionData.leaderEpoch());
return
Optional.of(ReadShareGroupStateResponse.toErrorResponseData(topicId,
partitionId, Errors.FENCED_LEADER_EPOCH, Errors.FENCED_LEADER_EPOCH.message()));
}
@@ -910,6 +926,7 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
Uuid topicId = topicData.topicId();
int partitionId = partitionData.partition();
+ int stateEpoch = partitionData.stateEpoch();
if (topicId == null) {
return
Optional.of(getInitializeErrorCoordinatorResult(Errors.INVALID_REQUEST,
NULL_TOPIC_ID, null, partitionId));
@@ -919,8 +936,12 @@ public class ShareCoordinatorShard implements
CoordinatorShard<CoordinatorRecord
return
Optional.of(getInitializeErrorCoordinatorResult(Errors.INVALID_REQUEST,
NEGATIVE_PARTITION_ID, topicId, partitionId));
}
+ if (stateEpoch < 0) {
+ return
Optional.of(getInitializeErrorCoordinatorResult(Errors.INVALID_REQUEST,
NEGATIVE_STATE_EPOCH, topicId, partitionId));
+ }
+
SharePartitionKey key =
SharePartitionKey.getInstance(request.groupId(), topicId, partitionId);
- if (partitionData.stateEpoch() != -1 && stateEpochMap.containsKey(key)
&& stateEpochMap.get(key) > partitionData.stateEpoch()) {
+ if (stateEpochMap.containsKey(key) && stateEpochMap.get(key) >
partitionData.stateEpoch()) {
log.info("Initialize request state epoch is smaller than last
recorded current: {}, requested: {}.", stateEpochMap.get(key),
partitionData.stateEpoch());
return
Optional.of(getInitializeErrorCoordinatorResult(Errors.FENCED_STATE_EPOCH,
Errors.FENCED_STATE_EPOCH.exception(), topicId, partitionId));
}
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 7e44c9b4a6a..23558216dc5 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
@@ -411,7 +411,7 @@ class ShareCoordinatorShardTest {
}
@Test
- public void testWriteStateInvalidRequestData() {
+ public void testWriteStateInvalidPartitionRequestData() {
int partition = -1;
WriteShareGroupStateRequestData request = new
WriteShareGroupStateRequestData()
@@ -443,6 +443,72 @@ class ShareCoordinatorShardTest {
assertNull(shard.getLeaderMapValue(SHARE_PARTITION_KEY));
}
+ @Test
+ public void testWriteStateInvalidLeaderEpochRequestData() {
+ int leaderEpoch = -1;
+
+ WriteShareGroupStateRequestData request = new
WriteShareGroupStateRequestData()
+ .setGroupId(GROUP_ID)
+ .setTopics(List.of(new
WriteShareGroupStateRequestData.WriteStateData()
+ .setTopicId(TOPIC_ID)
+ .setPartitions(List.of(new
WriteShareGroupStateRequestData.PartitionData()
+ .setPartition(0)
+ .setStartOffset(0)
+ .setDeliveryCompleteCount(11)
+ .setStateEpoch(0)
+ .setLeaderEpoch(leaderEpoch)
+ .setStateBatches(List.of(new
WriteShareGroupStateRequestData.StateBatch()
+ .setFirstOffset(0)
+ .setLastOffset(10)
+ .setDeliveryCount((short) 1)
+ .setDeliveryState((byte) 0)))))));
+
+ CoordinatorResult<WriteShareGroupStateResponseData, CoordinatorRecord>
result = shard.writeState(request);
+
+ WriteShareGroupStateResponseData expectedData =
WriteShareGroupStateResponse.toErrorResponseData(
+ TOPIC_ID, 0, Errors.INVALID_REQUEST,
ShareCoordinatorShard.NEGATIVE_LEADER_EPOCH.getMessage());
+ List<CoordinatorRecord> expectedRecords = List.of();
+
+ assertEquals(expectedData, result.response());
+ assertEquals(expectedRecords, result.records());
+
+ assertNull(shard.getShareStateMapValue(SHARE_PARTITION_KEY));
+ assertNull(shard.getLeaderMapValue(SHARE_PARTITION_KEY));
+ }
+
+ @Test
+ public void testWriteStateInvalidStateEpochRequestData() {
+ int stateEpoch = -1;
+
+ WriteShareGroupStateRequestData request = new
WriteShareGroupStateRequestData()
+ .setGroupId(GROUP_ID)
+ .setTopics(List.of(new
WriteShareGroupStateRequestData.WriteStateData()
+ .setTopicId(TOPIC_ID)
+ .setPartitions(List.of(new
WriteShareGroupStateRequestData.PartitionData()
+ .setPartition(0)
+ .setStartOffset(0)
+ .setDeliveryCompleteCount(11)
+ .setStateEpoch(stateEpoch)
+ .setLeaderEpoch(0)
+ .setStateBatches(List.of(new
WriteShareGroupStateRequestData.StateBatch()
+ .setFirstOffset(0)
+ .setLastOffset(10)
+ .setDeliveryCount((short) 1)
+ .setDeliveryState((byte) 0)))))));
+
+ CoordinatorResult<WriteShareGroupStateResponseData, CoordinatorRecord>
result = shard.writeState(request);
+
+ WriteShareGroupStateResponseData expectedData =
WriteShareGroupStateResponse.toErrorResponseData(
+ TOPIC_ID, 0, Errors.INVALID_REQUEST,
ShareCoordinatorShard.NEGATIVE_STATE_EPOCH.getMessage());
+ List<CoordinatorRecord> expectedRecords = List.of();
+
+ assertEquals(expectedData, result.response());
+ assertEquals(expectedRecords, result.records());
+
+ assertNull(shard.getShareStateMapValue(SHARE_PARTITION_KEY));
+ assertNull(shard.getLeaderMapValue(SHARE_PARTITION_KEY));
+ }
+
@Test
public void testWriteNullMetadataImage() {
initSharePartition(shard, SHARE_PARTITION_KEY);
@@ -1000,12 +1066,12 @@ class ShareCoordinatorShardTest {
.setTopicId(TOPIC_ID)
.setPartitions(List.of(new
ReadShareGroupStateRequestData.PartitionData()
.setPartition(PARTITION)
- .setLeaderEpoch(-1)
+ .setLeaderEpoch(2)
))));
CoordinatorResult<ReadShareGroupStateResponseData, CoordinatorRecord>
result2 = shard.readStateAndMaybeUpdateLeaderEpoch(request2);
- assertTrue(result2.records().isEmpty()); // Leader epoch -1 - no
update.
+ assertTrue(result2.records().isEmpty()); // Leader epoch same - no
update.
assertEquals(Errors.NONE.code(),
result2.response().results().get(0).partitions().get(0).errorCode());
assertEquals(2, shard.getLeaderMapValue(SHARE_PARTITION_KEY));
@@ -1015,7 +1081,7 @@ class ShareCoordinatorShardTest {
.setTopicId(TOPIC_ID)
.setPartitions(List.of(new
ReadShareGroupStateRequestData.PartitionData()
.setPartition(PARTITION)
- .setLeaderEpoch(-1)
+ .setLeaderEpoch(2)
))));
CoordinatorResult<ReadShareGroupStateResponseData, CoordinatorRecord>
result3 = shard.readStateAndMaybeUpdateLeaderEpoch(request3);
@@ -1026,6 +1092,29 @@ class ShareCoordinatorShardTest {
verify(shard.getMetricsShard()).record(ShareCoordinatorMetrics.SHARE_COORDINATOR_WRITE_SENSOR_NAME);
}
+ @Test
+ public void testReadStateNegativeLeaderEpochDisallowed() {
+ ReadShareGroupStateRequestData request1 = new
ReadShareGroupStateRequestData()
+ .setGroupId(GROUP_ID)
+ .setTopics(List.of(new
ReadShareGroupStateRequestData.ReadStateData()
+ .setTopicId(TOPIC_ID)
+ .setPartitions(List.of(new
ReadShareGroupStateRequestData.PartitionData()
+ .setPartition(PARTITION)
+ .setLeaderEpoch(-1)
+ ))));
+
+ CoordinatorResult<ReadShareGroupStateResponseData, CoordinatorRecord>
result = shard.readStateAndMaybeUpdateLeaderEpoch(request1);
+ short errCode =
result.response().results().get(0).partitions().get(0).errorCode();
+ String errMessage =
result.response().results().get(0).partitions().get(0).errorMessage();
+
+ assertEquals(Errors.INVALID_REQUEST.code(), errCode);
+ assertEquals(ShareCoordinatorShard.NEGATIVE_LEADER_EPOCH.getMessage(),
errMessage);
+ assertTrue(result.records().isEmpty()); // Record not generated.
+
+ assertNull(shard.getLeaderMapValue(SHARE_PARTITION_KEY));
+ verify(shard.getMetricsShard(),
times(0)).record(ShareCoordinatorMetrics.SHARE_COORDINATOR_WRITE_SENSOR_NAME);
+ }
+
@Test
public void testDeleteStateSuccess() {
DeleteShareGroupStateRequestData request = new
DeleteShareGroupStateRequestData()
@@ -1294,6 +1383,32 @@ class ShareCoordinatorShardTest {
assertEquals(expectedRecords, result.records());
}
+ @Test
+ public void testInitializeStateInvalidStateEpochRequestData() {
+ // invalid stateEpoch
+ int stateEpoch = -1;
+
+ InitializeShareGroupStateRequestData request = new
InitializeShareGroupStateRequestData()
+ .setGroupId(GROUP_ID)
+ .setTopics(List.of(new
InitializeShareGroupStateRequestData.InitializeStateData()
+ .setTopicId(TOPIC_ID)
+ .setPartitions(List.of(new
InitializeShareGroupStateRequestData.PartitionData()
+ .setPartition(0)
+ .setStateEpoch(stateEpoch)
+ .setStartOffset(1)
+ ))
+ ));
+
+ CoordinatorResult<InitializeShareGroupStateResponseData,
CoordinatorRecord> result = shard.initializeState(request);
+
+ InitializeShareGroupStateResponseData expectedData =
InitializeShareGroupStateResponse.toErrorResponseData(
+ TOPIC_ID, 0, Errors.INVALID_REQUEST,
ShareCoordinatorShard.NEGATIVE_STATE_EPOCH.getMessage());
+ List<CoordinatorRecord> expectedRecords = List.of();
+
+ assertEquals(expectedData, result.response());
+ assertEquals(expectedRecords, result.records());
+ }
+
@Test
public void testInitializeNullMetadataImage() {
shard.onMetadataUpdate(null, null);