Skip to content

Commit 1f36889

Browse files
authored
Have ExternalServer dynamically find available ports (#3398)
Developers have run into a fair number of issues with servers having different paths (e.g. if they have two copies of record layer), and thus not being cleaned up by the store. Instead, it now dynamically picks the ports (within a range), so if you have some leftover processes sitting around, new processes will find different ports. This still leaves the code that kills processes around, so that if you keep killing tests, you don't end up with hundreds of servers running.
1 parent 1a2c045 commit 1f36889

File tree

3 files changed

+119
-34
lines changed

3 files changed

+119
-34
lines changed

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/YamlTestExtension.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@
5050
import javax.annotation.Nonnull;
5151
import javax.annotation.Nullable;
5252
import java.io.File;
53+
import java.util.ArrayList;
5354
import java.util.List;
5455
import java.util.Objects;
5556
import java.util.Optional;
56-
import java.util.concurrent.atomic.AtomicInteger;
5757
import java.util.stream.Collectors;
5858
import java.util.stream.Stream;
5959

@@ -97,16 +97,16 @@ public void beforeAll(final ExtensionContext context) throws Exception {
9797
if (Boolean.parseBoolean(System.getProperty("tests.runQuick", "false"))) {
9898
testConfigs = List.of(new EmbeddedConfig(clusterFile));
9999
} else {
100-
AtomicInteger serverPort = new AtomicInteger(1111);
101100
List<File> jars = ExternalServer.getAvailableServers();
102101
// Fail the test if there are no available servers. This would force the execution in "runQuick" mode in case
103102
// we don't have access to the artifacts.
104103
// Potentially, we can relax this a little if all tests are disabled for multi-server execution, but this is
105104
// not a likely scenario.
106105
Assertions.assertFalse(jars.isEmpty(), "There are no external servers available to run");
107-
servers = jars.stream()
108-
.map(jar -> new ExternalServer(jar, serverPort.getAndIncrement(), serverPort.getAndIncrement(), clusterFile))
109-
.collect(Collectors.toList());
106+
servers = new ArrayList<>();
107+
for (File jar : jars) {
108+
servers.add(new ExternalServer(jar, clusterFile));
109+
}
110110
for (ExternalServer server : servers) {
111111
server.start();
112112
}

yaml-tests/src/main/java/com/apple/foundationdb/relational/yamltests/server/ExternalServer.java

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
import org.apache.logging.log4j.Logger;
2626
import org.junit.jupiter.api.Assertions;
2727

28+
import javax.annotation.Nonnull;
2829
import javax.annotation.Nullable;
2930
import java.io.File;
3031
import java.io.IOException;
32+
import java.net.ServerSocket;
3133
import java.util.Arrays;
3234
import java.util.List;
3335
import java.util.Objects;
@@ -44,11 +46,10 @@ public class ExternalServer {
4446
public static final String EXTERNAL_SERVER_PROPERTY_NAME = "yaml_testing_external_server";
4547
private static final boolean SAVE_SERVER_OUTPUT = false;
4648

47-
@Nullable
49+
@Nonnull
4850
private final File serverJar;
49-
private final int grpcPort;
50-
private final int httpPort;
51-
private SemanticVersion version;
51+
private int grpcPort;
52+
private final SemanticVersion version;
5253
private Process serverProcess;
5354
@Nullable
5455
private final String clusterFile;
@@ -58,21 +59,13 @@ public class ExternalServer {
5859
*
5960
* @param serverJar the path to the jar to run
6061
*/
61-
public ExternalServer(@Nullable File serverJar, final int grpcPort, final int httpPort) {
62-
this(serverJar, grpcPort, httpPort, null);
63-
}
62+
public ExternalServer(@Nonnull final File serverJar,
63+
@Nullable final String clusterFile) throws IOException {
64+
this.clusterFile = clusterFile;
6465

65-
/**
66-
* Create a new instance that will run a specific jar.
67-
*
68-
* @param serverJar the path to the jar to run
69-
*/
70-
public ExternalServer(@Nullable File serverJar, final int grpcPort, final int httpPort,
71-
@Nullable final String clusterFile) {
7266
this.serverJar = serverJar;
73-
this.grpcPort = grpcPort;
74-
this.httpPort = httpPort;
75-
this.clusterFile = clusterFile;
67+
Assertions.assertTrue(this.serverJar.exists(), "Jar could not be found " + serverJar.getAbsolutePath());
68+
this.version = getVersion(this.serverJar);
7669
}
7770

7871
static {
@@ -113,20 +106,12 @@ public SemanticVersion getVersion() {
113106
}
114107

115108
public void start() throws Exception {
116-
File jar;
117-
if (serverJar == null) {
118-
final List<File> externalServers = getAvailableServers();
119-
Assertions.assertEquals(1, externalServers.size());
120-
jar = externalServers.get(0);
121-
} else {
122-
jar = serverJar;
123-
}
124-
Assertions.assertTrue(jar.exists(), "Jar could not be found " + jar.getAbsolutePath());
125-
this.version = getVersion(jar);
109+
grpcPort = getAvailablePort(-1);
110+
final int httpPort = getAvailablePort(grpcPort);
126111
ProcessBuilder processBuilder = new ProcessBuilder("java",
127112
// TODO add support for debugging by adding, but need to take care with ports
128113
// "-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n",
129-
"-jar", jar.getAbsolutePath(),
114+
"-jar", serverJar.getAbsolutePath(),
130115
"--grpcPort", Integer.toString(grpcPort), "--httpPort", Integer.toString(httpPort));
131116
ProcessBuilder.Redirect out = SAVE_SERVER_OUTPUT ?
132117
ProcessBuilder.Redirect.to(File.createTempFile("JdbcServerOut-" + grpcPort, ".log")) :
@@ -144,7 +129,7 @@ public void start() throws Exception {
144129
Assertions.fail("Failed to start the external server");
145130
}
146131

147-
logger.info("Started {} Version: {}", jar, version);
132+
logger.info("Started {} Version: {}", serverJar, version);
148133
}
149134

150135
/**
@@ -201,6 +186,32 @@ private boolean startServer(ProcessBuilder processBuilder) throws IOException, I
201186
return serverProcess.isAlive();
202187
}
203188

189+
/**
190+
* Get a port that is currently available for the server.
191+
* @param unavailablePort Get a port that you know will be unavailable. This is mostly useful because the server
192+
* needs two ports, one for GRPC, and one for HTTP, so the GRPC port can be noted as unavailable when asking for
193+
* the http port and both can be provided to the server. If nothing is unavailable, use a negative number.
194+
* @return a port that is not currently in use on the system.
195+
*/
196+
private int getAvailablePort(final int unavailablePort) {
197+
// running locally on my laptop, testing if a port is available takes 0 milliseconds, so no need to optimize
198+
for (int i = 1111; i < 9999; i++) {
199+
if (i != unavailablePort && isAvailable(i)) {
200+
return i;
201+
}
202+
}
203+
return Assertions.fail("Could not find available port between 1111 and 9999");
204+
}
205+
206+
public static boolean isAvailable(int port) {
207+
try (ServerSocket tcpSocket = new ServerSocket(port)) {
208+
tcpSocket.setReuseAddress(true);
209+
return true;
210+
} catch (IOException e) {
211+
return false;
212+
}
213+
}
214+
204215
public void stop() {
205216
if ((serverProcess != null) && serverProcess.isAlive()) {
206217
serverProcess.destroy();
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* ExternalServerTest.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.relational.yamltests.server;
22+
23+
import org.assertj.core.api.Assertions;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
27+
import java.io.File;
28+
import java.io.IOException;
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
import java.util.stream.Collectors;
32+
33+
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
35+
class ExternalServerTest {
36+
37+
private File currentServerPath;
38+
39+
@BeforeEach
40+
void setUp() throws IOException {
41+
this.currentServerPath = getCurrentServerPath();
42+
}
43+
44+
private static File getCurrentServerPath() throws IOException {
45+
final List<File> availableServers = ExternalServer.getAvailableServers();
46+
for (File path : availableServers) {
47+
if (new ExternalServer(path, null).getVersion().equals(SemanticVersion.current())) {
48+
return path;
49+
}
50+
}
51+
return Assertions.fail("No current version server");
52+
}
53+
54+
@Test
55+
void startMultiple() throws Exception {
56+
final List<ExternalServer> servers = new ArrayList<>();
57+
for (int i = 0; i < 3; i++) {
58+
servers.add(new ExternalServer(currentServerPath, null));
59+
}
60+
try {
61+
for (final ExternalServer server : servers) {
62+
server.start();
63+
}
64+
// we can't assert about the actual values, because one of the ports may be busy,
65+
// so assert that each server has its own port, and none of them have the same port
66+
assertEquals(servers.stream().map(ExternalServer::getPort).distinct().collect(Collectors.toList()),
67+
servers.stream().map(ExternalServer::getPort).collect(Collectors.toList()));
68+
} finally {
69+
for (final ExternalServer server : servers) {
70+
server.stop();
71+
}
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)