KAFKA-19535: add integration tests for DescribeProducersOptions#brokerId#8
KAFKA-19535: add integration tests for DescribeProducersOptions#brokerId#8apalan60 wants to merge 6 commits into
Conversation
|
/review
" |
|
Manual-approval option for PR-Agent is disabled. You can enable it via a configuration file |
|
/gemini review |
|
/improve
" |
There was a problem hiding this comment.
Code Review
This pull request adds integration tests for the brokerId option in DescribeProducersOptions, which is a valuable addition. The tests cover the main scenarios, including routing to leaders, followers, and invalid brokers. I've identified a couple of areas where the tests can be improved for better type safety and more robust assertions. My feedback includes specific suggestions to address these points.
| this.clusterInstance = clusterInstance; | ||
| } | ||
|
|
||
| private static void sendTestRecords(Producer<Object, Object> producer) { |
There was a problem hiding this comment.
The producer is configured with StringSerializer for both key and value, so its type is Producer<String, String>. Using Producer<Object, Object> in the method signature is not type-safe. It's better to use the specific types to leverage Java's type system and prevent potential runtime errors.
| private static void sendTestRecords(Producer<Object, Object> producer) { | |
| private static void sendTestRecords(Producer<String, String> producer) { |
| var followerProducerId = followerState.activeProducers().iterator().next().producerId(); | ||
| var leaderProducerId = leaderState.activeProducers().iterator().next().producerId(); | ||
| assertEquals(leaderProducerId, followerProducerId); |
There was a problem hiding this comment.
The current assertion only compares the producer ID of the first active producer. A more robust test would compare the entire list of active producers between the leader and the follower, similar to what is done in testDescribeProducersDefaultRoutesToLeader. This ensures that all producer states are correctly replicated to the follower.
| var followerProducerId = followerState.activeProducers().iterator().next().producerId(); | |
| var leaderProducerId = leaderState.activeProducers().iterator().next().producerId(); | |
| assertEquals(leaderProducerId, followerProducerId); | |
| assertEquals(leaderState.activeProducers(), followerState.activeProducers()); |
| var stateWithDefaultRouting = admin.describeProducers(List.of(topicPartition)) | ||
| .partitionResult(topicPartition).get(); |
There was a problem hiding this comment.
Suggestion: Await leader election and metadata stabilization before asserting producer state to avoid flakiness. Add a wait/retry until the partition has a known leader and at least one active producer is observed, with a reasonable timeout. [general, importance: 6]
| var stateWithDefaultRouting = admin.describeProducers(List.of(topicPartition)) | |
| .partitionResult(topicPartition).get(); | |
| TestUtils.waitForCondition(() -> { | |
| try { | |
| var state = admin.describeProducers(List.of(topicPartition)) | |
| .partitionResult(topicPartition).get(); | |
| return state != null && !state.activeProducers().isEmpty(); | |
| } catch (Exception e) { | |
| return false; | |
| } | |
| }, 10_000L, "Timed out waiting for active producers on leader"); | |
| var stateWithDefaultRouting = admin.describeProducers(List.of(topicPartition)) | |
| .partitionResult(topicPartition).get(); |
| var followerState = admin.describeProducers(List.of(topicPartition), | ||
| new DescribeProducersOptions().brokerId(followerBrokerId)) | ||
| .partitionResult(topicPartition).get(); | ||
| var leaderState = admin.describeProducers(List.of(topicPartition)) | ||
| .partitionResult(topicPartition).get(); | ||
|
|
||
| assertNotNull(followerState); | ||
| assertFalse(followerState.activeProducers().isEmpty()); | ||
|
|
||
| var followerProducerId = followerState.activeProducers().iterator().next().producerId(); | ||
| var leaderProducerId = leaderState.activeProducers().iterator().next().producerId(); | ||
| assertEquals(leaderProducerId, followerProducerId); |
There was a problem hiding this comment.
Suggestion: Guard against the case where leader has no active producers yet to prevent NoSuchElementException. Add waits ensuring both leader and follower responses have at least one active producer before comparing IDs. [general, importance: 7]
| var followerState = admin.describeProducers(List.of(topicPartition), | |
| new DescribeProducersOptions().brokerId(followerBrokerId)) | |
| .partitionResult(topicPartition).get(); | |
| var leaderState = admin.describeProducers(List.of(topicPartition)) | |
| .partitionResult(topicPartition).get(); | |
| assertNotNull(followerState); | |
| assertFalse(followerState.activeProducers().isEmpty()); | |
| var followerProducerId = followerState.activeProducers().iterator().next().producerId(); | |
| var leaderProducerId = leaderState.activeProducers().iterator().next().producerId(); | |
| assertEquals(leaderProducerId, followerProducerId); | |
| TestUtils.waitForCondition(() -> { | |
| try { | |
| var ls = admin.describeProducers(List.of(topicPartition)).partitionResult(topicPartition).get(); | |
| var fs = admin.describeProducers(List.of(topicPartition), new DescribeProducersOptions().brokerId(followerBrokerId)) | |
| .partitionResult(topicPartition).get(); | |
| return ls != null && fs != null && !ls.activeProducers().isEmpty() && !fs.activeProducers().isEmpty(); | |
| } catch (Exception e) { | |
| return false; | |
| } | |
| }, 10_000L, "Timed out waiting for active producers on leader and follower"); | |
| var followerState = admin.describeProducers(List.of(topicPartition), | |
| new DescribeProducersOptions().brokerId(followerBrokerId)) | |
| .partitionResult(topicPartition).get(); | |
| var leaderState = admin.describeProducers(List.of(topicPartition)) | |
| .partitionResult(topicPartition).get(); | |
| assertNotNull(followerState); | |
| assertFalse(followerState.activeProducers().isEmpty()); | |
| assertFalse(leaderState.activeProducers().isEmpty()); | |
| var followerProducerId = followerState.activeProducers().iterator().next().producerId(); | |
| var leaderProducerId = leaderState.activeProducers().iterator().next().producerId(); | |
| assertEquals(leaderProducerId, followerProducerId); |
| var followerBrokerId = replicaBrokerIds.stream() | ||
| .filter(id -> !id.equals(leaderBrokerId)) | ||
| .findFirst() | ||
| .orElseThrow(() -> new IllegalStateException("No follower found")); |
There was a problem hiding this comment.
Suggestion: Ensure a true follower is selected when multiple replicas exist by excluding the leader and, if available, preferring an in-sync replica that is not leader. This avoids accidental selection of the leader after a leadership change. [general, importance: 1]
| var followerBrokerId = replicaBrokerIds.stream() | |
| .filter(id -> !id.equals(leaderBrokerId)) | |
| .findFirst() | |
| .orElseThrow(() -> new IllegalStateException("No follower found")); | |
| var followerBrokerId = replicaBrokerIds.stream() | |
| .filter(id -> !id.equals(leaderBrokerId)) | |
| .findFirst() | |
| .orElseThrow(() -> new IllegalStateException("No follower found for partition " + topicPartition)); |
| TestUtils.assertFutureThrows(NotLeaderOrFollowerException.class, | ||
| admin.describeProducers(List.of(topicPartition), | ||
| new DescribeProducersOptions().brokerId(nonReplicaBrokerId)) | ||
| .partitionResult(topicPartition)); |
There was a problem hiding this comment.
Suggestion: Wrap the future supplier to retry until metadata reflects the chosen non-replica to avoid transient leader movements causing different exceptions. Add a bounded wait ensuring the broker remains a non-replica before asserting the failure. [general, importance: 6]
| TestUtils.assertFutureThrows(NotLeaderOrFollowerException.class, | |
| admin.describeProducers(List.of(topicPartition), | |
| new DescribeProducersOptions().brokerId(nonReplicaBrokerId)) | |
| .partitionResult(topicPartition)); | |
| // Ensure the selected broker is not a replica before asserting | |
| TestUtils.waitForCondition(() -> { | |
| try { | |
| var desc = admin.describeTopics(List.of(TOPIC_NAME)).allTopicNames().get().get(TOPIC_NAME); | |
| var currentReplicaIds = desc.partitions().get(0).replicas().stream().map(Node::id).toList(); | |
| return !currentReplicaIds.contains(nonReplicaBrokerId); | |
| } catch (Exception e) { | |
| return false; | |
| } | |
| }, 10_000L, "Timed out ensuring broker is non-replica"); | |
| TestUtils.assertFutureThrows(NotLeaderOrFollowerException.class, | |
| admin.describeProducers(List.of(topicPartition), | |
| new DescribeProducersOptions().brokerId(nonReplicaBrokerId)) | |
| .partitionResult(topicPartition)); |
|
/review
" |
PR Reviewer Guide 🔍(Review updated until commit 956c3aa)Here are some key observations to aid the review process:
|
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request adds integration tests for the brokerId option in DescribeProducersOptions. The tests cover scenarios for routing to the leader, a follower, and an invalid broker that is not a replica. The test logic is sound and covers the intended functionality well. I've provided a couple of suggestions to improve the test structure by reducing code duplication, which will enhance maintainability.
| var admin = clusterInstance.admin()) { | ||
| sendTestRecords(producer); | ||
|
|
||
| var topicPartition = new TopicPartition(TOPIC_NAME, 0); | ||
| var leaderBrokerId = clusterInstance.getLeaderBrokerId(topicPartition); | ||
|
|
There was a problem hiding this comment.
To reduce code duplication across the test methods, consider creating the topic, producer, and admin client in a @BeforeEach setup method and closing the clients in an @AfterEach teardown method. This would remove the need for clusterInstance.createTopic(...) and the try-with-resources block in each test, making the tests cleaner and more maintainable.
For example:
private Admin admin;
private Producer<Object, Object> producer;
@BeforeEach
void setup() throws Exception {
admin = clusterInstance.admin();
producer = clusterInstance.producer(Map.of(
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class));
clusterInstance.createTopic(TOPIC_NAME, NUM_PARTITIONS, REPLICATION_FACTOR);
}
@AfterEach
void tearDown() {
if (producer != null) {
producer.close();
}
if (admin != null) {
admin.close();
}
}| .findFirst() | ||
| .orElseThrow(() -> new IllegalStateException("No follower found for partition " + topicPartition)); | ||
|
|
||
| var followerState = admin.describeProducers(List.of(topicPartition), | ||
| new DescribeProducersOptions().brokerId(followerBrokerId)) |
There was a problem hiding this comment.
This block of code to get replica broker IDs is duplicated in testDescribeProducersWithInvalidBrokerId. To improve maintainability and reduce duplication, consider extracting this logic into a private helper method. Additionally, new TopicPartition(TOPIC_NAME, 0) is used in all tests and could be defined as a static final constant for the class.
For example:
private static final TopicPartition TOPIC_PARTITION_0 = new TopicPartition(TOPIC_NAME, 0);
private List<Integer> getReplicaBrokerIds(Admin admin, TopicPartition topicPartition) throws Exception {
TopicDescription topicDescription = admin.describeTopics(List.of(topicPartition.topic()))
.allTopicNames().get().get(topicPartition.topic());
return topicDescription.partitions().get(topicPartition.partition()).replicas().stream()
.map(Node::id)
.toList();
}Then you could simplify this block to:
var replicaBrokerIds = getReplicaBrokerIds(admin, TOPIC_PARTITION_0);|
/gemini review |
|
/review
" |
|
Persistent review updated to latest commit 956c3aa |
There was a problem hiding this comment.
Code Review
This pull request adds integration tests for DescribeProducersOptions#brokerId. The tests cover the main scenarios: default routing to leader, querying a follower, and querying an invalid broker. The test logic is sound. I've provided a few suggestions to improve code quality by using explicit charsets, removing hardcoded values, and reducing code duplication. These changes will make the new test class more robust and maintainable.
|
|
||
| private static void sendTestRecords(Producer<byte[], byte[]> producer) { | ||
| for (int partition = 0; partition < NUM_PARTITIONS; partition++) { | ||
| producer.send(new ProducerRecord<>(TOPIC_NAME, partition, ("key-" + partition).getBytes(), ("value-" + partition).getBytes())); |
There was a problem hiding this comment.
Using String.getBytes() without specifying a charset can lead to platform-dependent behavior, as it uses the system's default charset. It's a best practice to always specify a charset, like StandardCharsets.UTF_8, to ensure consistent behavior across different environments.
| producer.send(new ProducerRecord<>(TOPIC_NAME, partition, ("key-" + partition).getBytes(), ("value-" + partition).getBytes())); | |
| producer.send(new ProducerRecord<>(TOPIC_NAME, partition, ("key-" + partition).getBytes(java.nio.charset.StandardCharsets.UTF_8), ("value-" + partition).getBytes(java.nio.charset.StandardCharsets.UTF_8))); |
| var topicDescription = admin.describeTopics(List.of(TOPIC_NAME)).allTopicNames().get().get(TOPIC_NAME); | ||
| var replicaBrokerIds = topicDescription.partitions().get(0).replicas().stream() | ||
| .map(Node::id) | ||
| .toList(); |
There was a problem hiding this comment.
This code block uses hardcoded values for the topic name (TOPIC_NAME) and partition index (0). It's better to use the topicPartition field to make the code more robust and readable.
Additionally, this logic is duplicated in testDescribeProducersWithInvalidBrokerId. Consider extracting it into a private helper method to improve maintainability.
| var topicDescription = admin.describeTopics(List.of(TOPIC_NAME)).allTopicNames().get().get(TOPIC_NAME); | |
| var replicaBrokerIds = topicDescription.partitions().get(0).replicas().stream() | |
| .map(Node::id) | |
| .toList(); | |
| var topicDescription = admin.describeTopics(List.of(topicPartition.topic())).allTopicNames().get().get(topicPartition.topic()); | |
| var replicaBrokerIds = topicDescription.partitions().get(topicPartition.partition()).replicas().stream() | |
| .map(Node::id) | |
| .toList(); |
| var topicDescription = admin.describeTopics(List.of(TOPIC_NAME)).allTopicNames().get().get(TOPIC_NAME); | ||
| var replicaBrokerIds = topicDescription.partitions().get(0).replicas().stream() | ||
| .map(Node::id) | ||
| .toList(); |
There was a problem hiding this comment.
This code block uses hardcoded values for the topic name (TOPIC_NAME) and partition index (0). It's better to use the topicPartition field to make the code more robust and readable.
This logic is also duplicated from testDescribeProducersFromFollower. Consider extracting it into a private helper method to improve maintainability.
| var topicDescription = admin.describeTopics(List.of(TOPIC_NAME)).allTopicNames().get().get(TOPIC_NAME); | |
| var replicaBrokerIds = topicDescription.partitions().get(0).replicas().stream() | |
| .map(Node::id) | |
| .toList(); | |
| var topicDescription = admin.describeTopics(List.of(topicPartition.topic())).allTopicNames().get().get(topicPartition.topic()); | |
| var replicaBrokerIds = topicDescription.partitions().get(topicPartition.partition()).replicas().stream() | |
| .map(Node::id) | |
| .toList(); |
|
/describe |
TitleAdd integration tests for DescribeProducersOptions#brokerId PR TypeTests Description
Diagram Walkthroughflowchart LR
A["Test Setup"] --> B["Create Topic with Replicas"]
B --> C["Send Test Records"]
C --> D["Test Default Routing"]
C --> E["Test Follower Query"]
C --> F["Test Invalid Broker ID"]
D --> G["Verify Leader Response"]
E --> H["Verify Follower Response"]
F --> I["Verify Exception Thrown"]
|
| Relevant files | |||
|---|---|---|---|
| Tests |
|
|
./gemini review |
I made some update, hope to continous reivew |
There was a problem hiding this comment.
Code Review
This pull request adds integration tests for the brokerId option in DescribeProducersOptions. The tests cover routing to the leader by default, explicitly routing to a follower, and handling invalid broker IDs. The test logic is sound and covers the main scenarios. I've provided a couple of suggestions to improve code maintainability by reducing duplication and to make the test setup more efficient.
| for (int partition = 0; partition < NUM_PARTITIONS; partition++) { | ||
| producer.send(new ProducerRecord<>(TOPIC_NAME, partition, ("key-" + partition).getBytes(), ("value-" + partition).getBytes())); | ||
| } |
There was a problem hiding this comment.
The tests in this class only use partition 0 (as defined by the topicPartition field). This loop sends records to all partitions, which is unnecessary for the test's purpose. To make the test setup more efficient, you can send a single record to partition 0.
producer.send(new ProducerRecord<>(TOPIC_NAME, 0, "key-0".getBytes(), "value-0".getBytes()));| var topicDescription = admin.describeTopics(List.of(topicPartition.topic())).allTopicNames().get().get(topicPartition.topic()); | ||
| var replicaBrokerIds = topicDescription.partitions().get(topicPartition.partition()).replicas().stream() | ||
| .map(Node::id) | ||
| .toList(); |
There was a problem hiding this comment.
This block of code to retrieve replica broker IDs is duplicated from testDescribeProducersFromFollower (lines 100-103). To improve maintainability and reduce code duplication, consider extracting this logic into a private helper method.
For example:
private List<Integer> getReplicaBrokerIds(Admin admin) throws Exception {
var topicDescription = admin.describeTopics(List.of(topicPartition.topic())).allTopicNames().get().get(topicPartition.topic());
return topicDescription.partitions().get(topicPartition.partition()).replicas().stream()
.map(Node::id)
.toList();
}You can then call this method in both testDescribeProducersFromFollower and testDescribeProducersWithInvalidBrokerId.
| private List<Integer> getReplicaBrokerIds(Admin admin, TopicPartition topicPartition) throws Exception { | ||
| var topicDescription = admin.describeTopics(List.of(topicPartition.topic())).allTopicNames().get().get(topicPartition.topic()); | ||
| return topicDescription.partitions().get(topicPartition.partition()).replicas().stream() | ||
| private List<Integer> getReplicaBrokerIds(Admin admin) throws Exception { |
| producer.flush(); | ||
| } | ||
|
|
||
| @BeforeEach |
| @ClusterTestDefaults( | ||
| brokers = 3 | ||
| ) | ||
| class DescribeProducersWithBrokerIdTest { |
No description provided.