GH-1254: Configurable Embedded Zookeeper Port

Resolves https://github.com/spring-projects/spring-kafka/issues/1254

- port `EmbeddedKafkaBroker` from scala and add ZK port configuration
- add `zookeeperPort` to `@EmbeddedKafka`
This commit is contained in:
Gary Russell
2019-09-30 13:20:05 -04:00
committed by Artem Bilan
parent 2115cf6224
commit 910233ec4c
7 changed files with 198 additions and 16 deletions

View File

@@ -18,6 +18,9 @@ package org.springframework.kafka.test;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@@ -46,6 +49,9 @@ import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.security.auth.SecurityProtocol;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;
import org.apache.zookeeper.server.NIOServerCnxnFactory;
import org.apache.zookeeper.server.ZooKeeperServer;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
@@ -63,7 +69,7 @@ import kafka.server.NotRunning;
import kafka.utils.CoreUtils;
import kafka.utils.TestUtils;
import kafka.utils.ZKStringSerializer$;
import kafka.zk.EmbeddedZookeeper;
import kafka.zk.ZkFourLetterWords;
/**
* An embedded Kafka Broker(s) and Zookeeper manager.
@@ -114,6 +120,8 @@ public class EmbeddedKafkaBroker implements InitializingBean, DisposableBean {
private String zkConnect;
private int zkPort;
private int[] kafkaPorts;
private Duration adminTimeout = DEFAULT_ADMIN_TIMEOUT;
@@ -190,6 +198,16 @@ public class EmbeddedKafkaBroker implements InitializingBean, DisposableBean {
return this;
}
/**
* Set an explicit port for the embedded Zookeeper.
* @param port the port.
* @return the {@link EmbeddedKafkaBroker}.
* @since 2.3
*/
public EmbeddedKafkaBroker zkPort(int port) {
this.zkPort = port;
return this;
}
/**
* Set the timeout in seconds for admin operations (e.g. topic creation, close).
* Default 30 seconds.
@@ -211,13 +229,36 @@ public class EmbeddedKafkaBroker implements InitializingBean, DisposableBean {
return this;
}
/**
* Get the port that the embedded Zookeeper is running on or will run on.
* @param zkPort the port.
* @since 2.3
*/
public int getZkPort() {
return this.zookeeper != null ? this.zookeeper.getPort() : this.zkPort;
}
/**
* Set the port to run the embedded Zookeeper on (default random).
* @param zkPort the port.
* @since 2.3
*/
public void setZkPort(int zkPort) {
this.zkPort = zkPort;
}
@Override
public void afterPropertiesSet() {
this.zookeeper = new EmbeddedZookeeper();
try {
this.zookeeper = new EmbeddedZookeeper(this.zkPort);
}
catch (IOException | InterruptedException e) {
throw new IllegalStateException("Failed to create embedded Zookeeper", e);
}
int zkConnectionTimeout = 6000; // NOSONAR magic #
int zkSessionTimeout = 6000; // NOSONAR magic #
this.zkConnect = "127.0.0.1:" + this.zookeeper.port();
this.zkConnect = "127.0.0.1:" + this.zookeeper.getPort();
this.zookeeperClient = new ZkClient(this.zkConnect, zkSessionTimeout, zkConnectionTimeout,
ZKStringSerializer$.MODULE$);
this.kafkaServers.clear();
@@ -360,6 +401,7 @@ public class EmbeddedKafkaBroker implements InitializingBean, DisposableBean {
}
try {
this.zookeeper.shutdown();
this.zkConnect = null;
}
catch (Exception e) {
// do nothing
@@ -510,4 +552,86 @@ public class EmbeddedKafkaBroker implements InitializingBean, DisposableBean {
logger.debug("Subscription Initiated");
}
/**
* Ported from scala to allow setting the port.
*
* @author Gary Russell
* @since 2.3
*/
public static final class EmbeddedZookeeper {
private final NIOServerCnxnFactory factory;
private final ZooKeeperServer zookeeper;
private final int port;
private final File snapshotDir;
private final File logDir;
public EmbeddedZookeeper(int zkPort) throws IOException, InterruptedException {
this.snapshotDir = TestUtils.tempDir();
this.logDir = TestUtils.tempDir();
int tickTime = 800; // allow a maxSessionTimeout of 20 * 800ms = 16 secs
System.setProperty("zookeeper.forceSync", "no"); // disable fsync to ZK txn
// log in tests to avoid
// timeout
this.zookeeper = new ZooKeeperServer(this.snapshotDir, this.logDir, tickTime);
this.factory = new NIOServerCnxnFactory();
InetSocketAddress addr = new InetSocketAddress("127.0.0.1", zkPort == 0 ? TestUtils.RandomPort() : zkPort);
this.factory.configure(addr, 0);
this.factory.startup(zookeeper);
this.port = zookeeper.getClientPort();
}
public int getPort() {
return this.port;
}
public File getSnapshotDir() {
return this.snapshotDir;
}
public File getLogDir() {
return this.logDir;
}
public void shutdown() throws IOException {
// Also shuts down ZooKeeperServer
try {
this.factory.shutdown();
}
catch (Exception e) {
logger.error(e, "ZK shutdown failed");
}
int n = 0;
while (n++ < 100) {
try {
ZkFourLetterWords.sendStat("127.0.0.1", port, 3000);
Thread.sleep(100);
}
catch (@SuppressWarnings("unused") Exception e) {
break;
}
}
if (n == 100) {
logger.debug("Zookeeper failed to stop");
}
try {
this.zookeeper.getZKDatabase().close();
}
catch (Exception e) {
logger.error(e, "ZK db close failed");
}
Utils.delete(this.logDir);
Utils.delete(this.snapshotDir);
}
}
}

View File

@@ -115,9 +115,9 @@ public class EmbeddedKafkaCondition implements ExecutionCondition, AfterAllCallb
@SuppressWarnings("unchecked")
private EmbeddedKafkaBroker createBroker(EmbeddedKafka embedded) {
EmbeddedKafkaBroker broker;
broker = new EmbeddedKafkaBroker(embedded.count(),
embedded.controlledShutdown(), embedded.topics());
broker.kafkaPorts(embedded.ports());
broker = new EmbeddedKafkaBroker(embedded.count(), embedded.controlledShutdown(), embedded.topics())
.zkPort(embedded.zookeeperPort())
.kafkaPorts(embedded.ports());
Properties properties = new Properties();
for (String pair : embedded.brokerProperties()) {

View File

@@ -95,6 +95,13 @@ public @interface EmbeddedKafka {
*/
int[] ports() default {0};
/**
* Set the port on which the embedded Zookeeper should listen;
* @return the port.
* @since 2.3
*/
int zookeeperPort() default 0;
/**
* @return partitions per topic
*/

View File

@@ -67,11 +67,11 @@ class EmbeddedKafkaContextCustomizer implements ContextCustomizer {
.toArray(String[]::new);
EmbeddedKafkaBroker embeddedKafkaBroker = new EmbeddedKafkaBroker(this.embeddedKafka.count(),
this.embeddedKafka.controlledShutdown(),
this.embeddedKafka.partitions(),
topics);
embeddedKafkaBroker.kafkaPorts(this.embeddedKafka.ports());
this.embeddedKafka.controlledShutdown(),
this.embeddedKafka.partitions(),
topics)
.kafkaPorts(this.embeddedKafka.ports())
.zkPort(this.embeddedKafka.zookeeperPort());
Properties properties = new Properties();

View File

@@ -96,6 +96,11 @@ public class EmbeddedKafkaRule extends ExternalResource implements TestRule {
return this;
}
public EmbeddedKafkaRule zkPort(int port) {
this.embeddedKafka.setZkPort(port);
return this;
}
/**
* Return an underlying delegator {@link EmbeddedKafkaBroker} instance.
* @return the {@link EmbeddedKafkaBroker} instance.

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.kafka.test;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
/**
* @author Gary Russell
* @since 2.3
*
*/
public class EmbeddedKafkaBrokerTests {
@Test
void testUpDown() {
EmbeddedKafkaBroker kafka = new EmbeddedKafkaBroker(1);
kafka.afterPropertiesSet();
assertThat(kafka.getZookeeperConnectionString()).startsWith("127");
kafka.destroy();
assertThat(kafka.getZookeeperConnectionString()).isNull();
}
}

View File

@@ -61,7 +61,8 @@ public class AddressableEmbeddedBrokerTests {
@Test
public void testKafkaEmbedded() {
assertThat(broker.getBrokersAsString()).isEqualTo("127.0.0.1:" + this.config.port);
assertThat(broker.getBrokersAsString()).isEqualTo("127.0.0.1:" + this.config.kafkaPort);
assertThat(broker.getZkPort()).isEqualTo(this.config.zkPort);
assertThat(broker.getBrokersAsString())
.isEqualTo(System.getProperty(EmbeddedKafkaBroker.SPRING_EMBEDDED_KAFKA_BROKERS));
assertThat(broker.getZookeeperConnectionString())
@@ -69,7 +70,7 @@ public class AddressableEmbeddedBrokerTests {
}
@Test
public void testLateStartedConsumer() throws Exception {
public void testLateStartedConsumer() {
Map<String, Object> consumerProps = KafkaTestUtils.consumerProps(TEST_EMBEDDED, "false", this.broker);
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
Consumer<Integer, String> consumer = new KafkaConsumer<>(consumerProps);
@@ -93,16 +94,22 @@ public class AddressableEmbeddedBrokerTests {
@Configuration
public static class Config {
private int port;
private int kafkaPort;
private int zkPort;
@Bean
public EmbeddedKafkaBroker broker() throws IOException {
ServerSocket ss = ServerSocketFactory.getDefault().createServerSocket(0);
this.port = ss.getLocalPort();
this.kafkaPort = ss.getLocalPort();
ss.close();
ss = ServerSocketFactory.getDefault().createServerSocket(0);
this.zkPort = ss.getLocalPort();
ss.close();
return new EmbeddedKafkaBroker(1, true, TEST_EMBEDDED)
.kafkaPorts(this.port);
.zkPort(this.zkPort)
.kafkaPorts(this.kafkaPort);
}
}