Skip to content

Commit 77cc1f0

Browse files
committed
Addressing feedback
Signed-off-by: Kyle Liberti <[email protected]>
1 parent 07a6129 commit 77cc1f0

4 files changed

Lines changed: 134 additions & 124 deletions

File tree

cruise-control-metrics-reporter/src/main/java/com/linkedin/kafka/cruisecontrol/metricsreporter/CruiseControlMetricsReporter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,8 @@ private void setIfAbsent(Properties props, String key, String value) {
566566
* exceptions related to reflection (e.g., {@link NoSuchMethodException}), invocation issues,
567567
* execution exceptions, timeouts, and interruptions.
568568
*/
569-
/* test */ static TopicDescription getTopicDescription(AdminClient adminClient, String ccMetricsTopic) throws KafkaTopicDescriptionException {
569+
/* test */ public static TopicDescription getTopicDescription(AdminClient adminClient, String ccMetricsTopic)
570+
throws KafkaTopicDescriptionException {
570571
try {
571572
// For compatibility with Kafka 4.0 and beyond we must use new API methods.
572573
Method topicDescriptionMethod = topicNameValuesMethod();

cruise-control-metrics-reporter/src/test/java/com/linkedin/kafka/cruisecontrol/metricsreporter/CruiseControlMetricsReporterTest.java

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package com.linkedin.kafka.cruisecontrol.metricsreporter;
66

7-
import com.linkedin.kafka.cruisecontrol.metricsreporter.exception.KafkaTopicDescriptionException;
87
import com.linkedin.kafka.cruisecontrol.metricsreporter.metric.CruiseControlMetric;
98
import com.linkedin.kafka.cruisecontrol.metricsreporter.metric.MetricSerde;
109
import com.linkedin.kafka.cruisecontrol.metricsreporter.utils.CCContainerizedKraftCluster;
@@ -20,11 +19,8 @@
2019
import java.util.Set;
2120
import java.util.concurrent.TimeoutException;
2221
import java.util.concurrent.atomic.AtomicInteger;
23-
import java.util.function.Predicate;
2422
import java.util.regex.Pattern;
2523
import org.apache.kafka.clients.CommonClientConfigs;
26-
import org.apache.kafka.clients.admin.Admin;
27-
import org.apache.kafka.clients.admin.AdminClient;
2824
import org.apache.kafka.clients.admin.TopicDescription;
2925
import org.apache.kafka.clients.consumer.Consumer;
3026
import org.apache.kafka.clients.consumer.ConsumerConfig;
@@ -36,7 +32,6 @@
3632
import org.apache.kafka.clients.producer.ProducerConfig;
3733
import org.apache.kafka.clients.producer.ProducerRecord;
3834
import org.apache.kafka.clients.producer.RecordMetadata;
39-
import org.apache.kafka.common.errors.UnknownTopicOrPartitionException;
4035
import org.apache.kafka.common.serialization.StringDeserializer;
4136
import org.junit.After;
4237
import org.junit.Before;
@@ -45,7 +40,6 @@
4540

4641
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporter.DEFAULT_BOOTSTRAP_SERVERS_HOST;
4742
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporter.DEFAULT_BOOTSTRAP_SERVERS_PORT;
48-
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporter.getTopicDescription;
4943
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_REPORTER_INTERVAL_MS_CONFIG;
5044
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_AUTO_CREATE_CONFIG;
5145
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_CONFIG;
@@ -206,41 +200,9 @@ public void testReportingMetrics() {
206200
assertEquals("Expected " + expectedMetricTypes + ", but saw " + metricTypes, expectedMetricTypes, metricTypes);
207201
}
208202

209-
private TopicDescription waitForTopicMetadata(Admin adminClient,
210-
Duration timeout,
211-
Predicate<TopicDescription> condition)
212-
throws InterruptedException, TimeoutException {
213-
214-
long deadline = System.currentTimeMillis() + timeout.toMillis();
215-
216-
while (System.currentTimeMillis() < deadline) {
217-
try {
218-
TopicDescription topicDescription = getTopicDescription((AdminClient) adminClient, TOPIC);
219-
220-
if (condition.test(topicDescription)) {
221-
return topicDescription;
222-
}
223-
} catch (KafkaTopicDescriptionException e) {
224-
if (!(e.getCause() instanceof UnknownTopicOrPartitionException)) {
225-
throw new RuntimeException("Failed to describe topic: " + TOPIC, e);
226-
}
227-
// else ignore and retry
228-
}
229-
230-
Thread.sleep(500);
231-
}
232-
233-
throw new TimeoutException("Timeout waiting for topic metadata condition to be met: " + TOPIC);
234-
}
235-
236203
@Test
237204
public void testUpdatingMetricsTopicConfig() throws InterruptedException, TimeoutException {
238-
Properties props = new Properties();
239-
setSecurityConfigs(props, "admin");
240-
props.setProperty(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers());
241-
Admin adminClient = Admin.create(props);
242-
243-
TopicDescription topicDescription = waitForTopicMetadata(adminClient, Duration.ofSeconds(30), td -> true);
205+
TopicDescription topicDescription = _cluster.waitForTopicMetadata(TOPIC, Duration.ofSeconds(30), td -> true);
244206
assertEquals(1, topicDescription.partitions().size());
245207

246208
KafkaContainer broker = _cluster.getBrokers().get(0);
@@ -261,7 +223,7 @@ public void testUpdatingMetricsTopicConfig() throws InterruptedException, Timeou
261223

262224
// Wait for topic metadata configuration change to propagate
263225
int oldPartitionCount = topicDescription.partitions().size();
264-
TopicDescription newTopicDescription = waitForTopicMetadata(adminClient, Duration.ofSeconds(30),
226+
TopicDescription newTopicDescription = _cluster.waitForTopicMetadata(TOPIC, Duration.ofSeconds(30),
265227
td -> td.partitions().size() != oldPartitionCount);
266228

267229
assertEquals(2, newTopicDescription.partitions().size());

cruise-control-metrics-reporter/src/test/java/com/linkedin/kafka/cruisecontrol/metricsreporter/utils/CCContainerizedKraftCluster.java

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
import com.github.dockerjava.api.command.InspectContainerResponse;
88
import com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporter;
99
import com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig;
10+
import com.linkedin.kafka.cruisecontrol.metricsreporter.exception.KafkaTopicDescriptionException;
1011
import org.apache.kafka.clients.CommonClientConfigs;
1112
import org.apache.kafka.clients.admin.Admin;
12-
import org.apache.kafka.clients.admin.DescribeClusterResult;
13+
import org.apache.kafka.clients.admin.AdminClient;
14+
import org.apache.kafka.clients.admin.TopicDescription;
1315
import org.apache.kafka.common.Uuid;
1416
import org.apache.kafka.common.config.types.Password;
17+
import org.apache.kafka.common.errors.UnknownTopicOrPartitionException;
1518
import org.testcontainers.containers.GenericContainer;
1619
import org.testcontainers.containers.Network;
1720
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
@@ -33,9 +36,12 @@
3336
import java.util.concurrent.ExecutionException;
3437
import java.util.concurrent.TimeUnit;
3538
import java.util.concurrent.TimeoutException;
39+
import java.util.function.Predicate;
40+
import java.util.function.Supplier;
3641
import java.util.stream.Collectors;
3742
import java.util.stream.IntStream;
3843

44+
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporter.getTopicDescription;
3945
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_REPORTER_INTERVAL_MS_CONFIG;
4046
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_CONFIG;
4147
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.DEFAULT_CRUISE_CONTROL_METRICS_TOPIC;
@@ -79,6 +85,7 @@ public class CCContainerizedKraftCluster implements Startable {
7985
private final List<KafkaContainer> _brokers;
8086
private final String _bootstrapServers;
8187
private final List<String> _brokerAddressList;
88+
private final Admin _adminClient;
8289

8390
public CCContainerizedKraftCluster(int numOfBrokers, List<Map<Object, Object>> brokerConfigs, Properties adminClientProps) {
8491
if (numOfBrokers <= 0) {
@@ -105,6 +112,7 @@ public CCContainerizedKraftCluster(int numOfBrokers, List<Map<Object, Object>> b
105112
Properties adminClientPropsWithBootstrapAddress = new Properties();
106113
adminClientPropsWithBootstrapAddress.putAll(adminClientProps);
107114
adminClientPropsWithBootstrapAddress.setProperty(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, _bootstrapServers);
115+
this._adminClient = Admin.create(adminClientPropsWithBootstrapAddress);
108116

109117
this._brokers =
110118
IntStream
@@ -154,7 +162,7 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) {
154162
//.withLogConsumer(outputFrame -> System.out.print(networkAlias + " | " + outputFrame.getUtf8String()))
155163
// Uncomment the following line when debugging SSL connection problems.
156164
//.withEnv("KAFKA_OPTS", "-Djavax.net.debug=ssl,handshake")
157-
.waitingFor(new BrokerWaitStrategy(brokerNum, metricsTopic, adminClientPropsWithBootstrapAddress)
165+
.waitingFor(new BrokerWaitStrategy(brokerNum, metricsTopic, _adminClient)
158166
.withStartupTimeout(Duration.ofMinutes(1))
159167
);
160168
kafkaContainer.setPortBindings(List.of(containerHostPort + ":" + CONTAINER_EXTERNAL_LISTENER_PORT));
@@ -300,49 +308,133 @@ public void start() {
300308

301309
@Override
302310
public void stop() {
311+
this._adminClient.close();
303312
this._brokers.stream().parallel().forEach(GenericContainer::close);
304313
}
305314

315+
private static <T> T waitUntil(Supplier<T> supplier, Predicate<T> condition, Duration timeout, String timeoutMessage) {
316+
long deadline = System.currentTimeMillis() + timeout.toMillis();
317+
318+
while (System.currentTimeMillis() < deadline) {
319+
T value;
320+
try {
321+
value = supplier.get();
322+
} catch (Exception e) {
323+
throw new RuntimeException(e);
324+
}
325+
326+
if (condition.test(value)) {
327+
return value;
328+
}
329+
330+
try {
331+
Thread.sleep(500);
332+
} catch (InterruptedException e) {
333+
Thread.currentThread().interrupt();
334+
throw new RuntimeException(e);
335+
}
336+
}
337+
338+
throw new RuntimeException(timeoutMessage);
339+
}
340+
341+
/**
342+
* Waits until the metadata for a Kafka topic meets the specified condition,
343+
* or until the given timeout period elapses.
344+
*
345+
* @param topicName the name of the topic whose metadata should be monitored
346+
* @param timeout the maximum duration to wait for the condition to be satisfied
347+
* @param condition a {@link Predicate} that tests the {@link TopicDescription} for readiness
348+
* @return the {@link TopicDescription} once the condition evaluates to {@code true}
349+
*/
350+
public TopicDescription waitForTopicMetadata(String topicName, Duration timeout, Predicate<TopicDescription> condition) {
351+
return waitUntil(
352+
() -> {
353+
try {
354+
return getTopicDescription((AdminClient) _adminClient, topicName);
355+
} catch (KafkaTopicDescriptionException e) {
356+
if (e.getCause() instanceof UnknownTopicOrPartitionException) {
357+
return null;
358+
}
359+
throw new RuntimeException("Failed to describe topic: " + topicName, e);
360+
}
361+
},
362+
td -> td != null && condition.test(td),
363+
timeout,
364+
String.format("Timeout waiting for topic %s metadata to be ready.", topicName)
365+
);
366+
}
367+
368+
/**
369+
* Shuts down the Kafka broker with the given broker ID and waits for it
370+
* to be removed from the cluster metadata.
371+
*
372+
* @param brokerId the ID of the broker to wait for shutdown.
373+
*/
374+
public void shutDownBroker(int brokerId) {
375+
Duration timeout = Duration.ofSeconds(30);
376+
_brokers.get(brokerId).stop();
377+
378+
// Wait until brokerId is no longer in the cluster metadata.
379+
waitUntil(
380+
() -> {
381+
try {
382+
return _adminClient.describeCluster().nodes().get().stream()
383+
.map(node -> String.valueOf(node.id()))
384+
.collect(Collectors.toSet());
385+
} catch (InterruptedException | ExecutionException e) {
386+
throw new RuntimeException(e);
387+
}
388+
},
389+
brokerIds -> !brokerIds.contains(String.valueOf(brokerId)),
390+
timeout,
391+
String.format("Broker %s was not removed from cluster metadata after shutdown.", brokerId)
392+
);
393+
}
394+
306395
public static class BrokerWaitStrategy extends AbstractWaitStrategy {
307396
private final int _brokerId;
308397
private final String _metricsTopic;
309-
private final Properties _adminClientProps;
398+
private final Admin _adminClient;
310399

311-
public BrokerWaitStrategy(int brokerId, String metricsTopic, Properties adminClientProps) {
400+
public BrokerWaitStrategy(int brokerId, String metricsTopic, Admin adminClient) {
312401
this._brokerId = brokerId;
313402
this._metricsTopic = metricsTopic;
314-
this._adminClientProps = adminClientProps;
403+
this._adminClient = adminClient;
315404
}
316405

317406
@Override
318407
protected void waitUntilReady() {
319-
long deadline = System.currentTimeMillis() + startupTimeout.toMillis();
320-
321-
try (Admin adminClient = Admin.create(_adminClientProps)) {
322-
while (System.currentTimeMillis() < deadline) {
408+
waitUntil(
409+
() -> {
410+
// Returns true if broker is online and metrics topic exists
323411
try {
324-
DescribeClusterResult cluster = adminClient.describeCluster();
325-
boolean brokerOnline = cluster.nodes().get().stream().anyMatch(node -> node.id() == _brokerId);
412+
boolean brokerOnline = _adminClient.describeCluster().nodes().get()
413+
.stream()
414+
.anyMatch(node -> node.id() == _brokerId);
326415

327416
if (!brokerOnline) {
328-
Thread.sleep(500);
329-
continue;
417+
return false;
330418
}
331419

332-
adminClient.describeTopics(Collections.singleton(_metricsTopic)).allTopicNames().get(5, TimeUnit.SECONDS);
333-
return;
420+
return _adminClient.describeTopics(Collections.singleton(_metricsTopic))
421+
.allTopicNames()
422+
.get(5, TimeUnit.SECONDS)
423+
.containsKey(_metricsTopic);
334424

335425
} catch (InterruptedException e) {
336426
// Restore interrupt status.
337427
Thread.currentThread().interrupt();
338-
throw new RuntimeException("Interrupted while waiting for broker to become ready", e);
428+
throw new RuntimeException(e);
339429
} catch (ExecutionException | TimeoutException e) {
340430
// Kafka broker is not ready yet, ignore and retry.
431+
return false;
341432
}
342-
}
343-
344-
throw new RuntimeException(String.format("Broker %d did not become ready within timeout of %d ms", _brokerId, startupTimeout.toMillis()));
345-
}
433+
},
434+
ready -> ready,
435+
startupTimeout,
436+
String.format("Broker %d did not become ready within timeout of %d ms", _brokerId, startupTimeout.toMillis())
437+
);
346438
}
347439
}
348440
}

0 commit comments

Comments
 (0)