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()

Reply via email to