Skip to content

Commit ec0a9aa

Browse files
kyguyJvD-Ericsson
authored andcommitted
Migrate CruiseControlMetricsReporterTest to TestContainers (linkedin#2303)
* Migrate CruiseControlMetricsReporterTest to TestContainers Signed-off-by: Kyle Liberti <kliberti.us@gmail.com> * Addressing feedback - gp Signed-off-by: Kyle Liberti <kliberti.us@gmail.com> --------- Signed-off-by: Kyle Liberti <kliberti.us@gmail.com>
1 parent cae7962 commit ec0a9aa

6 files changed

Lines changed: 431 additions & 82 deletions

File tree

.circleci/config.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ jobs:
55
build:
66
environment:
77
_JAVA_OPTIONS: "-Xms512m -Xmx1g"
8+
TESTCONTAINERS_RYUK_DISABLED: "true"
89
working_directory: ~/workspace
910
docker:
1011
- image: cimg/openjdk:17.0
1112
steps:
1213
- checkout
14+
- setup_remote_docker:
15+
docker_layer_caching: true
1316
- restore_cache:
1417
key: dependency-cache-{{ checksum "build.gradle" }}
1518
- run:
@@ -19,7 +22,17 @@ jobs:
1922
command: ./gradlew --max-workers=1 --no-daemon analyze
2023
- run:
2124
no_output_timeout: 15m
22-
command: ./gradlew --no-daemon -PmaxParallelForks=1 build
25+
command: |
26+
export NETWORK_NAME="test_containers_network"
27+
28+
# Create a shared Docker network, required for communication between the CircleCI job container
29+
# and TestContainers services.
30+
docker network create $NETWORK_NAME
31+
32+
# Extract Docker host for helping CircleCI find TestContainer services.
33+
export CONTAINER_HOST=$(docker network inspect "$NETWORK_NAME" --format='{{(index .IPAM.Config 0).Gateway}}')
34+
35+
./gradlew --no-daemon -PmaxParallelForks=1 build
2336
- save_cache:
2437
key: dependency-cache-{{ checksum "build.gradle" }}
2538
paths:

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ project(':cruise-control-metrics-reporter') {
484484
testImplementation "org.apache.kafka:kafka-raft:$kafkaVersion"
485485
testImplementation "org.apache.kafka:kafka-storage:$kafkaVersion"
486486
testImplementation 'commons-io:commons-io:2.11.0'
487+
testImplementation "org.testcontainers:kafka:1.21.3"
487488
testOutput sourceSets.test.output
488489
}
489490

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

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,13 @@
77
import java.io.File;
88
import java.io.IOException;
99
import java.util.Properties;
10-
import com.linkedin.kafka.cruisecontrol.metricsreporter.utils.CCKafkaTestUtils;
11-
import org.apache.kafka.clients.CommonClientConfigs;
10+
import javax.net.ssl.KeyManagerFactory;
11+
import com.linkedin.kafka.cruisecontrol.metricsreporter.utils.CCContainerizedKraftCluster;
1212
import org.apache.kafka.clients.producer.ProducerConfig;
13+
import org.apache.kafka.common.config.SslConfigs;
1314
import org.apache.kafka.common.security.auth.SecurityProtocol;
14-
import org.apache.kafka.coordinator.group.GroupCoordinatorConfig;
15-
import org.apache.kafka.network.SocketServerConfigs;
16-
import org.apache.kafka.server.config.ReplicationConfigs;
17-
import org.apache.kafka.server.config.ServerLogConfigs;
1815
import org.junit.Assert;
1916

20-
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_REPORTER_INTERVAL_MS_CONFIG;
21-
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_CONFIG;
22-
23-
2417
public class CruiseControlMetricsReporterSslTest extends CruiseControlMetricsReporterTest {
2518

2619
private File _trustStoreFile;
@@ -37,7 +30,6 @@ public CruiseControlMetricsReporterSslTest() {
3730
@Override
3831
public Properties overridingProps() {
3932
Properties props = new Properties();
40-
int port = CCKafkaTestUtils.findLocalPort();
4133
// We need to convert all the properties to the Cruise Control properties.
4234
setSecurityConfigs(props, "producer");
4335
for (String configName : ProducerConfig.configNames()) {
@@ -47,23 +39,16 @@ public Properties overridingProps() {
4739
props.put(appendPrefix(configName), value);
4840
}
4941
}
50-
props.setProperty(CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, CruiseControlMetricsReporter.class.getName());
51-
props.setProperty(SocketServerConfigs.LISTENERS_CONFIG, "SSL://127.0.0.1:" + port);
52-
props.setProperty(CruiseControlMetricsReporterConfig.config(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG), "127.0.0.1:" + port);
53-
props.setProperty(CruiseControlMetricsReporterConfig.config(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG), SecurityProtocol.SSL.name);
54-
props.setProperty(CRUISE_CONTROL_METRICS_REPORTER_INTERVAL_MS_CONFIG, "100");
55-
props.setProperty(CRUISE_CONTROL_METRICS_TOPIC_CONFIG, TOPIC);
56-
props.setProperty(ServerLogConfigs.LOG_FLUSH_INTERVAL_MESSAGES_CONFIG, "1");
57-
props.setProperty(GroupCoordinatorConfig.OFFSETS_TOPIC_REPLICATION_FACTOR_CONFIG, "1");
58-
props.setProperty(ReplicationConfigs.DEFAULT_REPLICATION_FACTOR_CONFIG, "2");
42+
props.putAll(super.overridingProps());
43+
props.put("listener.security.protocol.map", String.join(",",
44+
CCContainerizedKraftCluster.CONTROLLER_LISTENER_NAME + ":PLAINTEXT",
45+
CCContainerizedKraftCluster.INTERNAL_LISTENER_NAME + ":SSL",
46+
CCContainerizedKraftCluster.EXTERNAL_LISTENER_NAME + ":SSL"));
47+
// The Kafka brokers should use the same key manager algorithm as the host that generates the certs
48+
props.setProperty(SslConfigs.SSL_KEYMANAGER_ALGORITHM_CONFIG, KeyManagerFactory.getDefaultAlgorithm());
5949
return props;
6050
}
6151

62-
@Override
63-
public void testUpdatingMetricsTopicConfig() {
64-
// Skip this test since it is flaky due to undetermined time to propagate metadata.
65-
}
66-
6752
@Override
6853
public File trustStoreFile() {
6954
return _trustStoreFile;

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

Lines changed: 89 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@
77
import com.linkedin.kafka.cruisecontrol.metricsreporter.exception.KafkaTopicDescriptionException;
88
import com.linkedin.kafka.cruisecontrol.metricsreporter.metric.CruiseControlMetric;
99
import com.linkedin.kafka.cruisecontrol.metricsreporter.metric.MetricSerde;
10-
import com.linkedin.kafka.cruisecontrol.metricsreporter.utils.CCEmbeddedBroker;
10+
import com.linkedin.kafka.cruisecontrol.metricsreporter.utils.CCContainerizedKraftCluster;
1111
import com.linkedin.kafka.cruisecontrol.metricsreporter.utils.CCKafkaClientsIntegrationTestHarness;
1212
import java.time.Duration;
1313
import java.util.Arrays;
1414
import java.util.Collections;
1515
import java.util.HashMap;
1616
import java.util.HashSet;
17+
import java.util.List;
1718
import java.util.Map;
1819
import java.util.Properties;
1920
import java.util.Set;
20-
import java.util.concurrent.ExecutionException;
21+
import java.util.concurrent.TimeoutException;
2122
import java.util.concurrent.atomic.AtomicInteger;
23+
import java.util.function.Predicate;
2224
import java.util.regex.Pattern;
23-
import com.linkedin.kafka.cruisecontrol.metricsreporter.utils.CCKafkaTestUtils;
2425
import org.apache.kafka.clients.CommonClientConfigs;
26+
import org.apache.kafka.clients.admin.Admin;
2527
import org.apache.kafka.clients.admin.AdminClient;
2628
import org.apache.kafka.clients.admin.TopicDescription;
2729
import org.apache.kafka.clients.consumer.Consumer;
@@ -34,36 +36,45 @@
3436
import org.apache.kafka.clients.producer.ProducerConfig;
3537
import org.apache.kafka.clients.producer.ProducerRecord;
3638
import org.apache.kafka.clients.producer.RecordMetadata;
39+
import org.apache.kafka.common.errors.UnknownTopicOrPartitionException;
3740
import org.apache.kafka.common.serialization.StringDeserializer;
38-
import org.apache.kafka.coordinator.group.GroupCoordinatorConfig;
39-
import org.apache.kafka.network.SocketServerConfigs;
40-
import org.apache.kafka.server.config.ReplicationConfigs;
41-
import org.apache.kafka.server.config.ServerLogConfigs;
4241
import org.junit.After;
4342
import org.junit.Before;
4443
import org.junit.Test;
44+
import org.testcontainers.kafka.KafkaContainer;
4545

4646
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporter.DEFAULT_BOOTSTRAP_SERVERS_HOST;
4747
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporter.DEFAULT_BOOTSTRAP_SERVERS_PORT;
4848
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporter.getTopicDescription;
49+
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_REPORTER_INTERVAL_MS_CONFIG;
4950
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_AUTO_CREATE_CONFIG;
51+
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_CONFIG;
5052
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_NUM_PARTITIONS_CONFIG;
5153
import static com.linkedin.kafka.cruisecontrol.metricsreporter.CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_REPLICATION_FACTOR_CONFIG;
5254
import static com.linkedin.kafka.cruisecontrol.metricsreporter.metric.RawMetricType.*;
5355
import static org.junit.Assert.assertEquals;
5456
import static org.junit.Assert.assertTrue;
55-
import static org.junit.Assert.fail;
5657

5758
public class CruiseControlMetricsReporterTest extends CCKafkaClientsIntegrationTestHarness {
59+
private static final int NUM_OF_BROKERS = 2;
5860
protected static final String TOPIC = "CruiseControlMetricsReporterTest";
59-
private static final String HOST = "127.0.0.1";
61+
protected static final String HOST = "127.0.0.1";
62+
protected CCContainerizedKraftCluster _cluster;
63+
protected List<Map<Object, Object>> _brokerConfigs;
6064

6165
/**
6266
* Setup the unit test.
6367
*/
6468
@Before
6569
public void setUp() {
66-
super.setUp();
70+
Properties adminClientProps = new Properties();
71+
setSecurityConfigs(adminClientProps, "admin");
72+
73+
_brokerConfigs = buildBrokerConfigs();
74+
_cluster = new CCContainerizedKraftCluster(NUM_OF_BROKERS, _brokerConfigs, adminClientProps);
75+
_cluster.start();
76+
_bootstrapUrl = _cluster.getExternalBootstrapAddress();
77+
6778
Properties props = new Properties();
6879
props.setProperty(ProducerConfig.ACKS_CONFIG, "-1");
6980
AtomicInteger failed = new AtomicInteger(0);
@@ -82,23 +93,31 @@ public void onCompletion(RecordMetadata recordMetadata, Exception e) {
8293
assertEquals(0, failed.get());
8394
}
8495

96+
/**
97+
* Tear down the unit test.
98+
*/
8599
@After
86100
public void tearDown() {
87-
super.tearDown();
101+
if (_cluster != null) {
102+
_cluster.close();
103+
}
88104
}
89105

90106
@Override
91107
public Properties overridingProps() {
92108
Properties props = new Properties();
93-
int port = CCKafkaTestUtils.findLocalPort();
94109
props.setProperty(CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, CruiseControlMetricsReporter.class.getName());
95-
props.setProperty(SocketServerConfigs.LISTENERS_CONFIG, "PLAINTEXT://" + HOST + ":" + port);
96-
props.setProperty(CruiseControlMetricsReporterConfig.config(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG), HOST + ":" + port);
97-
props.setProperty(CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_REPORTER_INTERVAL_MS_CONFIG, "100");
98-
props.setProperty(CruiseControlMetricsReporterConfig.CRUISE_CONTROL_METRICS_TOPIC_CONFIG, TOPIC);
99-
props.setProperty(ServerLogConfigs.LOG_FLUSH_INTERVAL_MESSAGES_CONFIG, "1");
100-
props.setProperty(GroupCoordinatorConfig.OFFSETS_TOPIC_REPLICATION_FACTOR_CONFIG, "1");
101-
props.setProperty(ReplicationConfigs.DEFAULT_REPLICATION_FACTOR_CONFIG, "2");
110+
props.setProperty(CruiseControlMetricsReporterConfig.config(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG),
111+
HOST + ":" + CCContainerizedKraftCluster.CONTAINER_INTERNAL_LISTENER_PORT);
112+
props.put("listener.security.protocol.map", String.join(",",
113+
CCContainerizedKraftCluster.CONTROLLER_LISTENER_NAME + ":PLAINTEXT",
114+
CCContainerizedKraftCluster.INTERNAL_LISTENER_NAME + ":PLAINTEXT",
115+
CCContainerizedKraftCluster.EXTERNAL_LISTENER_NAME + ":PLAINTEXT"));
116+
props.setProperty(CRUISE_CONTROL_METRICS_REPORTER_INTERVAL_MS_CONFIG, "100");
117+
props.setProperty(CRUISE_CONTROL_METRICS_TOPIC_CONFIG, TOPIC);
118+
props.setProperty("log.flush.interval.messages", "1");
119+
props.setProperty("offsets.topic.replication.factor", "1");
120+
props.setProperty("default.replication.factor", "2");
102121
return props;
103122
}
104123

@@ -187,73 +206,89 @@ public void testReportingMetrics() {
187206
assertEquals("Expected " + expectedMetricTypes + ", but saw " + metricTypes, expectedMetricTypes, metricTypes);
188207
}
189208

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+
190236
@Test
191-
public void testUpdatingMetricsTopicConfig() throws ExecutionException, InterruptedException {
237+
public void testUpdatingMetricsTopicConfig() throws InterruptedException, TimeoutException {
192238
Properties props = new Properties();
193239
setSecurityConfigs(props, "admin");
194-
props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers());
195-
AdminClient adminClient = AdminClient.create(props);
240+
props.setProperty(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers());
241+
Admin adminClient = Admin.create(props);
196242

197-
// For compatibility with Kafka 4.0 and beyond we must use new API methods.
198-
TopicDescription topicDescription;
199-
try {
200-
topicDescription = getTopicDescription(adminClient, TOPIC);
201-
} catch (KafkaTopicDescriptionException e) {
202-
throw new RuntimeException(e);
203-
}
243+
TopicDescription topicDescription = waitForTopicMetadata(adminClient, Duration.ofSeconds(30), td -> true);
204244
assertEquals(1, topicDescription.partitions().size());
245+
246+
KafkaContainer broker = _cluster.getBrokers().get(0);
247+
205248
// Shutdown broker
206-
_brokers.get(0).shutdown();
249+
broker.stop();
250+
207251
// Change broker config
208-
Map<Object, Object> brokerConfig = buildBrokerConfigs().get(0);
252+
Map<Object, Object> brokerConfig = _brokerConfigs.get(0);
209253
brokerConfig.put(CRUISE_CONTROL_METRICS_TOPIC_AUTO_CREATE_CONFIG, "true");
210254
brokerConfig.put(CRUISE_CONTROL_METRICS_TOPIC_NUM_PARTITIONS_CONFIG, "2");
211255
brokerConfig.put(CRUISE_CONTROL_METRICS_TOPIC_REPLICATION_FACTOR_CONFIG, "1");
212-
try (CCEmbeddedBroker broker = new CCEmbeddedBroker(brokerConfig)) {
213-
// Restart broker
214-
broker.startup();
215-
// Check whether the topic config is updated
216-
long startTime = System.currentTimeMillis();
217-
boolean isTopicConfigChanged = false;
218-
while (!isTopicConfigChanged) {
219-
if (System.currentTimeMillis() > startTime + 60000) {
220-
fail("Topic config was not updated");
221-
}
222256

223-
TopicDescription description = adminClient.describeTopics(Collections.singleton(TOPIC)).topicNameValues().get(TOPIC).get();
224-
isTopicConfigChanged = 2 == description.partitions().size();
257+
_cluster.overrideBrokerConfig(broker, brokerConfig);
225258

226-
try {
227-
Thread.sleep(5000);
228-
} catch (InterruptedException ignored) {
229-
}
230-
}
231-
}
259+
// Restart broker
260+
broker.start();
261+
262+
// Wait for topic metadata configuration change to propagate
263+
int oldPartitionCount = topicDescription.partitions().size();
264+
TopicDescription newTopicDescription = waitForTopicMetadata(adminClient, Duration.ofSeconds(30),
265+
td -> td.partitions().size() != oldPartitionCount);
266+
267+
assertEquals(2, newTopicDescription.partitions().size());
232268
}
233269

234270
@Test
235271
public void testGetKafkaBootstrapServersConfigure() {
236272
// Test with a "listeners" config with a host
237273
Map<Object, Object> brokerConfig = buildBrokerConfigs().get(0);
238-
Map<String, Object> listenersMap = Collections.singletonMap(
239-
SocketServerConfigs.LISTENERS_CONFIG, brokerConfig.get(SocketServerConfigs.LISTENERS_CONFIG));
274+
Map<String, Object> listenersMap = Collections.singletonMap("listeners", brokerConfig.get("listeners"));
240275
String bootstrapServers = CruiseControlMetricsReporter.getBootstrapServers(listenersMap);
241276
String urlParse = "\\[?([0-9a-zA-Z\\-%._:]*)]?:(-?[0-9]+)";
242277
Pattern urlParsePattern = Pattern.compile(urlParse);
243278
assertTrue(urlParsePattern.matcher(bootstrapServers).matches());
244-
assertEquals(HOST, bootstrapServers.split(":")[0]);
279+
assertEquals("localhost", bootstrapServers.split(":")[0]);
245280

246281
// Test with a "listeners" config without a host in the first listener.
247282
String listeners = "SSL://:1234,PLAINTEXT://myhost:4321";
248-
listenersMap = Collections.singletonMap(SocketServerConfigs.LISTENERS_CONFIG, listeners);
283+
listenersMap = Collections.singletonMap("listeners", listeners);
249284
bootstrapServers = CruiseControlMetricsReporter.getBootstrapServers(listenersMap);
250285
assertTrue(urlParsePattern.matcher(bootstrapServers).matches());
251286
assertEquals(DEFAULT_BOOTSTRAP_SERVERS_HOST, bootstrapServers.split(":")[0]);
252287
assertEquals("1234", bootstrapServers.split(":")[1]);
253288

254289
// Test with "listeners" and "port" config together.
255290
listenersMap = new HashMap<>();
256-
listenersMap.put(SocketServerConfigs.LISTENERS_CONFIG, listeners);
291+
listenersMap.put("listeners", listeners);
257292
listenersMap.put("port", "43");
258293
bootstrapServers = CruiseControlMetricsReporter.getBootstrapServers(listenersMap);
259294
assertTrue(urlParsePattern.matcher(bootstrapServers).matches());

0 commit comments

Comments
 (0)