This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 6693442650 [#13044][#13045] improvement(test): identify the server an
integration test starts (#13048)
6693442650 is described below
commit 669344265093a05043dbd7269954cfe0765799aa
Author: Qi Yu <[email protected]>
AuthorDate: Fri Sep 11 17:37:28 2026 +0800
[#13044][#13045] improvement(test): identify the server an integration test
starts (#13048)
### What changes were proposed in this pull request?
Two changes that let an integration suite know which server it is
talking to.
- `BaseIT` refuses to launch the deploy mode server when something
already listens on the configured port, and names the port in the
message. `ITUtils.checkServerPortIsFree` does the check.
- `MiniGravitinoContext` carries the server to start, defaulting to
`GravitinoServer::main`, and `MiniGravitino` starts that one. Existing
callers are unaffected.
### Why are the changes needed?
**Any server on the port is accepted as ours.** `BaseIT.startServer()`
decides the deploy mode server is ready by polling `/metrics` until an
HTTP server answers, and never checks that the process answering is the
one it just launched. A Gravitino left behind by an interrupted run
satisfies the probe, so the suite runs against a stranger with unrelated
configuration.
This is easy to hit, because `gravitino.sh` fails to launch whenever a
port it needs is taken, including the debugger port, and the leftover
holding that port is usually also holding the server port. In my case
the launch failed four times:
```
ERROR: transport error 202: bind failed: Address already in use
ERROR: JDWP Transport dt_socket failed to initialize, TRANSPORT_INIT(510)
```
and the suite then ran against a leftover server that had authorization
enabled, so `createMetalake` came back as:
```
ForbiddenException: Forbidden error :User 'anonymous' is not authorized to
perform
operation 'createMetalake' : Only service admins can create metalakes
```
even though the configuration under test had
`gravitino.authorization.enable = false` and
`gravitino.authorization.serviceAdmins = anonymous`. The failure points
at authorization, and the cause is a stale process; it took several
rounds to find that in `logs/gravitino-server.out`.
**Embedded mode cannot start a downstream server.** `MiniGravitino`
hardcodes `GravitinoServer.main`, so a distribution that ships its own
entry point can only run integration tests in deploy mode. The failure
mode is quiet rather than loud: in embedded mode the components its
server would initialize are simply absent, so the event listeners that
depend on them throw on every event and the framework logs and swallows
it:
```
WARN ...Listener - Failed to handle event RegisterJobTemplateEvent@201b3159
java.lang.NullPointerException: ... because "this.searchService" is null
```
The suite keeps running, nothing is ever written, and assertions that
tolerate an empty result pass. A test can report success while
exercising nothing.
Fix: #13044
Fix: #13045
### Does this PR introduce _any_ user-facing change?
No. Both changes are in the integration test harness.
For test authors: deploy mode now fails fast, with the port named and an
`lsof` invocation to find the holder, instead of running against
whatever answers. `MiniGravitinoContext` gains a constructor that takes
a `ServerLauncher`; the existing constructor keeps starting
`GravitinoServer`.
### How was this patch tested?
- `TestITUtils` covers both sides of the port check: a free port passes,
and a port held by an open `ServerSocket` raises `IllegalStateException`
whose message names the port.
- `TestMiniGravitino` covers that the default context launches
`GravitinoServer` and that a custom launcher is the one invoked.
- `./gradlew :integration-test-common:test -PskipITs`
Both tests were written first and failed to compile against the old API.
Worth a reviewer's opinion: `EventListenerPluginWrapper` logs and
swallows every listener failure, which is what turned the second problem
into a silent one. A listener failing on every event is closer to a
broken deployment than to a recoverable error. I left that alone here,
but it seems worth addressing separately.
---
.../test/catalog/GravitinoCatalogManagerIT.java | 5 +-
.../gravitino/integration/test/MiniGravitino.java | 10 ++--
.../integration/test/MiniGravitinoContext.java | 57 ++++++++++++++++++++++
.../integration/test/TestMiniGravitino.java | 24 +++++++++
.../gravitino/integration/test/util/BaseIT.java | 6 +++
.../gravitino/integration/test/util/ITUtils.java | 33 +++++++++++++
.../integration/test/util/TestITUtils.java | 30 ++++++++++++
7 files changed, 158 insertions(+), 7 deletions(-)
diff --git
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/catalog/GravitinoCatalogManagerIT.java
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/catalog/GravitinoCatalogManagerIT.java
index 1792e926dc..3869e6d004 100644
---
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/catalog/GravitinoCatalogManagerIT.java
+++
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/catalog/GravitinoCatalogManagerIT.java
@@ -64,8 +64,7 @@ public abstract class GravitinoCatalogManagerIT extends
BaseIT {
@BeforeAll
void startUp() throws Exception {
- // Start Gravitino server
- super.startIntegrationTest();
+ // JUnit starts the server through BaseIT before this method.
initGravitinoEnv();
initMetalake();
initFlinkEnv();
@@ -75,7 +74,7 @@ public abstract class GravitinoCatalogManagerIT extends
BaseIT {
@AfterAll
void stop() throws Exception {
stopFlinkEnv();
- super.stopIntegrationTest();
+ // JUnit stops the server through BaseIT after this method.
LOG.info("Stop Flink env successfully.");
}
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitino.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitino.java
index 65fb60a9b5..841785548b 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitino.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitino.java
@@ -198,10 +198,12 @@ public class MiniGravitino {
executor.submit(
() -> {
try {
- GravitinoServer.main(
- new String[] {
- ITUtils.joinPath(mockConfDir.getAbsolutePath(),
"gravitino.conf")
- });
+ context
+ .serverLauncher()
+ .launch(
+ new String[] {
+ ITUtils.joinPath(mockConfDir.getAbsolutePath(),
"gravitino.conf")
+ });
} catch (Exception e) {
LOG.error("Exception in startup MiniGravitino Server ", e);
throw new RuntimeException(e);
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitinoContext.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitinoContext.java
index cf02bb3f3c..e925d14637 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitinoContext.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/MiniGravitinoContext.java
@@ -20,18 +20,75 @@
package org.apache.gravitino.integration.test;
import java.util.Map;
+import org.apache.gravitino.server.GravitinoServer;
public class MiniGravitinoContext {
+
+ /**
+ * Starts a Gravitino server from a configuration file, in the current JVM.
+ *
+ * <p>A distribution that ships its own entry point initializes components
the open source server
+ * knows nothing about. Without a way to name that entry point, its
integration tests can only run
+ * in deploy mode, and in embedded mode those components are quietly absent:
the event listeners
+ * that depend on them fail on every event, the framework logs and swallows
the failures, and
+ * tests that tolerate empty results pass while exercising nothing.
+ */
+ @FunctionalInterface
+ public interface ServerLauncher {
+
+ /**
+ * Starts the server.
+ *
+ * @param args The arguments to start the server with, the configuration
file path.
+ * @throws Exception If the server fails to start.
+ */
+ void launch(String[] args) throws Exception;
+ }
+
+ /** Starts the open source {@link GravitinoServer}, which is what most
callers want. */
+ public static final ServerLauncher DEFAULT_SERVER_LAUNCHER =
GravitinoServer::main;
+
Map<String, String> customConfig;
final boolean ignoreIcebergAuxRestService;
final boolean ignoreLanceAuxRestService;
+ private final ServerLauncher serverLauncher;
public MiniGravitinoContext(
Map<String, String> customConfig,
boolean ignoreIcebergAuxRestService,
boolean ignoreLanceAuxRestService) {
+ this(
+ customConfig,
+ ignoreIcebergAuxRestService,
+ ignoreLanceAuxRestService,
+ DEFAULT_SERVER_LAUNCHER);
+ }
+
+ /**
+ * Creates a context that starts the given server rather than the open
source one.
+ *
+ * @param customConfig The configuration entries to write into the server
configuration.
+ * @param ignoreIcebergAuxRestService Whether to leave the Iceberg REST
service out.
+ * @param ignoreLanceAuxRestService Whether to leave the Lance REST service
out.
+ * @param serverLauncher The server to start.
+ */
+ public MiniGravitinoContext(
+ Map<String, String> customConfig,
+ boolean ignoreIcebergAuxRestService,
+ boolean ignoreLanceAuxRestService,
+ ServerLauncher serverLauncher) {
this.customConfig = customConfig;
this.ignoreIcebergAuxRestService = ignoreIcebergAuxRestService;
this.ignoreLanceAuxRestService = ignoreLanceAuxRestService;
+ this.serverLauncher = serverLauncher;
+ }
+
+ /**
+ * Returns the server this context starts.
+ *
+ * @return the server launcher.
+ */
+ public ServerLauncher serverLauncher() {
+ return serverLauncher;
}
}
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/TestMiniGravitino.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/TestMiniGravitino.java
index e0f75347eb..40b915c78d 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/TestMiniGravitino.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/TestMiniGravitino.java
@@ -30,7 +30,9 @@ import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.gravitino.client.RESTClient;
@@ -41,6 +43,28 @@ class TestMiniGravitino {
@TempDir private Path mockConfDir;
+ @Test
+ void testContextLaunchesTheOpenSourceServerByDefault() {
+ // Existing callers pass no launcher and must keep getting the open source
server.
+ MiniGravitinoContext context = new
MiniGravitinoContext(Collections.emptyMap(), false, false);
+
+ assertSame(MiniGravitinoContext.DEFAULT_SERVER_LAUNCHER,
context.serverLauncher());
+ }
+
+ @Test
+ void testContextCarriesACustomServerLauncher() throws Exception {
+ // A distribution that ships its own entry point initializes components
the open source server
+ // knows nothing about, so embedded mode has to be able to start that
server instead.
+ List<String[]> launched = new ArrayList<>();
+ MiniGravitinoContext context =
+ new MiniGravitinoContext(Collections.emptyMap(), false, false,
launched::add);
+
+ context.serverLauncher().launch(new String[] {"gravitino.conf"});
+
+ assertEquals(1, launched.size());
+ assertEquals("gravitino.conf", launched.get(0)[0]);
+ }
+
@Test
void testStopCleansResourcesWhenServerTaskDoesNotTerminate() throws
Exception {
ExecutorService executor = mock(ExecutorService.class);
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
index a2bae427b2..bdbeaba005 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/BaseIT.java
@@ -412,6 +412,12 @@ public class BaseIT {
setupJdbcDrivers();
+ JettyServerConfig configuredJetty =
+ JettyServerConfig.fromConfig(serverConfig, WEBSERVER_CONF_PREFIX);
+ // The readiness check below only asks whether an HTTP server answers,
so a Gravitino left
+ // behind by an earlier run would pass it and the suite would run
against that process.
+ ITUtils.checkServerPortIsFree(configuredJetty.getHost(),
configuredJetty.getHttpPort());
+
GravitinoITUtils.startGravitinoServer();
JettyServerConfig jettyServerConfig =
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/ITUtils.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/ITUtils.java
index 70862e4de8..67829c3bbb 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/ITUtils.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/ITUtils.java
@@ -24,6 +24,8 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
@@ -50,6 +52,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ITUtils {
+
+ private static final int PORT_CHECK_TIMEOUT_MS = 500;
private static final Logger LOG = LoggerFactory.getLogger(ITUtils.class);
private static final String CI_ENV = "CI";
private static final String GITHUB_ACTIONS_ENV = "GITHUB_ACTIONS";
@@ -258,4 +262,33 @@ public class ITUtils {
}
private ITUtils() {}
+
+ /**
+ * Refuses to start a server on a port something else already holds.
+ *
+ * <p>The readiness probe that follows a deploy mode launch only asks
whether an HTTP server
+ * answers on the port, not whether it is the one just launched. A Gravitino
left behind by an
+ * interrupted run therefore satisfies it, and the suite proceeds against a
stranger with
+ * unrelated configuration, failing later in ways that point away from the
cause. Checking here
+ * turns that into an immediate, accurate error.
+ *
+ * @param host The host the server is configured to bind.
+ * @param port The port the server is configured to bind.
+ * @throws IllegalStateException If something already listens on the port.
+ */
+ public static void checkServerPortIsFree(String host, int port) {
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress(host, port), PORT_CHECK_TIMEOUT_MS);
+ } catch (IOException e) {
+ // Nothing answered, which is what we want.
+ return;
+ }
+
+ throw new IllegalStateException(
+ String.format(
+ "Something already listens on %s:%d, so the server under test
cannot bind it and the "
+ + "readiness probe would accept the existing process as if it
were ours. Stop it "
+ + "first, for instance with `lsof -nP -iTCP:%d -sTCP:LISTEN`.",
+ host, port, port));
+ }
}
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestITUtils.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestITUtils.java
index cb07597989..6744a9109b 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestITUtils.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/TestITUtils.java
@@ -18,6 +18,8 @@
*/
package org.apache.gravitino.integration.test.util;
+import java.io.IOException;
+import java.net.ServerSocket;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -43,4 +45,32 @@ public class TestITUtils {
Assertions.assertFalse(ITUtils.isCiEnvironment(Map.of()));
Assertions.assertFalse(ITUtils.isCiEnvironment(Map.of("CI", "false")));
}
+
+ @Test
+ void testCheckServerPortIsFreeAcceptsAnUnusedPort() throws IOException {
+ int port;
+ try (ServerSocket socket = new ServerSocket(0)) {
+ port = socket.getLocalPort();
+ }
+
+ Assertions.assertDoesNotThrow(() ->
ITUtils.checkServerPortIsFree("localhost", port));
+ }
+
+ @Test
+ void testCheckServerPortIsFreeRejectsAPortSomethingElseHolds() throws
IOException {
+ // A server left behind by an earlier run keeps answering on this port.
Starting a suite against
+ // it runs the tests against a stranger's configuration, so refuse before
the launch rather than
+ // after a readiness probe the leftover satisfies.
+ try (ServerSocket socket = new ServerSocket(0)) {
+ int port = socket.getLocalPort();
+
+ IllegalStateException e =
+ Assertions.assertThrows(
+ IllegalStateException.class, () ->
ITUtils.checkServerPortIsFree("localhost", port));
+
+ Assertions.assertTrue(
+ e.getMessage().contains(String.valueOf(port)),
+ "the message has to name the port so the cause is actionable: " +
e.getMessage());
+ }
+ }
}