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 48e2f0e03fc IGNITE-27063 Wait for node join in CLI (#6994)
48e2f0e03fc is described below

commit 48e2f0e03fc000b19a720f7e559410f89073d5be
Author: Vadim Pakhnushev <[email protected]>
AuthorDate: Fri Nov 21 15:42:29 2025 +0300

    IGNITE-27063 Wait for node join in CLI (#6994)
---
 gradle/libs.versions.toml                          |   2 +-
 .../cluster/init/ItClusterInitOneNodeTest.java     |  66 ++++++++++++
 .../internal/cli/call/cluster/ClusterInitCall.java |  59 +++++++----
 ...allFactory.java => ClusterInitCallFactory.java} |  15 +--
 .../call/cluster/unit/DeployUnitCallFactory.java   |   7 +-
 .../cli/call/cluster/unit/DeployUnitReplCall.java  |   4 +-
 .../commands/cluster/init/ClusterInitCommand.java  |  14 ++-
 .../cluster/init/ClusterInitConstants.java}        |  22 ++--
 .../cluster/init/ClusterInitReplCommand.java       |  33 +++++-
 .../cluster/unit/ClusterUnitDeployCommand.java     |   5 +-
 .../cli/core/call/AsyncCallExecutionPipeline.java  |   8 +-
 .../call/AsyncCallExecutionPipelineBuilder.java    |  31 ++++--
 .../internal/cli/core/call/AsyncCallFactory.java}  |  26 ++---
 .../cli/core/call/CallExecutionPipeline.java       |   6 +-
 .../call/SpinnerRenderer.java}                     |  33 +++---
 .../cli/core/repl/ConnectionHeartBeat.java         |  14 ++-
 .../internal/cli/commands/CliCommandTestBase.java  | 113 ++++++++++++++++++--
 .../internal/cli/commands/ProfileMixinTest.java    | 116 ++++++++++++++++++---
 .../cli/commands/UrlOptionsNegativeTest.java       |  34 +++---
 .../cli/commands/cluster/ClusterInitReplTest.java  |  11 ++
 .../cli/commands/cluster/ClusterInitTest.java      |  16 +--
 .../cli/core/call/SpinnerRendererTest.java}        |  30 +++---
 .../rest/cluster/ClusterManagementController.java  |  31 ++++--
 .../rest/cluster/ClusterManagementRestFactory.java |  13 ++-
 .../internal/rest/cluster/JoinFutureProvider.java} |  24 ++---
 .../ignite/internal/rest/RestComponentTest.java    |   3 -
 .../org/apache/ignite/internal/app/IgniteImpl.java |  23 +++-
 27 files changed, 557 insertions(+), 202 deletions(-)

diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index fcdebdf13ab..87add84c91c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -80,7 +80,7 @@ testkit = "1.14.0"
 openapi = "4.10.0"
 autoService = "1.1.1"
 awaitility = "4.3.0"
-progressBar = "0.10.0"
+progressBar = "0.10.1"
 guava = "33.5.0-jre"
 jna = "5.18.1"
 tree-sitter = "0.25.3"
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/init/ItClusterInitOneNodeTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/init/ItClusterInitOneNodeTest.java
new file mode 100644
index 00000000000..35f70e588cb
--- /dev/null
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/cluster/init/ItClusterInitOneNodeTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *      http://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.apache.ignite.internal.cli.commands.cluster.init;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import 
org.apache.ignite.internal.cli.commands.CliCommandTestNotInitializedIntegrationBase;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests that node is immediately available after the cluster init command.
+ *
+ * <p>Because the {@link org.apache.ignite.internal.cli.CliIntegrationTest} 
extends
+ * {@link org.apache.ignite.internal.ClusterPerClassIntegrationTest}, each CLI 
test case for init has to be placed in a separate
+ * test class. It'd ideal to refactor the base classes to have a CLI init test 
class with multiple test cases.
+ * This may be needed if more tests are added.
+ */
+public class ItClusterInitOneNodeTest extends 
CliCommandTestNotInitializedIntegrationBase {
+    @Override
+    protected int initialNodes() {
+        return 1;
+    }
+
+    @Test
+    void initCluster() {
+        // when
+        connect(NODE_URL);
+
+        execute("cluster", "init", "--name", "cluster");
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertErrOutputIsEmpty,
+                () -> assertOutputContains("Cluster was initialized 
successfully")
+        );
+
+        // then status is immediately available and initialized
+        execute("cluster", "status");
+
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertErrOutputIsEmpty,
+                () -> assertOutputIs(format(
+                        "[name: cluster, nodes: 1, status: active, cmgNodes: 
[{}], msNodes: [{}]]" + System.lineSeparator(),
+                        CLUSTER.nodeName(0),
+                        CLUSTER.nodeName(0)
+                ))
+        );
+    }
+}
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCall.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCall.java
index 5ebf016d51d..690458894c3 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCall.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCall.java
@@ -17,45 +17,64 @@
 
 package org.apache.ignite.internal.cli.call.cluster;
 
-import jakarta.inject.Singleton;
-import org.apache.ignite.internal.cli.core.call.Call;
-import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+import static 
org.apache.ignite.internal.cli.core.call.DefaultCallOutput.failure;
+import static 
org.apache.ignite.internal.cli.core.call.DefaultCallOutput.success;
+
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.internal.cli.core.call.AsyncCall;
+import org.apache.ignite.internal.cli.core.call.CallOutput;
+import org.apache.ignite.internal.cli.core.call.ProgressTracker;
 import org.apache.ignite.internal.cli.core.exception.IgniteCliApiException;
 import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
 import org.apache.ignite.rest.client.api.ClusterManagementApi;
+import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.model.InitCommand;
 
 /**
  * Inits cluster.
  */
