This is an automated email from the ASF dual-hosted git repository.
mpochatkin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 5168177d12b IGNITE-26364 Allow starting node with string configuration
(#6963)
5168177d12b is described below
commit 5168177d12b251538d7cc0766c6486a8c4aad397
Author: Vadim Pakhnushev <[email protected]>
AuthorDate: Tue Nov 25 13:25:26 2025 +0300
IGNITE-26364 Allow starting node with string configuration (#6963)
---
.../internal/runner/app/ItIgniteServerTest.java | 155 ++++++++++++---------
.../main/java/org/apache/ignite/IgniteServer.java | 57 +++++++-
.../ignite/internal/app/IgniteServerImpl.java | 48 ++++++-
3 files changed, 189 insertions(+), 71 deletions(-)
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteServerTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteServerTest.java
index 52ad2387602..f30df3542d6 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteServerTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/ItIgniteServerTest.java
@@ -17,9 +17,12 @@
package org.apache.ignite.internal.runner.app;
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
import static
org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause;
import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrow;
import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.apache.ignite.internal.util.Constants.MiB;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notNullValue;
@@ -36,10 +39,16 @@ import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import org.apache.ignite.IgniteServer;
import org.apache.ignite.InitParameters;
+import org.apache.ignite.configuration.ConfigurationChangeException;
+import org.apache.ignite.internal.app.IgniteImpl;
+import org.apache.ignite.internal.configuration.NodeChange;
+import org.apache.ignite.internal.configuration.NodeConfiguration;
+import org.apache.ignite.internal.rest.configuration.RestExtensionChange;
import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
import org.apache.ignite.internal.testframework.WorkDirectory;
import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.wrapper.Wrappers;
import org.apache.ignite.lang.ClusterNotInitializedException;
import org.apache.ignite.lang.NodeStartException;
import org.junit.jupiter.api.AfterEach;
@@ -53,10 +62,19 @@ import org.junit.jupiter.params.provider.EnumSource;
/**
* IgniteServer interface tests.
*/
+@SuppressWarnings("ThrowableNotThrown")
@ExtendWith(WorkDirectoryExtension.class)
class ItIgniteServerTest extends BaseIgniteAbstractTest {
- /** Network ports of the test nodes. */
- private static final int[] PORTS = {3344, 3345, 3346};
+ private static final String NODE_CONFIGURATION_TEMPLATE = "ignite {\n"
+ + " network: {\n"
+ + " port: {},\n"
+ + " nodeFinder.netClusterNodes: [ \"localhost:3344\",
\"localhost:3345\", \"localhost:3346\" ]\n"
+ + " },\n"
+ + " clientConnector.port: {},\n"
+ + " rest.port: {},\n"
+ + " failureHandler.dumpThreadsOnFailure: false,\n"
+ + " storage.profiles.default {engine: aipersist, sizeBytes: " +
256 * MiB + "}\n"
+ + "}";
/** Nodes bootstrap configuration. */
private final Map<String, String> nodesBootstrapCfg = new
LinkedHashMap<>();
@@ -72,54 +90,13 @@ class ItIgniteServerTest extends BaseIgniteAbstractTest {
*/
@BeforeEach
void setUp(TestInfo testInfo) {
- String node0Name = testNodeName(testInfo, PORTS[0]);
- String node1Name = testNodeName(testInfo, PORTS[1]);
- String node2Name = testNodeName(testInfo, PORTS[2]);
-
- nodesBootstrapCfg.put(
- node0Name,
- "ignite {\n"
- + " network: {\n"
- + " port: " + PORTS[0] + ",\n"
- + " nodeFinder: {\n"
- + " netClusterNodes: [ \"localhost:3344\",
\"localhost:3345\", \"localhost:3346\" ]\n"
- + " }\n"
- + " },\n"
- + " clientConnector.port: 10800,\n"
- + " rest.port: 10300,\n"
- + " failureHandler.dumpThreadsOnFailure: false\n"
- + "}"
- );
-
- nodesBootstrapCfg.put(
- node1Name,
- "ignite {\n"
- + " network: {\n"
- + " port: " + PORTS[1] + ",\n"
- + " nodeFinder: {\n"
- + " netClusterNodes: [ \"localhost:3344\",
\"localhost:3345\", \"localhost:3346\" ]\n"
- + " }\n"
- + " },\n"
- + " clientConnector.port: 10801,\n"
- + " rest.port: 10301,\n"
- + " failureHandler.dumpThreadsOnFailure: false\n"
- + "}"
- );
-
- nodesBootstrapCfg.put(
- node2Name,
- "ignite {\n"
- + " network: {\n"
- + " port: " + PORTS[2] + ",\n"
- + " nodeFinder: {\n"
- + " netClusterNodes: [ \"localhost:3344\",
\"localhost:3345\", \"localhost:3346\" ]\n"
- + " }\n"
- + " },\n"
- + " clientConnector.port: 10802,\n"
- + " rest.port: 10302,\n"
- + " failureHandler.dumpThreadsOnFailure: false\n"
- + "}"
- );
+ for (int i = 0; i < 3; i++) {
+ int port = 3344 + i;
+ nodesBootstrapCfg.put(
+ testNodeName(testInfo, port),
+ format(NODE_CONFIGURATION_TEMPLATE, port, 10800 + i, 10300
+ i)
+ );
+ }
}
/**
@@ -226,55 +203,107 @@ class ItIgniteServerTest extends BaseIgniteAbstractTest {
assertThat(igniteServer.initClusterAsync(initParameters),
willCompleteSuccessfully());
}
+ @Test
+ void startWithConfigStringIsReadOnly() {
+ // Start a single node
+ Map.Entry<String, String> firstNode =
nodesBootstrapCfg.entrySet().stream().findFirst().orElseThrow();
+ startAndRegisterNode(firstNode.getKey(), name -> startNode(name,
firstNode.getValue(), StartKind.START_STRING_CONFIG));
+
+ // Initialize cluster
+ IgniteServer igniteServer = startedIgniteServers.get(0);
+ InitParameters initParameters =
InitParameters.builder().clusterName("cluster").build();
+ assertThat(igniteServer.initClusterAsync(initParameters),
willCompleteSuccessfully());
+
+ IgniteImpl igniteImpl = Wrappers.unwrap(igniteServer.api(),
IgniteImpl.class);
+
+ // Try to change node configuration
+ CompletableFuture<Void> changeFuture =
igniteImpl.nodeConfiguration().change(superRootChange -> {
+ NodeChange nodeChange =
superRootChange.changeRoot(NodeConfiguration.KEY);
+ ((RestExtensionChange) nodeChange).changeRest().changePort(10400);
+ });
+
+ assertThat(changeFuture,
willThrow(ConfigurationChangeException.class));
+ }
+
private void startAndRegisterNode(String nodeName, Function<String,
IgniteServer> starter) {
startedIgniteServers.add(starter.apply(nodeName));
}
private IgniteServer startNode(String name, String config) {
- return startNode(name, config, IgniteServer::start);
+ return startNode(name, config, StartKind.START_FILE_CONFIG);
}
private IgniteServer startNode(String name, String config, Starter
starter) {
- Path nodeWorkDir = workDir.resolve(name);
+ return starter.start(name, config, workDir.resolve(name));
+ }
+
+ private static Path writeConfig(Path nodeWorkDir, String config) {
Path configPath = nodeWorkDir.resolve("ignite-config.conf");
try {
Files.createDirectories(nodeWorkDir);
- Files.writeString(configPath, config);
+ return Files.writeString(configPath, config);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
- return starter.start(name, configPath, nodeWorkDir);
}
@FunctionalInterface
private interface Starter {
- IgniteServer start(String name, Path config, Path workDir);
+ IgniteServer start(String name, String config, Path workDir);
}
enum StartKind implements Starter {
- START {
+ START_FILE_CONFIG {
+ @Override
+ public IgniteServer start(String name, String config, Path
workDir) {
+ return IgniteServer.start(name, writeConfig(workDir, config),
workDir);
+ }
+ },
+ START_ASYNC_JOIN_FILE_CONFIG {
+ @Override
+ public IgniteServer start(String name, String config, Path
workDir) {
+ return interruptibleJoin(IgniteServer.startAsync(name,
writeConfig(workDir, config), workDir));
+ }
+ },
+ BUILD_START_FILE_CONFIG {
+ @Override
+ public IgniteServer start(String name, String config, Path
workDir) {
+ IgniteServer server = IgniteServer.builder(name,
writeConfig(workDir, config), workDir).build();
+ server.start();
+ return server;
+ }
+ },
+ BUILD_START_ASYNC_JOIN_FILE_CONFIG {
+ @Override
+ public IgniteServer start(String name, String config, Path
workDir) {
+ IgniteServer server = IgniteServer.builder(name,
writeConfig(workDir, config), workDir).build();
+ interruptibleJoin(server.startAsync());
+ return server;
+ }
+ },
+ START_STRING_CONFIG {
@Override
- public IgniteServer start(String name, Path config, Path workDir) {
+ public IgniteServer start(String name, String config, Path
workDir) {
return IgniteServer.start(name, config, workDir);
}
},
- START_ASYNC_JOIN {
+ START_ASYNC_JOIN_STRING_CONFIG {
@Override
- public IgniteServer start(String name, Path config, Path workDir) {
+ public IgniteServer start(String name, String config, Path
workDir) {
return interruptibleJoin(IgniteServer.startAsync(name, config,
workDir));
}
},
- BUILD_START {
+ BUILD_START_STRING_CONFIG {
@Override
- public IgniteServer start(String name, Path config, Path workDir) {
+ public IgniteServer start(String name, String config, Path
workDir) {
IgniteServer server = IgniteServer.builder(name, config,
workDir).build();
server.start();
return server;
}
},
- BUILD_START_ASYNC_JOIN {
+ BUILD_START_ASYNC_JOIN_STRING_CONFIG {
@Override
- public IgniteServer start(String name, Path config, Path workDir) {
+ public IgniteServer start(String name, String config, Path
workDir) {
IgniteServer server = IgniteServer.builder(name, config,
workDir).build();
interruptibleJoin(server.startAsync());
return server;
diff --git a/modules/runner/src/main/java/org/apache/ignite/IgniteServer.java
b/modules/runner/src/main/java/org/apache/ignite/IgniteServer.java
index be976a1c9d6..1b281a16f03 100644
--- a/modules/runner/src/main/java/org/apache/ignite/IgniteServer.java
+++ b/modules/runner/src/main/java/org/apache/ignite/IgniteServer.java
@@ -49,6 +49,24 @@ public interface IgniteServer {
return server.startAsync().thenApply(unused -> server);
}
+ /**
+ * Starts an embedded Ignite node with a configuration from a HOCON string.
+ *
+ * <p>When the future returned from this method completes, the node is
partially started, and is ready to accept the init command (that
+ * is, its REST endpoint is
+ * functional).
+ *
+ * @param nodeName Name of the node. Must not be {@code null}.
+ * @param configString Node configuration in the HOCON format. Must not be
{@code null}.
+ * @param workDir Work directory for the node. Must not be {@code null}.
+ * @return Future that will be completed when the node is partially
started, and is ready to accept the init command (that is, its REST
+ * endpoint is functional).
+ */
+ static CompletableFuture<IgniteServer> startAsync(String nodeName, String
configString, Path workDir) {
+ IgniteServer server = builder(nodeName, configString, workDir).build();
+ return server.startAsync().thenApply(unused -> server);
+ }
+
/**
* Starts the node.
*
@@ -78,6 +96,23 @@ public interface IgniteServer {
return server;
}
+ /**
+ * Starts an embedded Ignite node with a configuration from a HOCON string
synchronously.
+ *
+ * <p>When this method returns, the node is partially started, and is
ready to accept the init command (that is, its REST endpoint is
+ * functional).
+ *
+ * @param nodeName Name of the node. Must not be {@code null}.
+ * @param configString Node configuration in the HOCON format. Must not be
{@code null}.
+ * @param workDir Work directory for the node. Must not be {@code null}.
+ * @return Node instance.
+ */
+ static IgniteServer start(String nodeName, String configString, Path
workDir) {
+ IgniteServer server = builder(nodeName, configString, workDir).build();
+ server.start();
+ return server;
+ }
+
/**
* Starts the node.
*
@@ -100,6 +135,18 @@ public interface IgniteServer {
return new Builder(nodeName, configPath, workDir);
}
+ /**
+ * Returns a builder for an embedded Ignite node.
+ *
+ * @param nodeName Name of the node. Must not be {@code null}.
+ * @param configString Node configuration in the HOCON format. Must not be
{@code null}.
+ * @param workDir Work directory for the node. Must not be {@code null}.
+ * @return Node instance.
+ */
+ static Builder builder(String nodeName, String configString, Path workDir)
{
+ return new Builder(nodeName, configString, workDir);
+ }
+
/**
* Returns the Ignite API. The API is available when the node has joined
an initialized cluster. This method throws a
* {@link org.apache.ignite.lang.ClusterNotInitializedException} if the
cluster is not yet initialized.
@@ -186,7 +233,8 @@ public interface IgniteServer {
*/
final class Builder {
private final String nodeName;
- private final Path configPath;
+ private Path configPath;
+ private String configString;
private final Path workDir;
private @Nullable ClassLoader serviceLoaderClassLoader;
@@ -198,6 +246,12 @@ public interface IgniteServer {
this.workDir = workDir;
}
+ private Builder(String nodeName, String configString, Path workDir) {
+ this.nodeName = nodeName;
+ this.configString = configString;
+ this.workDir = workDir;
+ }
+
/**
* Specifies class loader to use when loading components via {@link
java.util.ServiceLoader}.
*
@@ -230,6 +284,7 @@ public interface IgniteServer {
return new IgniteServerImpl(
nodeName,
configPath,
+ configString,
workDir,
serviceLoaderClassLoader,
asyncContinuationExecutor
diff --git
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteServerImpl.java
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteServerImpl.java
index 2e85d361750..bbb2262732f 100644
---
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteServerImpl.java
+++
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteServerImpl.java
@@ -30,6 +30,7 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@@ -78,6 +79,8 @@ public class IgniteServerImpl implements IgniteServer {
" ## /____/\n"
};
+ private static final Random RANDOM = new Random();
+
static {
ErrorGroups.initialize();
IgniteEventType.initialize();
@@ -135,7 +138,10 @@ public class IgniteServerImpl implements IgniteServer {
* Constructs an embedded node.
*
* @param nodeName Name of the node. Must not be {@code null}.
- * @param configPath Path to the node configuration in the HOCON format.
Must not be {@code null}. Must exist
+ * @param configPath Path to the node configuration in the HOCON format.
Must not be {@code null} if the {@code configString} is
+ * {@code null}. Must exist if not {@code null}.
+ * @param configString Node configuration in the HOCON format. Must not be
{@code null} if the {@code configPath} is
+ * {@code null}.
* @param workDir Work directory for the started node. Must not be {@code
null}.
* @param classLoader The class loader to be used to load
provider-configuration files and provider classes, or {@code null} if
* the system class loader (or, failing that, the bootstrap class
loader) is to be used
@@ -143,7 +149,8 @@ public class IgniteServerImpl implements IgniteServer {
*/
public IgniteServerImpl(
String nodeName,
- Path configPath,
+ @Nullable Path configPath,
+ @Nullable String configString,
Path workDir,
@Nullable ClassLoader classLoader,
Executor asyncContinuationExecutor
@@ -154,10 +161,10 @@ public class IgniteServerImpl implements IgniteServer {
if (nodeName.isEmpty()) {
throw new NodeStartException("Node name must not be empty.");
}
- if (configPath == null) {
- throw new NodeStartException("Config path must not be null");
+ if (configPath == null && configString == null) {
+ throw new NodeStartException("Either config path or config string
must not be null");
}
- if (Files.notExists(configPath)) {
+ if (configPath != null && Files.notExists(configPath)) {
throw new NodeStartException("Config file doesn't exist");
}
if (workDir == null) {
@@ -168,7 +175,7 @@ public class IgniteServerImpl implements IgniteServer {
}
this.nodeName = nodeName;
- this.configPath = configPath;
+ this.configPath = configPath != null ? configPath :
createConfigFile(configString, workDir);
this.workDir = workDir;
this.classLoader = classLoader;
this.asyncContinuationExecutor = asyncContinuationExecutor;
@@ -177,6 +184,32 @@ public class IgniteServerImpl implements IgniteServer {
publicIgnite = new RestartProofIgnite(attachmentLock);
}
+ private static Path createConfigFile(String config, Path workDir) {
+ try {
+ Files.createDirectories(workDir);
+ Path configFile = Files.writeString(getConfigFile(workDir),
config);
+ if (!configFile.toFile().setReadOnly()) {
+ throw new NodeStartException("Cannot set read-only flag on
node configuration file");
+ }
+ return configFile;
+ } catch (IOException e) {
+ throw new NodeStartException("Cannot write node configuration
file", e);
+ }
+ }
+
+ private static Path getConfigFile(Path workDir) {
+ // First try the well-known name
+ Path path = workDir.resolve("ignite-config.conf");
+ if (Files.notExists(path)) {
+ return path;
+ }
+
+ while (Files.exists(path)) {
+ path = workDir.resolve("ignite-config." + RANDOM.nextInt() +
".conf");
+ }
+ return path;
+ }
+
@Override
public Ignite api() {
IgniteImpl instance = ignite;
@@ -209,7 +242,8 @@ public class IgniteServerImpl implements IgniteServer {
throw new NodeNotStartedException();
}
try {
- return instance.initClusterAsync(parameters.metaStorageNodeNames(),
+ return instance.initClusterAsync(
+ parameters.metaStorageNodeNames(),
parameters.cmgNodeNames(),
parameters.clusterName(),
parameters.clusterConfiguration()