-@Singleton
-public class ClusterInitCall implements Call<ClusterInitCallInput, String> {
+public class ClusterInitCall implements AsyncCall<ClusterInitCallInput, 
String> {
+    private final ProgressTracker tracker;
+
     private final ApiClientFactory clientFactory;
 
-    public ClusterInitCall(ApiClientFactory clientFactory) {
+    /**
+     * Custom read timeout for the init operation, default is 10 seconds which 
could be not enough.
+     */
+    private static final int READ_TIMEOUT_MILLIS = 60_000;
+
+    ClusterInitCall(ProgressTracker tracker, ApiClientFactory clientFactory) {
+        this.tracker = tracker;
         this.clientFactory = clientFactory;
     }
 
-    /** {@inheritDoc} */
     @Override
-    public DefaultCallOutput<String> execute(ClusterInitCallInput input) {
+    public CompletableFuture<CallOutput<String>> execute(ClusterInitCallInput 
input) {
         ClusterManagementApi client = createApiClient(input);
 
-        try {
-            client.init(new InitCommand()
-                    .metaStorageNodes(input.getMetaStorageNodes())
-                    .cmgNodes(input.getCmgNodes())
-                    .clusterName(input.getClusterName())
-                    .clusterConfiguration(input.clusterConfiguration())
-            );
-            return DefaultCallOutput.success("Cluster was initialized 
successfully");
-        } catch (ApiException | IllegalArgumentException e) {
-            return DefaultCallOutput.failure(new IgniteCliApiException(e, 
input.getClusterUrl()));
-        }
+        tracker.maxSize(-1);
+        return supplyAsync(() -> {
+            try {
+                client.init(new InitCommand()
+                        .metaStorageNodes(input.getMetaStorageNodes())
+                        .cmgNodes(input.getCmgNodes())
+                        .clusterName(input.getClusterName())
+                        .clusterConfiguration(input.clusterConfiguration())
+                );
+                return success("Cluster was initialized successfully.");
+            } catch (ApiException | IllegalArgumentException e) {
+                return failure(new IgniteCliApiException(e, 
input.getClusterUrl()));
+            } finally {
+                tracker.done();
+            }
+        });
     }
 
     private ClusterManagementApi createApiClient(ClusterInitCallInput input) {
-        return new 
ClusterManagementApi(clientFactory.getClient(input.getClusterUrl()));
+        ApiClient client = clientFactory.getClient(input.getClusterUrl());
+        client.setReadTimeout(READ_TIMEOUT_MILLIS);
+        return new ClusterManagementApi(client);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCallFactory.java
similarity index 66%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCallFactory.java
index 5e208d1b665..d3a09f5317a 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCallFactory.java
@@ -15,23 +15,26 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.call.cluster.unit;
+package org.apache.ignite.internal.cli.call.cluster;
 
 import jakarta.inject.Singleton;
+import org.apache.ignite.internal.cli.core.call.AsyncCall;
+import org.apache.ignite.internal.cli.core.call.AsyncCallFactory;
 import org.apache.ignite.internal.cli.core.call.ProgressTracker;
 import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
 
-/** Factory for {@link DeployUnitCall}. */
+/** Factory for {@link ClusterInitCall}. */
 @Singleton
-public class DeployUnitCallFactory {
+public class ClusterInitCallFactory implements 
AsyncCallFactory<ClusterInitCallInput, String> {
 
     private final ApiClientFactory factory;
 
-    public DeployUnitCallFactory(ApiClientFactory factory) {
+    public ClusterInitCallFactory(ApiClientFactory factory) {
         this.factory = factory;
     }
 
-    public DeployUnitCall create(ProgressTracker tracker) {
-        return new DeployUnitCall(tracker, factory);
+    @Override
+    public AsyncCall<ClusterInitCallInput, String> create(ProgressTracker 
tracker) {
+        return new ClusterInitCall(tracker, factory);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
index 5e208d1b665..d92b62e641d 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
@@ -18,12 +18,14 @@
 package org.apache.ignite.internal.cli.call.cluster.unit;
 
 import jakarta.inject.Singleton;
+import org.apache.ignite.internal.cli.core.call.AsyncCall;
+import org.apache.ignite.internal.cli.core.call.AsyncCallFactory;
 import org.apache.ignite.internal.cli.core.call.ProgressTracker;
 import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
 
 /** Factory for {@link DeployUnitCall}. */
 @Singleton
-public class DeployUnitCallFactory {
+public class DeployUnitCallFactory implements 
AsyncCallFactory<DeployUnitCallInput, String> {
 
     private final ApiClientFactory factory;
 
@@ -31,7 +33,8 @@ public class DeployUnitCallFactory {
         this.factory = factory;
     }
 
-    public DeployUnitCall create(ProgressTracker tracker) {
+    @Override
+    public AsyncCall<DeployUnitCallInput, String> create(ProgressTracker 
tracker) {
         return new DeployUnitCall(tracker, factory);
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitReplCall.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitReplCall.java
index ff3256722c2..dac5ca056f6 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitReplCall.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitReplCall.java
@@ -24,11 +24,11 @@ import 
org.apache.ignite.internal.cli.core.repl.registry.UnitsRegistry;
 
 /** Call to deploy a unit and refresh units registry. */
 public class DeployUnitReplCall implements AsyncCall<DeployUnitCallInput, 
String>  {
-    private final DeployUnitCall deployUnitCall;
+    private final AsyncCall<DeployUnitCallInput, String> deployUnitCall;
 
     private final UnitsRegistry unitsRegistry;
 
-    DeployUnitReplCall(DeployUnitCall deployUnitCall, UnitsRegistry 
unitsRegistry) {
+    DeployUnitReplCall(AsyncCall<DeployUnitCallInput, String> deployUnitCall, 
UnitsRegistry unitsRegistry) {
         this.deployUnitCall = deployUnitCall;
         this.unitsRegistry = unitsRegistry;
     }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitCommand.java
index 63b0faac3c7..52953ab9132 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitCommand.java
@@ -17,13 +17,16 @@
 
 package org.apache.ignite.internal.cli.commands.cluster.init;
 
+import static 
org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitConstants.SPINNER_PREFIX;
+import static 
org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitConstants.SPINNER_UPDATE_INTERVAL_MILLIS;
+import static 
org.apache.ignite.internal.cli.core.call.CallExecutionPipeline.asyncBuilder;
+
 import jakarta.inject.Inject;
 import java.util.concurrent.Callable;
-import org.apache.ignite.internal.cli.call.cluster.ClusterInitCall;
+import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallFactory;
 import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallInput;
 import org.apache.ignite.internal.cli.commands.BaseCommand;
 import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlProfileMixin;
-import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Mixin;
 
@@ -40,13 +43,14 @@ public class ClusterInitCommand extends BaseCommand 
implements Callable<Integer>
     private ClusterUrlProfileMixin clusterUrl;
 
     @Inject
-    private ClusterInitCall call;
+    private ClusterInitCallFactory callFactory;
 
-    /** {@inheritDoc} */
     @Override
     public Integer call() {
-        return runPipeline(CallExecutionPipeline.builder(call)
+        return runPipeline(asyncBuilder(callFactory)
                 .inputProvider(this::buildCallInput)
+                .enableSpinner(SPINNER_PREFIX)
+                .updateIntervalMillis(SPINNER_UPDATE_INTERVAL_MILLIS)
         );
     }
 
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitConstants.java
similarity index 61%
copy from 
modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitConstants.java
index 18f900e6857..651bcf56e66 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitConstants.java
@@ -15,21 +15,13 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.cluster;
+package org.apache.ignite.internal.cli.commands.cluster.init;
 
-import 
org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitReplCommand;
-import org.junit.jupiter.api.DisplayName;
-
-/** Tests "cluster init" command in REPL mode. */
-@DisplayName("cluster init repl")
-public class ClusterInitReplTest extends ClusterInitTest {
-    @Override
-    protected Class<?> getCommandClass() {
-        return ClusterInitReplCommand.class;
-    }
+/**
+ * Constants for cluster init command.
+ */
+class ClusterInitConstants {
+    static final String SPINNER_PREFIX = "Initializing";
 
-    @Override
-    protected int errorExitCode() {
-        return 0;
-    }
+    static final int SPINNER_UPDATE_INTERVAL_MILLIS = 500;
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitReplCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitReplCommand.java
index ee70428f36e..e04f94dddb0 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitReplCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/init/ClusterInitReplCommand.java
@@ -18,17 +18,23 @@
 package org.apache.ignite.internal.cli.commands.cluster.init;
 
 import static 
org.apache.ignite.internal.cli.commands.Options.Constants.CLUSTER_CONFIG_OPTION;
+import static 
org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitConstants.SPINNER_PREFIX;
+import static 
org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitConstants.SPINNER_UPDATE_INTERVAL_MILLIS;
+import static 
org.apache.ignite.internal.cli.core.call.CallExecutionPipeline.asyncBuilder;
 import static 
org.apache.ignite.internal.cli.core.style.component.QuestionUiComponent.fromYesNoQuestion;
 import static picocli.CommandLine.Command;
 
 import jakarta.inject.Inject;
-import org.apache.ignite.internal.cli.call.cluster.ClusterInitCall;
+import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallFactory;
 import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallInput;
 import org.apache.ignite.internal.cli.commands.BaseCommand;
 import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlMixin;
 import 
org.apache.ignite.internal.cli.commands.questions.ConnectToClusterQuestion;
+import org.apache.ignite.internal.cli.core.call.AsyncCall;
+import org.apache.ignite.internal.cli.core.call.ProgressTracker;
 import org.apache.ignite.internal.cli.core.flow.builder.FlowBuilder;
 import org.apache.ignite.internal.cli.core.flow.builder.Flows;
+import org.apache.ignite.internal.cli.core.repl.ConnectionHeartBeat;
 import org.apache.ignite.internal.cli.core.style.component.QuestionUiComponent;
 import picocli.CommandLine.Mixin;
 
@@ -45,18 +51,19 @@ public class ClusterInitReplCommand extends BaseCommand 
implements Runnable {
     private ClusterInitOptions clusterInitOptions;
 
     @Inject
-    private ClusterInitCall call;
+    private ClusterInitCallFactory callFactory;
 
     @Inject
     private ConnectToClusterQuestion question;
 
-    /** {@inheritDoc} */
+    @Inject
+    private ConnectionHeartBeat connectionHeartBeat;
+
     @Override
     public void run() {
         runFlow(question.askQuestionIfNotConnected(clusterUrl.getClusterUrl())
                 .then(askQuestionIfConfigIsPath().build())
-                .then(Flows.fromCall(call))
-                .print()
+                .then(Flows.mono(this::runAsync))
         );
     }
 
@@ -89,4 +96,20 @@ public class ClusterInitReplCommand extends BaseCommand 
implements Runnable {
                 .fromClusterInitOptions(clusterInitOptions)
                 .build();
     }
+
+    private int runAsync(ClusterInitCallInput input) {
+        return runPipeline(
+                asyncBuilder(this::createCall)
+                        .inputProvider(() -> input)
+                        .enableSpinner(SPINNER_PREFIX)
+                        .updateIntervalMillis(SPINNER_UPDATE_INTERVAL_MILLIS)
+        );
+    }
+
+    private AsyncCall<ClusterInitCallInput, String> createCall(ProgressTracker 
tracker) {
+        AsyncCall<ClusterInitCallInput, String> delegate = 
callFactory.create(tracker);
+        return input -> delegate.execute(input)
+                // Refresh connected state immediately after execution because 
node state is unavailable during cluster initialization
+                .whenComplete((output, throwable) -> 
connectionHeartBeat.pingConnection());
+    }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/unit/ClusterUnitDeployCommand.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/unit/ClusterUnitDeployCommand.java
index 3cb47049261..0ae4f30a5e8 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/unit/ClusterUnitDeployCommand.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/commands/cluster/unit/ClusterUnitDeployCommand.java
@@ -17,12 +17,13 @@
 
 package org.apache.ignite.internal.cli.commands.cluster.unit;
 
+import static 
org.apache.ignite.internal.cli.core.call.CallExecutionPipeline.asyncBuilder;
+
 import jakarta.inject.Inject;
 import java.util.concurrent.Callable;
 import org.apache.ignite.internal.cli.call.cluster.unit.DeployUnitCallFactory;
 import org.apache.ignite.internal.cli.commands.BaseCommand;
 import org.apache.ignite.internal.cli.commands.cluster.ClusterUrlProfileMixin;
-import org.apache.ignite.internal.cli.core.call.CallExecutionPipeline;
 import 
org.apache.ignite.internal.cli.core.exception.handler.ClusterNotInitializedExceptionHandler;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Mixin;
@@ -42,7 +43,7 @@ public class ClusterUnitDeployCommand extends BaseCommand 
implements Callable<In
 
     @Override
     public Integer call() throws Exception {
-        return 
runPipeline(CallExecutionPipeline.asyncBuilder(callFactory::create)
+        return runPipeline(asyncBuilder(callFactory)
                 .inputProvider(() -> 
options.toDeployUnitCallInput(clusterUrl.getClusterUrl()))
                 
.exceptionHandler(ClusterNotInitializedExceptionHandler.createHandler("Cannot 
deploy unit"))
         );
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallExecutionPipeline.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallExecutionPipeline.java
index baa31d91b05..104b804d06b 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallExecutionPipeline.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallExecutionPipeline.java
@@ -19,7 +19,6 @@ package org.apache.ignite.internal.cli.core.call;
 
 import java.io.PrintWriter;
 import java.util.concurrent.CompletionException;
-import java.util.function.Function;
 import java.util.function.Supplier;
 import me.tongfei.progressbar.DelegatingProgressBarConsumer;
 import me.tongfei.progressbar.ProgressBarBuilder;
@@ -30,13 +29,13 @@ import 
org.apache.ignite.internal.cli.core.exception.ExceptionHandlers;
 /** Call execution pipeline that executes an async call and displays progress 
bar. */
 public class AsyncCallExecutionPipeline<I extends CallInput, T> extends 
AbstractCallExecutionPipeline<I, T> {
     /** Async call factory. */
-    private final Function<ProgressTracker, AsyncCall<I, T>> callFactory;
+    private final AsyncCallFactory<I, T> callFactory;
 
     /** Builder for progress bar rendering. */
     private final ProgressBarBuilder progressBarBuilder;
 
     AsyncCallExecutionPipeline(
-            Function<ProgressTracker, AsyncCall<I, T>> callFactory,
+            AsyncCallFactory<I, T> callFactory,
             ProgressBarBuilder progressBarBuilder,
             PrintWriter output,
             PrintWriter errOutput,
@@ -62,11 +61,10 @@ public class AsyncCallExecutionPipeline<I extends 
CallInput, T> extends Abstract
                 output.flush();
             }
         });
-        progressBarBuilder.setUpdateIntervalMillis(60);
 
         try {
             ProgressBarTracker tracker = new 
ProgressBarTracker(progressBarBuilder);
-            CallOutput<T> result = callFactory.apply(tracker)
+            CallOutput<T> result = callFactory.create(tracker)
                     .execute(callInput)
                     .whenComplete((el, err) -> tracker.close())
                     .join();
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallExecutionPipelineBuilder.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallExecutionPipelineBuilder.java
index f98be0fe89e..5354d896048 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallExecutionPipelineBuilder.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallExecutionPipelineBuilder.java
@@ -21,7 +21,6 @@ import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.nio.charset.Charset;
 import java.time.temporal.ChronoUnit;
-import java.util.function.Function;
 import java.util.function.Supplier;
 import me.tongfei.progressbar.ProgressBarBuilder;
 import me.tongfei.progressbar.ProgressBarStyle;
@@ -34,7 +33,7 @@ import 
org.apache.ignite.internal.cli.core.exception.handler.DefaultExceptionHan
 /** Builder for {@link AsyncCallExecutionPipeline}. */
 public class AsyncCallExecutionPipelineBuilder<I extends CallInput, T> 
implements CallExecutionPipelineBuilder<I, T> {
 
-    private final Function<ProgressTracker, AsyncCall<I, T>> callFactory;
+    private final AsyncCallFactory<I, T> callFactory;
 
     private final ProgressBarBuilder progressBarBuilder = new 
ProgressBarBuilder()
             .setStyle(ProgressBarStyle.UNICODE_BLOCK)
@@ -42,8 +41,8 @@ public class AsyncCallExecutionPipelineBuilder<I extends 
CallInput, T> implement
             .setSpeedUnit(ChronoUnit.SECONDS)
             .setInitialMax(100)
             .hideEta()
-            .setTaskName("")
-            .showSpeed();
+            .showSpeed()
+            .setUpdateIntervalMillis(60);
 
     private final ExceptionHandlers exceptionHandlers = new 
DefaultExceptionHandlers();
 
@@ -57,7 +56,7 @@ public class AsyncCallExecutionPipelineBuilder<I extends 
CallInput, T> implement
 
     private boolean[] verbose;
 
-    AsyncCallExecutionPipelineBuilder(Function<ProgressTracker, AsyncCall<I, 
T>> callFactory) {
+    AsyncCallExecutionPipelineBuilder(AsyncCallFactory<I, T> callFactory) {
         this.callFactory = callFactory;
     }
 
@@ -110,14 +109,26 @@ public class AsyncCallExecutionPipelineBuilder<I extends 
CallInput, T> implement
         return this;
     }
 
-    @Override
-    public AsyncCallExecutionPipelineBuilder<I, T> verbose(boolean[] verbose) {
-        this.verbose = verbose;
+    /**
+     * Changes default progress bar to simple spinner, which prints 1 to 3 
dots after the prefix.
+     *
+     * @param prefix Prefix.
+     * @return This builder.
+     */
+    public AsyncCallExecutionPipelineBuilder<I, T> enableSpinner(String 
prefix) {
+        SpinnerRenderer renderer = new SpinnerRenderer(prefix);
+        this.progressBarBuilder.setRenderer((progress, maxLength) -> 
renderer.render(maxLength));
         return this;
     }
 
-    public AsyncCallExecutionPipelineBuilder<I, T> name(String name) {
-        this.progressBarBuilder.setTaskName(name);
+    public AsyncCallExecutionPipelineBuilder<I, T> updateIntervalMillis(int 
updateIntervalMillis) {
+        this.progressBarBuilder.setUpdateIntervalMillis(updateIntervalMillis);
+        return this;
+    }
+
+    @Override
+    public AsyncCallExecutionPipelineBuilder<I, T> verbose(boolean[] verbose) {
+        this.verbose = verbose;
         return this;
     }
 
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallFactory.java
similarity index 61%
copy from 
modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallFactory.java
index 18f900e6857..0d13f5211cf 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/AsyncCallFactory.java
@@ -15,21 +15,15 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.cluster;
+package org.apache.ignite.internal.cli.core.call;
 
-import 
org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitReplCommand;
-import org.junit.jupiter.api.DisplayName;
-
-/** Tests "cluster init" command in REPL mode. */
-@DisplayName("cluster init repl")
-public class ClusterInitReplTest extends ClusterInitTest {
-    @Override
-    protected Class<?> getCommandClass() {
-        return ClusterInitReplCommand.class;
-    }
-
-    @Override
-    protected int errorExitCode() {
-        return 0;
-    }
+/**
+ * Async call factory.
+ *
+ * @param <IT> Input type.
+ * @param <OT> Output type.
+ */
+@FunctionalInterface
+public interface AsyncCallFactory<IT extends CallInput, OT> {
+    AsyncCall<IT, OT> create(ProgressTracker tracker);
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/CallExecutionPipeline.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/CallExecutionPipeline.java
index 2665640a9c6..3cda3daaa68 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/CallExecutionPipeline.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/CallExecutionPipeline.java
@@ -17,8 +17,6 @@
 
 package org.apache.ignite.internal.cli.core.call;
 
-import java.util.function.Function;
-
 /** Pipeline that executes a call. */
 @FunctionalInterface
 public interface CallExecutionPipeline<I extends CallInput, T> {
@@ -32,9 +30,7 @@ public interface CallExecutionPipeline<I extends CallInput, 
T> {
     }
 
     /** Builder helper method. */
-    static <I extends CallInput, T> AsyncCallExecutionPipelineBuilder<I, T> 
asyncBuilder(
-            Function<ProgressTracker, AsyncCall<I, T>> callFactory
-    ) {
+    static <I extends CallInput, T> AsyncCallExecutionPipelineBuilder<I, T> 
asyncBuilder(AsyncCallFactory<I, T> callFactory) {
         return new AsyncCallExecutionPipelineBuilder<>(callFactory);
     }
 
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/SpinnerRenderer.java
similarity index 54%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
copy to 
modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/SpinnerRenderer.java
index 5e208d1b665..4f970a000fe 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/call/SpinnerRenderer.java
@@ -15,23 +15,30 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.call.cluster.unit;
+package org.apache.ignite.internal.cli.core.call;
 
-import jakarta.inject.Singleton;
-import org.apache.ignite.internal.cli.core.call.ProgressTracker;
-import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
-
-/** Factory for {@link DeployUnitCall}. */
-@Singleton
-public class DeployUnitCallFactory {
+/**
+ * Simple spinner renderer which adds up to 3 dots to the prefix text on each 
render call.
+ */
+class SpinnerRenderer {
+    private final String prefix;
 
-    private final ApiClientFactory factory;
+    private int current;
 
-    public DeployUnitCallFactory(ApiClientFactory factory) {
-        this.factory = factory;
+    SpinnerRenderer(String prefix) {
+        this.prefix = prefix;
     }
 
-    public DeployUnitCall create(ProgressTracker tracker) {
-        return new DeployUnitCall(tracker, factory);
+    public String render(int maxLength) {
+        if (maxLength <= 0) {
+            return "";
+        }
+
+        String res = prefix + ".".repeat(current + 1)
+                + " ".repeat(2 - current)
+                + "\b".repeat(2 - current); // Make the cursor appear after 
the last dot
+        current = (current + 1) % 3;
+
+        return res.substring(0, Math.min(res.length(), maxLength));
     }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/ConnectionHeartBeat.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/ConnectionHeartBeat.java
index 7e25c11faa1..61cfda05bc5 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/ConnectionHeartBeat.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/ConnectionHeartBeat.java
@@ -23,6 +23,7 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
@@ -57,6 +58,8 @@ public class ConnectionHeartBeat implements 
ConnectionEventListener {
 
     private final AtomicBoolean connected = new AtomicBoolean(false);
 
+    private final AtomicReference<String> lastKnownUrl = new 
AtomicReference<>(null);
+
     private final Lock lock = new ReentrantLock();
 
     /**
@@ -84,6 +87,8 @@ public class ConnectionHeartBeat implements 
ConnectionEventListener {
             eventPublisher.publish(Events.connectionRestored());
         }
 
+        lastKnownUrl.set(sessionInfo.nodeUrl());
+
         lock.lock();
         try {
             if (scheduledConnectionHeartbeatExecutor == null) {
@@ -92,7 +97,7 @@ public class ConnectionHeartBeat implements 
ConnectionEventListener {
 
                 // Start connection heart beat
                 scheduledConnectionHeartbeatExecutor.scheduleAtFixedRate(
-                        () -> pingConnection(sessionInfo.nodeUrl()),
+                        this::pingConnection,
                         0,
                         cliCheckConnectionPeriodSecond,
                         TimeUnit.SECONDS
@@ -119,9 +124,12 @@ public class ConnectionHeartBeat implements 
ConnectionEventListener {
         }
     }
 
-    private void pingConnection(String nodeUrl) {
+    /**
+     * Checks connection to last connected node.
+     */
+    public void pingConnection() {
         try {
-            new 
NodeManagementApi(clientFactory.getClient(nodeUrl)).nodeState();
+            new 
NodeManagementApi(clientFactory.getClient(lastKnownUrl.get())).nodeState();
             if (connected.compareAndSet(false, true)) {
                 eventPublisher.publish(Events.connectionRestored());
             }
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java
index 1ab5f40adda..47ba427771e 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/CliCommandTestBase.java
@@ -17,17 +17,17 @@
 
 package org.apache.ignite.internal.cli.commands;
 
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.emptyString;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.matchesRegex;
 import static org.hamcrest.Matchers.not;
 import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -42,6 +42,8 @@ import java.io.StringWriter;
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Function;
+import org.apache.ignite.internal.cli.core.call.AsyncCall;
+import org.apache.ignite.internal.cli.core.call.AsyncCallFactory;
 import org.apache.ignite.internal.cli.core.call.Call;
 import org.apache.ignite.internal.cli.core.call.CallInput;
 import org.apache.ignite.internal.cli.core.call.DefaultCallOutput;
@@ -162,8 +164,6 @@ public abstract class CliCommandTestBase extends 
BaseIgniteAbstractTest {
      * @param expectedOutput Expected command output.
      */
     protected void assertSuccessfulOutputIs(String expectedOutput) {
-        log.info(sout.toString());
-        log.info(serr.toString());
         assertAll(
                 this::assertExitCodeIsZero,
                 () -> assertOutputIs(expectedOutput),
@@ -171,15 +171,30 @@ public abstract class CliCommandTestBase extends 
BaseIgniteAbstractTest {
         );
     }
 
+    /**
+     * Asserts that the command's exit code is zero, output contains expected 
output and the error output is empty.
+     *
+     * @param expectedOutput Expected command output.
+     */
+    protected void assertSuccessfulOutputContains(String expectedOutput) {
+        assertAll(
+                this::assertExitCodeIsZero,
+                () -> assertOutputContains(expectedOutput),
+                this::assertErrOutputIsEmpty
+        );
+    }
+
     /**
      * Asserts that {@code expected} and {@code actual} are equals ignoring 
differences in line separators.
      *
      * @param reason Description of the assertion.
-     * @param exp Expected result.
      * @param actual Actual result.
+     * @param exp Expected result.
      */
-    private static void assertEqualsIgnoreLineSeparators(String reason, String 
exp, String actual) {
-        assertThat(reason, exp.lines().collect(toList()), 
contains(actual.lines().toArray(String[]::new)));
+    private static void assertEqualsIgnoreLineSeparators(String reason, String 
actual, String exp) {
+        String actualJoined = 
actual.lines().collect(joining(System.lineSeparator()));
+        String expJoined = 
exp.lines().collect(joining(System.lineSeparator()));
+        assertThat(reason, actualJoined, is(expJoined));
     }
 
     /**
@@ -206,9 +221,52 @@ public abstract class CliCommandTestBase extends 
BaseIgniteAbstractTest {
         T call = registerMockCall(callClass);
         // Recreate the CommandLine object so that the registered mocks are 
available to this command.
         createCommand();
+
         execute(command + " " + parameters);
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertErrOutputIsEmpty
+        );
+
         IT callInput = verifyCallInput(call, callInputClass);
-        assertEquals(expected, inputTransformer.apply(callInput));
+        assertThat(inputTransformer.apply(callInput), is(expected));
+    }
+
+    /**
+     * Runs the command with the mock call and verifies that the call was 
executed with the expected input.
+     *
+     * @param command Command string.
+     * @param callFactoryClass Call factory class.
+     * @param callClass Call class.
+     * @param callInputClass Call input class.
+     * @param inputTransformer Function which transforms the call input to 
string.
+     * @param parameters Command arguments.
+     * @param expected Expected call input string.
+     * @param <IT> Input for the call.
+     * @param <OT> Output of the call.
+     * @param <T> Call type.
+     */
+    protected <IT extends CallInput, OT, T extends AsyncCall<IT, OT>, FT 
extends AsyncCallFactory<IT, OT>> void checkParametersAsync(
+            String command,
+            Class<FT> callFactoryClass,
+            Class<T> callClass,
+            Class<IT> callInputClass,
+            Function<IT, String> inputTransformer,
+            String parameters,
+            String expected
+    ) {
+        AsyncCall<IT, OT> call = registerMockCallAsync(callFactoryClass, 
callClass);
+        // Recreate the CommandLine object so that the registered mocks are 
available to this command.
+        createCommand();
+
+        execute(command + " " + parameters);
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertErrOutputIsEmpty
+        );
+
+        IT callInput = verifyCallInputAsync(call, callInputClass);
+        assertThat(inputTransformer.apply(callInput), is(expected));
     }
 
     /**
@@ -227,6 +285,29 @@ public abstract class CliCommandTestBase extends 
BaseIgniteAbstractTest {
         return mock;
     }
 
+    /**
+     * Registers mock async call factory of the specified class into the 
Micronaut's context. Mock factory creates mock calls. Mock call
+     * returns empty output when executed.
+     *
+     * @param callFactoryClass Call class.
+     * @param <IT> Input for the call.
+     * @param <OT> Output of the call.
+     * @param <FT> Call factory type.
+     * @param <T> Call type.
+     * @return Created mock.
+     */
+    private <IT extends CallInput, OT, T extends AsyncCall<IT, OT>, FT extends 
AsyncCallFactory<IT, OT>>
+            T registerMockCallAsync(Class<FT> callFactoryClass, Class<T> 
callClass) {
+        FT mockCallFactory = mock(callFactoryClass);
+        context.registerSingleton(mockCallFactory);
+
+        T mockCall = mock(callClass);
+        
when(mockCall.execute(any())).thenReturn(completedFuture(DefaultCallOutput.empty()));
+
+        when(mockCallFactory.create(any())).thenReturn(mockCall);
+        return mockCall;
+    }
+
     /**
      * Verifies that the call was executed and returns its input.
      *
@@ -242,4 +323,20 @@ public abstract class CliCommandTestBase extends 
BaseIgniteAbstractTest {
         verify(call).execute(captor.capture());
         return captor.getValue();
     }
+
+    /**
+     * Verifies that the async call was executed and returns its input.
+     *
+     * @param call Call mock.
+     * @param inputClass Call input class.
+     * @param <IT> Input for the call.
+     * @param <OT> Output of the call.
+     * @param <T> Call type.
+     * @return Call input.
+     */
+    private static <IT extends CallInput, OT, T extends AsyncCall<IT, OT>> IT 
verifyCallInputAsync(T call, Class<IT> inputClass) {
+        ArgumentCaptor<IT> captor = ArgumentCaptor.forClass(inputClass);
+        verify(call).execute(captor.capture());
+        return captor.getValue();
+    }
 }
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java
index e1f8bcfa1d0..ff1cf433df8 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/ProfileMixinTest.java
@@ -19,14 +19,22 @@ package org.apache.ignite.internal.cli.commands;
 
 import static org.junit.jupiter.params.provider.Arguments.arguments;
 
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.function.Function;
 import java.util.stream.Stream;
 import org.apache.ignite.internal.cli.call.cluster.ClusterInitCall;
+import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallFactory;
 import org.apache.ignite.internal.cli.call.cluster.ClusterInitCallInput;
 import org.apache.ignite.internal.cli.call.cluster.status.ClusterStatusCall;
 import 
org.apache.ignite.internal.cli.call.cluster.topology.LogicalTopologyCall;
 import 
org.apache.ignite.internal.cli.call.cluster.topology.PhysicalTopologyCall;
 import org.apache.ignite.internal.cli.call.cluster.unit.ClusterListUnitCall;
+import org.apache.ignite.internal.cli.call.cluster.unit.DeployUnitCall;
+import org.apache.ignite.internal.cli.call.cluster.unit.DeployUnitCallFactory;
+import org.apache.ignite.internal.cli.call.cluster.unit.DeployUnitCallInput;
 import org.apache.ignite.internal.cli.call.cluster.unit.UndeployUnitCall;
 import org.apache.ignite.internal.cli.call.cluster.unit.UndeployUnitCallInput;
 import org.apache.ignite.internal.cli.call.configuration.ClusterConfigShowCall;
@@ -46,10 +54,16 @@ import 
org.apache.ignite.internal.cli.call.recovery.restart.RestartPartitionsCal
 import org.apache.ignite.internal.cli.call.recovery.states.PartitionStatesCall;
 import 
org.apache.ignite.internal.cli.call.recovery.states.PartitionStatesCallInput;
 import org.apache.ignite.internal.cli.call.unit.ListUnitCallInput;
+import org.apache.ignite.internal.cli.core.call.AsyncCall;
+import org.apache.ignite.internal.cli.core.call.AsyncCallFactory;
 import org.apache.ignite.internal.cli.core.call.Call;
 import org.apache.ignite.internal.cli.core.call.CallInput;
 import org.apache.ignite.internal.cli.core.call.UrlCallInput;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -57,6 +71,8 @@ import org.junit.jupiter.params.provider.MethodSource;
 /**
  * Test for --profile override for --url options.
  */
+@MicronautTest(resolveParameters = false)
+@ExtendWith(WorkDirectoryExtension.class)
 public class ProfileMixinTest extends CliCommandTestBase {
     /**
      * Cluster URL from default profile in integration_tests.ini.
@@ -73,6 +89,16 @@ public class ProfileMixinTest extends CliCommandTestBase {
      */
     private static final String URL_FROM_CMD = "http://localhost:10302";;
 
+    @WorkDirectory
+    private static Path WORK_DIR;
+
+    private static String TEMP_FILE_PATH;
+
+    @BeforeAll
+    public static void createTempFile() throws IOException {
+        TEMP_FILE_PATH = 
Files.createFile(WORK_DIR.resolve("temp.txt")).toString();
+    }
+
     @ParameterizedTest
     @DisplayName("Should take URL from default profile")
     @MethodSource("allCallsProvider")
@@ -85,6 +111,19 @@ public class ProfileMixinTest extends CliCommandTestBase {
         checkParameters(command, callClass, callInputClass, urlSupplier, "", 
DEFAULT_URL);
     }
 
+    @ParameterizedTest
+    @DisplayName("Should take URL from default profile")
+    @MethodSource("allAsyncCallsProvider")
+    <IT extends CallInput, OT, T extends AsyncCall<IT, OT>, FT extends 
AsyncCallFactory<IT, OT>> void defaultUrlAsync(
+            String command,
+            Class<FT> callFactoryClass,
+            Class<T> callClass,
+            Class<IT> callInputClass,
+            Function<IT, String> urlSupplier
+    ) {
+        checkParametersAsync(command, callFactoryClass, callClass, 
callInputClass, urlSupplier, "", DEFAULT_URL);
+    }
+
     @ParameterizedTest
     @DisplayName("Should take URL from specified profile")
     @MethodSource("allCallsProvider")
@@ -97,6 +136,19 @@ public class ProfileMixinTest extends CliCommandTestBase {
         checkParameters(command, callClass, callInputClass, urlSupplier, 
"--profile test", URL_FROM_PROFILE);
     }
 
+    @ParameterizedTest
+    @DisplayName("Should take URL from specified profile")
+    @MethodSource("allAsyncCallsProvider")
+    <IT extends CallInput, OT, T extends AsyncCall<IT, OT>, FT extends 
AsyncCallFactory<IT, OT>> void profileUrlAsync(
+            String command,
+            Class<FT> callFactoryClass,
+            Class<T> callClass,
+            Class<IT> callInputClass,
+            Function<IT, String> urlSupplier
+    ) {
+        checkParametersAsync(command, callFactoryClass, callClass, 
callInputClass, urlSupplier, "--profile test", URL_FROM_PROFILE);
+    }
+
     @ParameterizedTest
     @DisplayName("Should take node URL from command line")
     @MethodSource("nodeCallsProvider")
@@ -121,6 +173,19 @@ public class ProfileMixinTest extends CliCommandTestBase {
         checkParameters(command, callClass, callInputClass, urlSupplier, 
"--url " + URL_FROM_CMD, URL_FROM_CMD);
     }
 
+    @ParameterizedTest
+    @DisplayName("Should take cluster endpoint URL from command line")
+    @MethodSource("clusterAsyncCallsProvider")
+    <IT extends CallInput, OT, T extends AsyncCall<IT, OT>, FT extends 
AsyncCallFactory<IT, OT>> void commandClusterUrlAsync(
+            String command,
+            Class<FT> callFactoryClass,
+            Class<T> callClass,
+            Class<IT> callInputClass,
+            Function<IT, String> urlSupplier
+    ) {
+        checkParametersAsync(command, callFactoryClass, callClass, 
callInputClass, urlSupplier, "--url " + URL_FROM_CMD, URL_FROM_CMD);
+    }
+
     @ParameterizedTest
     @DisplayName("Node URL from command line should override specified 
profile")
     @MethodSource("nodeCallsProvider")
@@ -145,6 +210,21 @@ public class ProfileMixinTest extends CliCommandTestBase {
         checkParameters(command, callClass, callInputClass, urlSupplier, 
"--profile test --url " + URL_FROM_CMD, URL_FROM_CMD);
     }
 
+    @ParameterizedTest
+    @DisplayName("Cluster endpoint URL from command line should override 
specified profile")
+    @MethodSource("clusterAsyncCallsProvider")
+    <IT extends CallInput, OT, T extends AsyncCall<IT, OT>, FT extends 
AsyncCallFactory<IT, OT>>
+            void commandClusterUrlOverridesProfileAsync(
+            String command,
+            Class<FT> callFactoryClass,
+            Class<T> callClass,
+            Class<IT> callInputClass,
+            Function<IT, String> urlSupplier
+    ) {
+        checkParametersAsync(command, callFactoryClass, callClass, 
callInputClass, urlSupplier, "--profile test --url " + URL_FROM_CMD,
+                URL_FROM_CMD);
+    }
+
     private static Stream<Arguments> nodeCallsProvider() {
         return Stream.of(
                 arguments(
@@ -188,12 +268,6 @@ public class ProfileMixinTest extends CliCommandTestBase {
                         ClusterConfigUpdateCallInput.class,
                         (Function<ClusterConfigUpdateCallInput, String>) 
ClusterConfigUpdateCallInput::getClusterUrl
                 ),
-                arguments(
-                        "cluster init --name cluster --metastorage-group node",
-                        ClusterInitCall.class,
-                        ClusterInitCallInput.class,
-                        (Function<ClusterInitCallInput, String>) 
ClusterInitCallInput::getClusterUrl
-                ),
                 arguments(
                         "cluster topology physical",
                         PhysicalTopologyCall.class,
@@ -212,13 +286,6 @@ public class ProfileMixinTest extends CliCommandTestBase {
                         UrlCallInput.class,
                         (Function<UrlCallInput, String>) UrlCallInput::getUrl
                 ),
-                // Doesn't work because this command is special - it uses 
AsyncCall and call factory
-                // arguments(
-                //         "cluster unit deploy",
-                //         DeployUnitCall.class,
-                //         DeployUnitCallInput.class,
-                //         (Function<DeployUnitCallInput, String>) 
DeployUnitCallInput::clusterUrl
-                // ),
                 arguments(
                         "cluster unit list",
                         ClusterListUnitCall.class,
@@ -252,10 +319,33 @@ public class ProfileMixinTest extends CliCommandTestBase {
         );
     }
 
+    private static Stream<Arguments> clusterAsyncCallsProvider() {
+        return Stream.of(
+                arguments(
+                        "cluster init --name cluster --metastorage-group node",
+                        ClusterInitCallFactory.class,
+                        ClusterInitCall.class,
+                        ClusterInitCallInput.class,
+                        (Function<ClusterInitCallInput, String>) 
ClusterInitCallInput::getClusterUrl
+                ),
+                 arguments(
+                         "cluster unit deploy foo --version=1 --path=" + 
TEMP_FILE_PATH,
+                         DeployUnitCallFactory.class,
+                         DeployUnitCall.class,
+                         DeployUnitCallInput.class,
+                         (Function<DeployUnitCallInput, String>) 
DeployUnitCallInput::clusterUrl
+                 )
+        );
+    }
+
     private static Stream<Arguments> allCallsProvider() {
         return Stream.concat(nodeCallsProvider(), clusterCallsProvider());
     }
 
+    private static Stream<Arguments> allAsyncCallsProvider() {
+        return clusterAsyncCallsProvider();
+    }
+
     @Override
     protected Class<?> getCommandClass() {
         return TopLevelCliCommand.class;
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
index aea7b052219..7904bdbb24f 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/UrlOptionsNegativeTest.java
@@ -20,9 +20,12 @@ package org.apache.ignite.internal.cli.commands;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertAll;
 import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.mockito.Mockito.mock;
 
 import io.micronaut.configuration.picocli.MicronautFactory;
 import io.micronaut.context.ApplicationContext;
+import io.micronaut.context.annotation.Bean;
+import io.micronaut.context.annotation.Replaces;
 import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
 import jakarta.inject.Inject;
 import java.io.IOException;
@@ -70,8 +73,9 @@ import 
org.apache.ignite.internal.cli.commands.node.status.NodeStatusCommand;
 import 
org.apache.ignite.internal.cli.commands.node.status.NodeStatusReplCommand;
 import org.apache.ignite.internal.cli.commands.node.unit.NodeUnitListCommand;
 import 
org.apache.ignite.internal.cli.commands.node.unit.NodeUnitListReplCommand;
+import org.apache.ignite.internal.cli.core.repl.ConnectionHeartBeat;
 import 
org.apache.ignite.internal.cli.core.repl.context.CommandLineContextProvider;
-import org.apache.ignite.internal.cli.core.repl.registry.NodeNameRegistry;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
 import org.apache.ignite.internal.testframework.WorkDirectory;
 import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
 import org.junit.jupiter.api.BeforeAll;
@@ -88,7 +92,7 @@ import picocli.CommandLine;
  */
 @MicronautTest
 @ExtendWith(WorkDirectoryExtension.class)
-public class UrlOptionsNegativeTest {
+public class UrlOptionsNegativeTest extends BaseIgniteAbstractTest {
     private static final String NODE_URL = "http://localhost:10300";;
 
     private static final String NODE_URL_OPTION = "--url=";
@@ -107,9 +111,6 @@ public class UrlOptionsNegativeTest {
     @Inject
     TestConfigManagerProvider configManagerProvider;
 
-    @Inject
-    NodeNameRegistry nodeNameRegistry;
-
     @WorkDirectory
     protected static Path WORK_DIR;
 
@@ -139,7 +140,7 @@ public class UrlOptionsNegativeTest {
         exitCode = cmd.execute(options.toArray(new String[0]));
     }
 
-    static List<Arguments> cmdClassAndOptionsProvider() {
+    private static List<Arguments> cmdClassAndOptionsProvider() {
         return List.of(
                 arguments(NodeConfigShowCommand.class, NODE_URL_OPTION, 
List.of()),
                 arguments(NodeConfigUpdateCommand.class, NODE_URL_OPTION, 
List.of("{key: value}")),
@@ -165,7 +166,7 @@ public class UrlOptionsNegativeTest {
         );
     }
 
-    static List<Arguments> cmdReplClassAndOptionsProvider() {
+    private static List<Arguments> cmdReplClassAndOptionsProvider() {
         return List.of(
                 arguments(NodeConfigShowReplCommand.class, NODE_URL_OPTION, 
List.of()),
                 arguments(NodeConfigUpdateReplCommand.class, NODE_URL_OPTION, 
List.of("{key: value}")),
@@ -229,7 +230,6 @@ public class UrlOptionsNegativeTest {
 
         assertAll(
                 this::assertExitCodeIsFailure,
-                this::assertOutputIsEmpty,
                 () -> assertErrOutputIs(
                         "Unknown host: http://no-such-host.com"; + 
System.lineSeparator())
         );
@@ -243,7 +243,6 @@ public class UrlOptionsNegativeTest {
 
         assertAll(
                 this::assertExitCodeIsFailure,
-                this::assertOutputIsEmpty,
                 () -> assertErrOutputIs("Node unavailable" + 
System.lineSeparator()
                         + "Could not connect to node with URL " + NODE_URL + 
System.lineSeparator())
         );
@@ -283,10 +282,7 @@ public class UrlOptionsNegativeTest {
     void invalidUrlRepl(Class<?> cmdClass, String urlOptionName, List<String> 
additionalOptions) {
         execute(cmdClass, urlOptionName, "http://no-such-host.com";, 
additionalOptions);
 
-        assertAll(
-                this::assertOutputIsEmpty,
-                () -> assertErrOutputIs("Unknown host: 
http://no-such-host.com"; + System.lineSeparator())
-        );
+        assertErrOutputIs("Unknown host: http://no-such-host.com"; + 
System.lineSeparator());
     }
 
     @ParameterizedTest
@@ -295,11 +291,8 @@ public class UrlOptionsNegativeTest {
     void connectErrorRepl(Class<?> cmdClass, String urlOptionName, 
List<String> additionalOptions) {
         execute(cmdClass, urlOptionName, NODE_URL, additionalOptions);
 
-        assertAll(
-                this::assertOutputIsEmpty,
-                () -> assertErrOutputIs("Node unavailable" + 
System.lineSeparator()
-                        + "Could not connect to node with URL " + NODE_URL + 
System.lineSeparator())
-        );
+        assertErrOutputIs("Node unavailable" + System.lineSeparator()
+                        + "Could not connect to node with URL " + NODE_URL + 
System.lineSeparator());
     }
 
     @Test
@@ -344,4 +337,9 @@ public class UrlOptionsNegativeTest {
                 .contains(expectedErrOutput);
     }
 
+    @Bean
+    @Replaces(ConnectionHeartBeat.class)
+    public static ConnectionHeartBeat connectionHeartBeat() {
+        return mock(ConnectionHeartBeat.class);
+    }
 }
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
index 18f900e6857..cd25c036dea 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
@@ -17,7 +17,12 @@
 
 package org.apache.ignite.internal.cli.commands.cluster;
 
+import static org.mockito.Mockito.mock;
+
+import io.micronaut.context.annotation.Bean;
+import io.micronaut.context.annotation.Replaces;
 import 
org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitReplCommand;
+import org.apache.ignite.internal.cli.core.repl.ConnectionHeartBeat;
 import org.junit.jupiter.api.DisplayName;
 
 /** Tests "cluster init" command in REPL mode. */
@@ -32,4 +37,10 @@ public class ClusterInitReplTest extends ClusterInitTest {
     protected int errorExitCode() {
         return 0;
     }
+
+    @Bean
+    @Replaces(ConnectionHeartBeat.class)
+    public static ConnectionHeartBeat connectionHeartBeat() {
+        return mock(ConnectionHeartBeat.class);
+    }
 }
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitTest.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitTest.java
index 7c45eda673d..2fd16bcb8f6 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitTest.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitTest.java
@@ -85,7 +85,7 @@ class ClusterInitTest extends IgniteCliInterfaceTestBase {
                 "--name", "cluster"
         );
 
-        assertSuccessfulOutputIs("Cluster was initialized successfully");
+        assertSuccessfulOutputContains("Cluster was initialized 
successfully.");
     }
 
     @Test
@@ -131,7 +131,7 @@ class ClusterInitTest extends IgniteCliInterfaceTestBase {
                 "--name", "cluster"
         );
 
-        assertSuccessfulOutputIs("Cluster was initialized successfully");
+        assertSuccessfulOutputContains("Cluster was initialized 
successfully.");
     }
 
     @Test
@@ -155,7 +155,7 @@ class ClusterInitTest extends IgniteCliInterfaceTestBase {
                 "--name", "cluster"
         );
 
-        assertSuccessfulOutputIs("Cluster was initialized successfully");
+        assertSuccessfulOutputContains("Cluster was initialized 
successfully.");
     }
 
     @Test
@@ -197,7 +197,7 @@ class ClusterInitTest extends IgniteCliInterfaceTestBase {
                 "--config-files", clusterConfigurationFile.toString()
         );
 
-        assertSuccessfulOutputIs("Cluster was initialized successfully");
+        assertSuccessfulOutputContains("Cluster was initialized 
successfully.");
     }
 
     @Test
@@ -221,7 +221,7 @@ class ClusterInitTest extends IgniteCliInterfaceTestBase {
 
         assertAll(
                 this::assertExitCodeIsError,
-                this::assertOutputIsEmpty,
+                () -> assertOutputContains("Initializing"), // Spinner output
                 () -> assertErrOutputIs("Oops")
         );
     }
@@ -249,7 +249,7 @@ class ClusterInitTest extends IgniteCliInterfaceTestBase {
                 "--name", "cluster"
         );
 
-        assertSuccessfulOutputIs("Cluster was initialized successfully");
+        assertSuccessfulOutputContains("Cluster was initialized 
successfully.");
     }
 
     @Test
@@ -263,7 +263,7 @@ class ClusterInitTest extends IgniteCliInterfaceTestBase {
                 "--name", "cluster"
         );
 
-        assertSuccessfulOutputIs("Cluster was initialized successfully");
+        assertSuccessfulOutputContains("Cluster was initialized 
successfully.");
     }
 
     @Test
@@ -338,7 +338,7 @@ class ClusterInitTest extends IgniteCliInterfaceTestBase {
                 "--config-files", String.join(",", clusterConfigurationFile1, 
clusterConfigurationFile2)
         );
 
-        assertSuccessfulOutputIs("Cluster was initialized successfully");
+        assertSuccessfulOutputContains("Cluster was initialized 
successfully.");
     }
 
     private static String escapedJson(String configuration) {
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/core/call/SpinnerRendererTest.java
similarity index 53%
copy from 
modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
copy to 
modules/cli/src/test/java/org/apache/ignite/internal/cli/core/call/SpinnerRendererTest.java
index 5e208d1b665..18e2badf0ba 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/unit/DeployUnitCallFactory.java
+++ 
b/modules/cli/src/test/java/org/apache/ignite/internal/cli/core/call/SpinnerRendererTest.java
@@ -15,23 +15,27 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.call.cluster.unit;
+package org.apache.ignite.internal.cli.core.call;
 
-import jakarta.inject.Singleton;
-import org.apache.ignite.internal.cli.core.call.ProgressTracker;
-import org.apache.ignite.internal.cli.core.rest.ApiClientFactory;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.emptyString;
+import static org.hamcrest.Matchers.is;
 
-/** Factory for {@link DeployUnitCall}. */
-@Singleton
-public class DeployUnitCallFactory {
+import org.junit.jupiter.api.Test;
 
-    private final ApiClientFactory factory;
+class SpinnerRendererTest {
 
-    public DeployUnitCallFactory(ApiClientFactory factory) {
-        this.factory = factory;
-    }
+    @Test
+    void render() {
+        SpinnerRenderer renderer = new SpinnerRenderer("prefix");
+
+        assertThat(renderer.render(0), is(emptyString()));
+
+        assertThat(renderer.render(11), is("prefix.  \b\b"));
+        assertThat(renderer.render(11), is("prefix.. \b"));
+        assertThat(renderer.render(11), is("prefix..."));
+        assertThat(renderer.render(11), is("prefix.  \b\b"));
 
-    public DeployUnitCall create(ProgressTracker tracker) {
-        return new DeployUnitCall(tracker, factory);
+        assertThat(renderer.render(1), is("p"));
     }
 }
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
index e0113fce6d4..009bbcb5370 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementController.java
@@ -50,18 +50,23 @@ public class ClusterManagementController implements 
ClusterManagementApi, Resour
 
     private ClusterManagementGroupManager clusterManagementGroupManager;
 
+    private JoinFutureProvider joinFutureProvider;
+
     /**
      * Cluster management controller constructor.
      *
-     * @param clusterInitializer cluster initializer.
-     * @param clusterManagementGroupManager cluster management group manager.
+     * @param clusterInitializer Cluster initializer.
+     * @param clusterManagementGroupManager Cluster management group manager.
+     * @param joinFutureProvider Node join future provider.
      */
     public ClusterManagementController(
             ClusterInitializer clusterInitializer,
-            ClusterManagementGroupManager clusterManagementGroupManager
+            ClusterManagementGroupManager clusterManagementGroupManager,
+            JoinFutureProvider joinFutureProvider
     ) {
         this.clusterInitializer = clusterInitializer;
         this.clusterManagementGroupManager = clusterManagementGroupManager;
+        this.joinFutureProvider = joinFutureProvider;
     }
 
     @Override
@@ -80,13 +85,18 @@ public class ClusterManagementController implements 
ClusterManagementApi, Resour
         );
 
         return clusterInitializer.initCluster(
-                initCommand.metaStorageNodes(),
-                initCommand.cmgNodes(),
-                initCommand.clusterName(),
-                initCommand.clusterConfiguration()
-        ).exceptionally(ex -> {
-            throw mapException(ex);
-        });
+                        initCommand.metaStorageNodes(),
+                        initCommand.cmgNodes(),
+                        initCommand.clusterName(),
+                        initCommand.clusterConfiguration()
+                )
+                .thenCompose(unused -> joinFutureProvider.joinFuture())
+                .handle((unused, ex) -> {
+                    if (ex != null) {
+                        throw mapException(ex);
+                    }
+                    return null;
+                });
     }
 
     private static ClusterState mapClusterState(@Nullable 
org.apache.ignite.internal.cluster.management.ClusterState clusterState) {
@@ -124,5 +134,6 @@ public class ClusterManagementController implements 
ClusterManagementApi, Resour
     public void cleanResources() {
         clusterInitializer = null;
         clusterManagementGroupManager = null;
+        joinFutureProvider = null;
     }
 }
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementRestFactory.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementRestFactory.java
index 0fb47d94c15..7636eafc304 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementRestFactory.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/ClusterManagementRestFactory.java
@@ -37,15 +37,19 @@ public class ClusterManagementRestFactory implements 
RestFactory {
 
     private ClusterManagementGroupManager cmgManager;
 
+    private JoinFutureProvider joinFutureProvider;
+
     /** Constructor. */
     public ClusterManagementRestFactory(
             ClusterService clusterService,
             ClusterInitializer clusterInitializer,
-            ClusterManagementGroupManager cmgManager
+            ClusterManagementGroupManager cmgManager,
+            JoinFutureProvider joinFutureProvider
     ) {
         this.clusterService = clusterService;
         this.clusterInitializer = clusterInitializer;
         this.cmgManager = cmgManager;
+        this.joinFutureProvider = joinFutureProvider;
     }
 
     @Bean
@@ -66,10 +70,17 @@ public class ClusterManagementRestFactory implements 
RestFactory {
         return clusterService.topologyService();
     }
 
+    @Bean
+    @Singleton
+    public JoinFutureProvider joinFutureProvider() {
+        return joinFutureProvider;
+    }
+
     @Override
     public void cleanResources() {
         clusterService = null;
         clusterInitializer = null;
         cmgManager = null;
+        joinFutureProvider = null;
     }
 }
diff --git 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/JoinFutureProvider.java
similarity index 61%
copy from 
modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
copy to 
modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/JoinFutureProvider.java
index 18f900e6857..d9652e56e57 100644
--- 
a/modules/cli/src/test/java/org/apache/ignite/internal/cli/commands/cluster/ClusterInitReplTest.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/cluster/JoinFutureProvider.java
@@ -15,21 +15,15 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.cli.commands.cluster;
+package org.apache.ignite.internal.rest.cluster;
 
-import 
org.apache.ignite.internal.cli.commands.cluster.init.ClusterInitReplCommand;
-import org.junit.jupiter.api.DisplayName;
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.Ignite;
 
-/** Tests "cluster init" command in REPL mode. */
-@DisplayName("cluster init repl")
-public class ClusterInitReplTest extends ClusterInitTest {
-    @Override
-    protected Class<?> getCommandClass() {
-        return ClusterInitReplCommand.class;
-    }
-
-    @Override
-    protected int errorExitCode() {
-        return 0;
-    }
+/**
+ * Provides node join future for the rest components.
+ */
+@FunctionalInterface
+public interface JoinFutureProvider {
+    CompletableFuture<Ignite> joinFuture();
 }
diff --git 
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
 
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
index 5ec36a173c8..62ff1c37f1f 100644
--- 
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
+++ 
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
@@ -49,7 +49,6 @@ import 
org.apache.ignite.internal.network.configuration.MulticastNodeFinderConfi
 import 
org.apache.ignite.internal.network.configuration.NetworkExtensionConfigurationSchema;
 import 
org.apache.ignite.internal.network.configuration.StaticNodeFinderConfigurationSchema;
 import 
org.apache.ignite.internal.rest.authentication.AuthenticationProviderFactory;
-import org.apache.ignite.internal.rest.cluster.ClusterManagementRestFactory;
 import org.apache.ignite.internal.rest.configuration.PresentationsFactory;
 import org.apache.ignite.internal.rest.configuration.RestConfiguration;
 import 
org.apache.ignite.internal.rest.configuration.RestExtensionConfiguration;
@@ -107,13 +106,11 @@ public class RestComponentTest extends 
BaseIgniteAbstractTest {
                 configurationManager,
                 mock(ConfigurationManager.class)
         );
-        Supplier<RestFactory> clusterManagementRestFactory = () -> new 
ClusterManagementRestFactory(null, null, cmg);
         Supplier<RestFactory> restManagerFactory = () -> new 
RestManagerFactory(restManager);
 
         restComponent = new RestComponent(
                 List.of(restPresentationFactory,
                         authProviderFactory,
-                        clusterManagementRestFactory,
                         restManagerFactory),
                 restManager,
                 restConfiguration
diff --git 
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java 
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index c5200c53d69..4d4a188c36b 100644
--- 
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++ 
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -515,6 +515,9 @@ public class IgniteImpl implements Ignite {
 
     private final PartitionModificationCounterFactory 
partitionModificationCounterFactory;
 
+    /** Future that completes when the node has joined the cluster. */
+    private final CompletableFuture<Ignite> joinFuture = new 
CompletableFuture<>();
+
     /**
      * The Constructor.
      *
@@ -1404,7 +1407,12 @@ public class IgniteImpl implements Ignite {
     private RestComponent createRestComponent(String name) {
         RestManager restManager = new RestManager();
         Supplier<RestFactory> presentationsFactory = () -> new 
PresentationsFactory(nodeCfgMgr, clusterCfgMgr);
-        Supplier<RestFactory> clusterManagementRestFactory = () -> new 
ClusterManagementRestFactory(clusterSvc, clusterInitializer, cmgMgr);
+        Supplier<RestFactory> clusterManagementRestFactory = () -> new 
ClusterManagementRestFactory(
+                clusterSvc,
+                clusterInitializer,
+                cmgMgr,
+                () -> joinFuture
+        );
         Supplier<RestFactory> nodeManagementRestFactory = () -> new 
NodeManagementRestFactory(lifecycleManager, () -> name,
                 new JdbcPortProviderImpl(nodeCfgMgr.configurationRegistry()));
         Supplier<RestFactory> metricRestFactory = () -> new 
MetricRestFactory(metricManager, metricMessaging);
@@ -1553,7 +1561,7 @@ public class IgniteImpl implements Ignite {
         );
         ComponentContext componentContext = new ComponentContext(joinExecutor);
 
-        return cmgMgr.joinFuture()
+        cmgMgr.joinFuture()
                 .thenComposeAsync(unused -> cmgMgr.clusterState(), 
joinExecutor)
                 .thenAcceptAsync(clusterState -> {
                     this.clusterState = clusterState;
@@ -1668,7 +1676,16 @@ public class IgniteImpl implements Ignite {
                     return (Ignite) this;
                 }, joinExecutor)
                 // Moving to the common pool on purpose to close the join pool 
and proceed with user's code in the common pool.
-                .whenCompleteAsync((res, ex) -> joinExecutor.shutdownNow());
+                .whenCompleteAsync((res, ex) -> {
+                    joinExecutor.shutdownNow();
+                    if (ex != null) {
+                        joinFuture.completeExceptionally(ex);
+                    } else {
+                        joinFuture.complete(res);
+                    }
+                });
+
+        return joinFuture;
     }
 
     private CompletableFuture<Void> awaitSelfInLocalLogicalTopology() {

Reply via email to