From 08b6c34da72d92131e0af52e4ecfd1f3f7cc5b70 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Tue, 26 May 2026 15:40:39 +0500 Subject: [PATCH 1/3] PoC --- modules/core/pom.xml | 14 + .../IgniteClusterContainer.java | 68 ++++ .../testcontainers/IgniteContainer.java | 307 ++++++++++++++++++ .../IgniteRebalanceOnUpgradeTest.java | 90 +++++ .../src/test/resources/docker/run-wrapper.sh | 30 ++ .../src/test/resources/docker/test-config.xml | 42 +++ parent/pom.xml | 1 + 7 files changed, 552 insertions(+) create mode 100644 modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java create mode 100644 modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java create mode 100644 modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java create mode 100644 modules/core/src/test/resources/docker/run-wrapper.sh create mode 100644 modules/core/src/test/resources/docker/test-config.xml diff --git a/modules/core/pom.xml b/modules/core/pom.xml index 959a706488e3c..5c2f906a91edb 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -237,6 +237,20 @@ 0.23.0 test + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + + org.slf4j + slf4j-simple + 2.0.12 + test + diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java new file mode 100644 index 0000000000000..50c23b9f0995f --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java @@ -0,0 +1,68 @@ +/* + * 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.testcontainers; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.testcontainers.containers.Network; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.lifecycle.Startables; + +/** Ignite cluster container. */ +public class IgniteClusterContainer implements Startable { + /** Containers. */ + private final List containers; + + /** Network. */ + private final Network net = Network.newNetwork(); + + /** @param nodeIds Node ids. */ + public IgniteClusterContainer(List nodeIds) { + containers = new ArrayList<>(nodeIds.size()); + + for (int i = 0; i < nodeIds.size(); i++) { + String hostname = "node" + (1 + i); + + IgniteContainer ignite = new IgniteContainer(net, hostname, nodeIds.get(i)); + + containers.add(ignite); + } + } + + /** {@inheritDoc} */ + @Override public void start() { + Startables.deepStart(containers).join(); + } + + /** {@inheritDoc} */ + @Override public void stop() { + for (IgniteContainer container : containers) + container.stop(); + } + + /** @return All started nodes. */ + public List nodes() { + return Collections.unmodifiableList(containers); + } + + /** @return First started node in cluster. */ + public IgniteContainer firstNode() { + return containers.get(0); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java new file mode 100644 index 0000000000000..c8fb515c92cd4 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java @@ -0,0 +1,307 @@ +/* + * 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.testcontainers; + +import java.io.File; +import java.io.IOException; +import java.time.Duration; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import com.github.dockerjava.api.command.InspectContainerResponse; +import org.apache.ignite.IgniteException; +import org.apache.ignite.Ignition; +import org.apache.ignite.client.IgniteClient; +import org.apache.ignite.cluster.ClusterState; +import org.apache.ignite.configuration.ClientConfiguration; +import org.apache.ignite.internal.IgniteInterruptedCheckedException; +import org.apache.ignite.internal.util.typedef.internal.U; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import static org.apache.ignite.testframework.GridTestUtils.waitForCondition; +import static org.testcontainers.utility.MountableFile.forClasspathResource; + +/** Ignite container. */ +public class IgniteContainer extends GenericContainer { + /** Logger. */ + private static final Logger LOGGER = LoggerFactory.getLogger(IgniteContainer.class); + + /** Docker image name. */ + private static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse("apacheignite/ignite:2.18.0"); + + /** Work directory. */ + private static final String WORK_DIR = "/opt/ignite/apache-ignite/"; + + /** Libs directory in container. */ + private static final File LIBS_DIR = new File(WORK_DIR + "libs"); + + /** Target libs directory in container. */ + private static final File TARGET_LIBS_DIR = new File("/opt/ignite/target-libs"); + + /** Local target libs directory to copy in container. */ + private static final String LOCAL_TARGET_LIBS_DIR = System.getProperty("local.target.libs", "/tmp/target-libs"); + + /** */ + private static final String CFG_PATH = WORK_DIR + "config/test-config.xml"; + + /** */ + private static final String ENABLE_EXPERIMENTAL_FLAG = "--enable-experimental"; + + /** */ + private static final Pattern CLUSTER_STATE_PATTERN = Pattern.compile("Cluster state: (ACTIVE|INACTIVE)"); + + /** */ + private static final Pattern RU_STATUS_PATTERN = Pattern.compile("Rolling upgrade status: (enabled|disabled)"); + + /** */ + private static final String WRAPPER_SCRIPT = "/opt/ignite/run-wrapper.sh"; + + /** Default thin client port. */ + private static final int THIN_CLIENT_PORT = 10800; + + /** Ignite thin client. */ + private IgniteClient client; + + /** Constructor. */ + public IgniteContainer() { + this(Network.newNetwork(), "node0", UUID.randomUUID().toString()); + } + + /** Constructor. */ + public IgniteContainer(Network net, String hostname, String nodeId) { + super(DOCKER_IMAGE_NAME); + + withEnv("CONFIG_URI", "file://" + CFG_PATH); + withEnv("IGNITE_QUIET", "false"); + withEnv("IGNITE_NODE_NAME", nodeId); + withEnv("TZ", ZoneId.systemDefault().toString()); + //withEnv("JVM_OPTS", String.format("-Xmx%s", heapSize)); + + withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); + withCopyFileToContainer(forClasspathResource("docker/run-wrapper.sh"), WRAPPER_SCRIPT); + withCopyToContainer(MountableFile.forHostPath(LOCAL_TARGET_LIBS_DIR), TARGET_LIBS_DIR.getAbsolutePath()); + withNetwork(net); + withNetworkAliases(hostname); + withExposedPorts(THIN_CLIENT_PORT); + + withCommand("sh", "-c", "chmod +x " + WRAPPER_SCRIPT + " && exec " + WRAPPER_SCRIPT); + + waitingFor(Wait.forLogMessage(".*Node started.*", 1) + .withStartupTimeout(Duration.ofSeconds(60))); + } + + /** @return Thin client instance. */ + public IgniteClient client() { + if (client == null) + client = Ignition.startClient(clientConfig()); + + return client; + } + + /** */ + public void activateCluster() { + execControl("--set-state", "ACTIVE", "--yes"); + + try { + waitForCondition(() -> { + String out = execControl("--state"); + + Matcher matcher = CLUSTER_STATE_PATTERN.matcher(out); + + if (matcher.find()) + return ClusterState.valueOf(matcher.group(1)) == ClusterState.ACTIVE; + + return false; + }, 30_000); + } + catch (IgniteInterruptedCheckedException e) { + throw new IgniteException(e); + } + } + + /** + * 1. Stop ignite node. + * 2. Remove current libs dir. + * 3. Upgrade JAR's (copy libs from target dir). + * 4. Start ignite node. + */ + public void upgrade() throws Exception { + LOGGER.info(">>> Upgrade container {}", getContainerName()); + + closeClient(); + + exec("Failed to kill Ignite process", "kill", "-INT", "1"); + + for (int i = 0; i < 30; i++) { + ExecResult res = execInContainer("pgrep", "-f", "org.apache.ignite.startup.cmdline.CommandLineStartup"); + + if (res.getExitCode() == 1) + break; + + U.sleep(1_000); + } + + exec("Failed to remove old libs", "sh", "-c", "rm -rf " + LIBS_DIR + "/*"); + + exec("Failed to copy new libs", "sh", "-c", "cp -r " + TARGET_LIBS_DIR + "/* " + LIBS_DIR + "/"); + + execInContainer("sh", "-c", WORK_DIR + "run.sh &"); + + waitForCondition(() -> { + try { + return client().cluster().nodes().size() == 3; + } + catch (Exception e) { + return false; + } + }, 30_000); + } + + /** */ + public RollingUpgradeStatus rollingUpgradeStatus() { + String out = execControl("--rolling-upgrade", "status"); + + LOGGER.info(">>> Rolling upgrade status: {}", out); + + Matcher matcher = RU_STATUS_PATTERN.matcher(out); + + if (matcher.find()) + return RollingUpgradeStatus.valueOf(matcher.group(1).toUpperCase()); + + throw new IllegalStateException("Failed to parse rolling upgrade status from output:\n" + out); + } + + /** */ + public void rollingUpgradeEnable(String targetVer) { + execControl("--rolling-upgrade", "enable", targetVer, "--yes"); + } + + /** */ + public void rollingUpgradeDisable() { + execControl("--rolling-upgrade", "disable"); + } + + /** */ + public int nodesCountForVersion(String targetVer) { + String out = execControl("--rolling-upgrade", "status"); + + // Match the version block + Pattern verPattern = Pattern.compile( + "Version\\s+" + Pattern.quote(targetVer) + ".*?:\\s*\n" + + "((?:\\s*Node\\[.*?\\]\\s*\n)*)", + Pattern.DOTALL + ); + + Matcher verMatcher = verPattern.matcher(out); + + if (!verMatcher.find()) + return 0; + + String nodesBlock = verMatcher.group(1); + + // Count Node entries + Pattern nodePattern = Pattern.compile("^\\s*Node\\[.*?\\]", Pattern.MULTILINE); + Matcher nodeMatcher = nodePattern.matcher(nodesBlock); + + int cnt = 0; + + while (nodeMatcher.find()) + cnt++; + + return cnt; + } + + /** {@inheritDoc} */ + @Override protected void containerIsStopping(InspectContainerResponse containerInfo) { + closeClient(); + } + + /** */ + private String execControl(String... cmd) { + String[] fullCmd = new String[cmd.length + 2]; + + fullCmd[0] = WORK_DIR + "bin/control.sh"; + fullCmd[1] = ENABLE_EXPERIMENTAL_FLAG; + + System.arraycopy(cmd, 0, fullCmd, 2, cmd.length); + + ExecResult result; + + try { + LOGGER.info("Running command: {}", Arrays.toString(fullCmd).replace(", ", " ")); + + result = execInContainer(fullCmd); + } + catch (IOException | InterruptedException e) { + throw new IgniteException(e); + } + + if (result.getExitCode() != 0) + throw new IllegalStateException(result.toString()); + + return result.getStdout(); + } + + /** */ + private ExecResult exec(String errMsg, String... cmd) { + try { + ExecResult res = execInContainer(cmd); + + if (res.getExitCode() != 0) + throw new IllegalStateException(errMsg + ": " + res.getStderr()); + + return res; + } + catch (IOException | InterruptedException e) { + throw new IgniteException(e); + } + } + + /** */ + private void closeClient() { + if (client != null) { + client.close(); + + client = null; + } + } + + /** */ + private ClientConfiguration clientConfig() { + return new ClientConfiguration() + .setAddresses("127.0.0.1:" + getMappedPort(THIN_CLIENT_PORT)) + .setRequestTimeout(30_000); + } + + /** */ + public enum RollingUpgradeStatus { + /** */ + ENABLED, + + /** */ + DISABLED + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java new file mode 100644 index 0000000000000..d531847316369 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java @@ -0,0 +1,90 @@ +/* + * 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.testcontainers; + +import java.util.List; +import org.apache.ignite.cache.CacheAtomicityMode; +import org.apache.ignite.client.ClientCache; +import org.apache.ignite.client.ClientCacheConfiguration; +import org.junit.Test; + +import static org.apache.ignite.testcontainers.IgniteContainer.RollingUpgradeStatus.DISABLED; +import static org.apache.ignite.testcontainers.IgniteContainer.RollingUpgradeStatus.ENABLED; +import static org.junit.Assert.assertEquals; + +/** Smoke test for rolling upgrade with persistence. */ +public class IgniteRebalanceOnUpgradeTest { + /** Node IDs. */ + private static final List NODE_IDS = List.of( + "ad26bff6-5ff5-49f1-9a61-425a827953ed", + "c1099d16-e7d7-49f4-925c-53329286c444", + "7b880b69-8a9e-4b84-b555-250d365e2e67" + ); + + /** Target version for RU. */ + private static final String TARGET_VER = "2.18.1"; + + /** */ + private static final String CACHE_NAME = "ru-test-cache"; + + /** */ + @Test + public void testRollingUpgrade() throws Exception { + try (IgniteClusterContainer cluster = new IgniteClusterContainer(NODE_IDS)) { + cluster.start(); + + IgniteContainer node = cluster.firstNode(); + + node.activateCluster(); + + ClientCacheConfiguration cfg = new ClientCacheConfiguration() + .setName(CACHE_NAME) + .setBackups(1) + .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL); + + ClientCache cache = node.client().createCache(cfg); + + for (int i = 0; i < 1000; i++) + cache.put(i, i); + + assertEquals(DISABLED, node.rollingUpgradeStatus()); + + node.rollingUpgradeEnable(TARGET_VER); + + assertEquals(ENABLED, node.rollingUpgradeStatus()); + + for (IgniteContainer container : cluster.nodes()) + container.upgrade(); + + assertEquals(NODE_IDS.size(), node.nodesCountForVersion(TARGET_VER)); + + node.rollingUpgradeDisable(); + + assertEquals(DISABLED, node.rollingUpgradeStatus()); + + ClientCache targetCache = node.client().getOrCreateCache(CACHE_NAME); + + for (int i = 0; i < 1000; i++) + assertEquals("Data mismatch after upgrade at key: " + i, i, (int)targetCache.get(i)); + + targetCache.put(1001, 1001); + + assertEquals(1001, (int)targetCache.get(1001)); + } + } +} diff --git a/modules/core/src/test/resources/docker/run-wrapper.sh b/modules/core/src/test/resources/docker/run-wrapper.sh new file mode 100644 index 0000000000000..41bb74cba7d28 --- /dev/null +++ b/modules/core/src/test/resources/docker/run-wrapper.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# Wrapper script for starting Ignite that doesn't die on SIGINT +IGNITE_SCRIPT="/opt/ignite/apache-ignite/run.sh" + +# Function to stop Java process +stop_ignite() { + echo "Received SIGINT/SIGTERM, stopping Ignite..." + pkill -f "org.apache.ignite.startup.cmdline.CommandLineStartup" + + if [ -n "$IGNITE_PID" ]; then + wait $IGNITE_PID 2>/dev/null + fi + echo "Ignite stopped." +} + +# Catch SIGTERM and SIGINT, but don't exit — only stop the Java process +trap 'stop_ignite' TERM INT + +# Start Ignite in the background +$IGNITE_SCRIPT & +IGNITE_PID=$! + +# Wait for Java process to be alive +wait $IGNITE_PID + +# If Java process died on its own, just wait for the next start +while true; do + sleep 1 +done diff --git a/modules/core/src/test/resources/docker/test-config.xml b/modules/core/src/test/resources/docker/test-config.xml new file mode 100644 index 0000000000000..ce6f034dbdd5d --- /dev/null +++ b/modules/core/src/test/resources/docker/test-config.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/parent/pom.xml b/parent/pom.xml index ac68955a0d172..c28aea123f4c5 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -110,6 +110,7 @@ 5.3.39 3.5.4 10.0.27 + 2.0.5 0.8.3 3.9.5 1.5.7-8 From 1f27f57b4e8efca59dab0043346fc6e207edad37 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Mon, 1 Jun 2026 13:30:36 +0500 Subject: [PATCH 2/3] refactoring --- modules/core/pom.xml | 7 - .../IgniteClusterContainer.java | 38 +++- .../testcontainers/IgniteContainer.java | 167 +++++------------- .../IgniteRebalanceOnUpgradeTest.java | 66 ++++++- .../IgniteRollingUpgradeDockerTestSuite.java | 30 ++++ .../src/test/resources/docker/run-wrapper.sh | 30 ---- 6 files changed, 164 insertions(+), 174 deletions(-) create mode 100644 modules/core/src/test/java/org/apache/ignite/testsuites/IgniteRollingUpgradeDockerTestSuite.java delete mode 100644 modules/core/src/test/resources/docker/run-wrapper.sh diff --git a/modules/core/pom.xml b/modules/core/pom.xml index 5c2f906a91edb..b7dd12064fca4 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -244,13 +244,6 @@ ${testcontainers.version} test - - - org.slf4j - slf4j-simple - 2.0.12 - test - diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java index 50c23b9f0995f..4b2802208cde2 100644 --- a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java @@ -18,14 +18,17 @@ package org.apache.ignite.testcontainers; import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.ListIterator; import org.testcontainers.containers.Network; import org.testcontainers.lifecycle.Startable; import org.testcontainers.lifecycle.Startables; /** Ignite cluster container. */ public class IgniteClusterContainer implements Startable { + /** Source version. */ + private final String ver; + /** Containers. */ private final List containers; @@ -33,13 +36,14 @@ public class IgniteClusterContainer implements Startable { private final Network net = Network.newNetwork(); /** @param nodeIds Node ids. */ - public IgniteClusterContainer(List nodeIds) { + public IgniteClusterContainer(String ver, List nodeIds) { + this.ver = ver; containers = new ArrayList<>(nodeIds.size()); for (int i = 0; i < nodeIds.size(); i++) { String hostname = "node" + (1 + i); - IgniteContainer ignite = new IgniteContainer(net, hostname, nodeIds.get(i)); + IgniteContainer ignite = new IgniteContainer(ver, net, hostname, nodeIds.get(i)); containers.add(ignite); } @@ -54,15 +58,35 @@ public IgniteClusterContainer(List nodeIds) { @Override public void stop() { for (IgniteContainer container : containers) container.stop(); - } - /** @return All started nodes. */ - public List nodes() { - return Collections.unmodifiableList(containers); + net.close(); } /** @return First started node in cluster. */ public IgniteContainer firstNode() { return containers.get(0); } + + /** Rolling upgrade cluster. + * + * @param ver Target version. + */ + public void upgrade(String ver) { + if (this.ver.equals(ver)) + throw new IllegalArgumentException("Target version matches the current version."); + + ListIterator it = containers.listIterator(); + + while (it.hasNext()) { + IgniteContainer oldNode = it.next(); + + oldNode.stop(); + + IgniteContainer newNode = new IgniteContainer(ver, net, oldNode.hostname(), oldNode.nodeId()); + + newNode.start(); + + it.set(newNode); + } + } } diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java index c8fb515c92cd4..b22f3bdb162f9 100644 --- a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java @@ -17,55 +17,46 @@ package org.apache.ignite.testcontainers; -import java.io.File; import java.io.IOException; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; -import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; -import com.github.dockerjava.api.command.InspectContainerResponse; import org.apache.ignite.IgniteException; -import org.apache.ignite.Ignition; -import org.apache.ignite.client.IgniteClient; import org.apache.ignite.cluster.ClusterState; -import org.apache.ignite.configuration.ClientConfiguration; import org.apache.ignite.internal.IgniteInterruptedCheckedException; -import org.apache.ignite.internal.util.typedef.internal.U; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.MountableFile; import static org.apache.ignite.testframework.GridTestUtils.waitForCondition; import static org.testcontainers.utility.MountableFile.forClasspathResource; /** Ignite container. */ public class IgniteContainer extends GenericContainer { - /** Logger. */ - private static final Logger LOGGER = LoggerFactory.getLogger(IgniteContainer.class); + /** Property for local work directory. */ + static final String LOCAL_WORK_DIR_PROP = "local.work.dir"; - /** Docker image name. */ - private static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse("apacheignite/ignite:2.18.0"); + /** Local work directory. */ + static final String LOCAL_WORK_DIR_PATH = System.getProperty(LOCAL_WORK_DIR_PROP, + System.getProperty("user.home") + "/test-ignite-work"); - /** Work directory. */ - private static final String WORK_DIR = "/opt/ignite/apache-ignite/"; - - /** Libs directory in container. */ - private static final File LIBS_DIR = new File(WORK_DIR + "libs"); + /** Logger. */ + private static final Logger LOGGER = LoggerFactory.getLogger(IgniteContainer.class); - /** Target libs directory in container. */ - private static final File TARGET_LIBS_DIR = new File("/opt/ignite/target-libs"); + /** Ignite root directory in container. */ + private static final String ROOT_DIR_PATH = "/opt/ignite/apache-ignite/"; - /** Local target libs directory to copy in container. */ - private static final String LOCAL_TARGET_LIBS_DIR = System.getProperty("local.target.libs", "/tmp/target-libs"); + /** Ignite work directory in container. */ + private static final String WORK_DIR_PATH = ROOT_DIR_PATH + "work"; - /** */ - private static final String CFG_PATH = WORK_DIR + "config/test-config.xml"; + /** Config path in container. */ + private static final String CFG_PATH = ROOT_DIR_PATH + "config/test-config.xml"; /** */ private static final String ENABLE_EXPERIMENTAL_FLAG = "--enable-experimental"; @@ -76,52 +67,51 @@ public class IgniteContainer extends GenericContainer { /** */ private static final Pattern RU_STATUS_PATTERN = Pattern.compile("Rolling upgrade status: (enabled|disabled)"); - /** */ - private static final String WRAPPER_SCRIPT = "/opt/ignite/run-wrapper.sh"; - /** Default thin client port. */ private static final int THIN_CLIENT_PORT = 10800; - /** Ignite thin client. */ - private IgniteClient client; + /** Hostname. */ + private final String hostname; - /** Constructor. */ - public IgniteContainer() { - this(Network.newNetwork(), "node0", UUID.randomUUID().toString()); - } + /** Node ID. */ + private final String nodeId; /** Constructor. */ - public IgniteContainer(Network net, String hostname, String nodeId) { - super(DOCKER_IMAGE_NAME); + public IgniteContainer(String ver, Network net, String hostname, String nodeId) { + super(DockerImageName.parse("apacheignite/ignite:" + ver)); + + this.hostname = hostname; + this.nodeId = nodeId; withEnv("CONFIG_URI", "file://" + CFG_PATH); withEnv("IGNITE_QUIET", "false"); withEnv("IGNITE_NODE_NAME", nodeId); + withEnv("IGNITE_WORK_DIR", WORK_DIR_PATH + "/" + hostname); withEnv("TZ", ZoneId.systemDefault().toString()); - //withEnv("JVM_OPTS", String.format("-Xmx%s", heapSize)); + + withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); - withCopyFileToContainer(forClasspathResource("docker/run-wrapper.sh"), WRAPPER_SCRIPT); - withCopyToContainer(MountableFile.forHostPath(LOCAL_TARGET_LIBS_DIR), TARGET_LIBS_DIR.getAbsolutePath()); + withNetwork(net); withNetworkAliases(hostname); withExposedPorts(THIN_CLIENT_PORT); - withCommand("sh", "-c", "chmod +x " + WRAPPER_SCRIPT + " && exec " + WRAPPER_SCRIPT); - waitingFor(Wait.forLogMessage(".*Node started.*", 1) .withStartupTimeout(Duration.ofSeconds(60))); } - /** @return Thin client instance. */ - public IgniteClient client() { - if (client == null) - client = Ignition.startClient(clientConfig()); + /** @return Hostname. */ + public String hostname() { + return hostname; + } - return client; + /** @return Node ID. */ + public String nodeId() { + return nodeId; } - /** */ + /** Activate cluster. */ public void activateCluster() { execControl("--set-state", "ACTIVE", "--yes"); @@ -142,45 +132,7 @@ public void activateCluster() { } } - /** - * 1. Stop ignite node. - * 2. Remove current libs dir. - * 3. Upgrade JAR's (copy libs from target dir). - * 4. Start ignite node. - */ - public void upgrade() throws Exception { - LOGGER.info(">>> Upgrade container {}", getContainerName()); - - closeClient(); - - exec("Failed to kill Ignite process", "kill", "-INT", "1"); - - for (int i = 0; i < 30; i++) { - ExecResult res = execInContainer("pgrep", "-f", "org.apache.ignite.startup.cmdline.CommandLineStartup"); - - if (res.getExitCode() == 1) - break; - - U.sleep(1_000); - } - - exec("Failed to remove old libs", "sh", "-c", "rm -rf " + LIBS_DIR + "/*"); - - exec("Failed to copy new libs", "sh", "-c", "cp -r " + TARGET_LIBS_DIR + "/* " + LIBS_DIR + "/"); - - execInContainer("sh", "-c", WORK_DIR + "run.sh &"); - - waitForCondition(() -> { - try { - return client().cluster().nodes().size() == 3; - } - catch (Exception e) { - return false; - } - }, 30_000); - } - - /** */ + /** @return Rolling upgrade status. */ public RollingUpgradeStatus rollingUpgradeStatus() { String out = execControl("--rolling-upgrade", "status"); @@ -194,17 +146,17 @@ public RollingUpgradeStatus rollingUpgradeStatus() { throw new IllegalStateException("Failed to parse rolling upgrade status from output:\n" + out); } - /** */ + /** Enable rolling upgrade. */ public void rollingUpgradeEnable(String targetVer) { execControl("--rolling-upgrade", "enable", targetVer, "--yes"); } - /** */ + /** Disable rolling upgrade. */ public void rollingUpgradeDisable() { execControl("--rolling-upgrade", "disable"); } - /** */ + /** @return Number of cluster nodes for given release version. */ public int nodesCountForVersion(String targetVer) { String out = execControl("--rolling-upgrade", "status"); @@ -234,16 +186,16 @@ public int nodesCountForVersion(String targetVer) { return cnt; } - /** {@inheritDoc} */ - @Override protected void containerIsStopping(InspectContainerResponse containerInfo) { - closeClient(); + /** @return Client address. */ + public String clientAddress() { + return getHost() + ":" + getMappedPort(THIN_CLIENT_PORT); } /** */ private String execControl(String... cmd) { String[] fullCmd = new String[cmd.length + 2]; - fullCmd[0] = WORK_DIR + "bin/control.sh"; + fullCmd[0] = ROOT_DIR_PATH + "bin/control.sh"; fullCmd[1] = ENABLE_EXPERIMENTAL_FLAG; System.arraycopy(cmd, 0, fullCmd, 2, cmd.length); @@ -265,38 +217,7 @@ private String execControl(String... cmd) { return result.getStdout(); } - /** */ - private ExecResult exec(String errMsg, String... cmd) { - try { - ExecResult res = execInContainer(cmd); - - if (res.getExitCode() != 0) - throw new IllegalStateException(errMsg + ": " + res.getStderr()); - - return res; - } - catch (IOException | InterruptedException e) { - throw new IgniteException(e); - } - } - - /** */ - private void closeClient() { - if (client != null) { - client.close(); - - client = null; - } - } - - /** */ - private ClientConfiguration clientConfig() { - return new ClientConfiguration() - .setAddresses("127.0.0.1:" + getMappedPort(THIN_CLIENT_PORT)) - .setRequestTimeout(30_000); - } - - /** */ + /** Rolling upgrade status. */ public enum RollingUpgradeStatus { /** */ ENABLED, diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java index d531847316369..4617b8533f934 100644 --- a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java @@ -17,12 +17,20 @@ package org.apache.ignite.testcontainers; +import java.io.File; import java.util.List; +import org.apache.ignite.Ignition; import org.apache.ignite.cache.CacheAtomicityMode; import org.apache.ignite.client.ClientCache; import org.apache.ignite.client.ClientCacheConfiguration; +import org.apache.ignite.client.IgniteClient; +import org.apache.ignite.configuration.ClientConfiguration; +import org.apache.ignite.internal.util.typedef.internal.U; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; +import static org.apache.ignite.testcontainers.IgniteContainer.LOCAL_WORK_DIR_PATH; import static org.apache.ignite.testcontainers.IgniteContainer.RollingUpgradeStatus.DISABLED; import static org.apache.ignite.testcontainers.IgniteContainer.RollingUpgradeStatus.ENABLED; import static org.junit.Assert.assertEquals; @@ -36,16 +44,37 @@ public class IgniteRebalanceOnUpgradeTest { "7b880b69-8a9e-4b84-b555-250d365e2e67" ); + /** Source version. */ + private static final String SOURCE_VER = "2.18.0"; + /** Target version for RU. */ private static final String TARGET_VER = "2.18.1"; - /** */ + /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; + /** Local work directory. */ + private static final File LOCAL_WORK_DIR = new File(LOCAL_WORK_DIR_PATH); + + /** Thin client. */ + private IgniteClient client; + /** */ + @BeforeClass + public static void beforeClass() { + U.delete(LOCAL_WORK_DIR); + } + + /** */ + @AfterClass + public static void afterClass() { + U.delete(LOCAL_WORK_DIR); + } + + /** Basic RU test. */ @Test - public void testRollingUpgrade() throws Exception { - try (IgniteClusterContainer cluster = new IgniteClusterContainer(NODE_IDS)) { + public void testRollingUpgrade() { + try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_VER, NODE_IDS)) { cluster.start(); IgniteContainer node = cluster.firstNode(); @@ -57,7 +86,7 @@ public void testRollingUpgrade() throws Exception { .setBackups(1) .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL); - ClientCache cache = node.client().createCache(cfg); + ClientCache cache = client(node.clientAddress()).createCache(cfg); for (int i = 0; i < 1000; i++) cache.put(i, i); @@ -68,8 +97,11 @@ public void testRollingUpgrade() throws Exception { assertEquals(ENABLED, node.rollingUpgradeStatus()); - for (IgniteContainer container : cluster.nodes()) - container.upgrade(); + closeClient(); + + cluster.upgrade(TARGET_VER); + + node = cluster.firstNode(); assertEquals(NODE_IDS.size(), node.nodesCountForVersion(TARGET_VER)); @@ -77,7 +109,7 @@ public void testRollingUpgrade() throws Exception { assertEquals(DISABLED, node.rollingUpgradeStatus()); - ClientCache targetCache = node.client().getOrCreateCache(CACHE_NAME); + ClientCache targetCache = client(node.clientAddress()).getOrCreateCache(CACHE_NAME); for (int i = 0; i < 1000; i++) assertEquals("Data mismatch after upgrade at key: " + i, i, (int)targetCache.get(i)); @@ -86,5 +118,25 @@ public void testRollingUpgrade() throws Exception { assertEquals(1001, (int)targetCache.get(1001)); } + finally { + closeClient(); + } + } + + /** */ + private IgniteClient client(String addr) { + if (client == null) + client = Ignition.startClient(new ClientConfiguration().setAddresses(addr)); + + return client; + } + + /** */ + private void closeClient() { + if (client != null) { + client.close(); + + client = null; + } } } diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteRollingUpgradeDockerTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteRollingUpgradeDockerTestSuite.java new file mode 100644 index 0000000000000..a542deaf7929d --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteRollingUpgradeDockerTestSuite.java @@ -0,0 +1,30 @@ +/* + * 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.testsuites; + +import org.apache.ignite.testcontainers.IgniteRebalanceOnUpgradeTest; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** Contains RU tests based on testcontainers. */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + IgniteRebalanceOnUpgradeTest.class +}) +public class IgniteRollingUpgradeDockerTestSuite { +} diff --git a/modules/core/src/test/resources/docker/run-wrapper.sh b/modules/core/src/test/resources/docker/run-wrapper.sh deleted file mode 100644 index 41bb74cba7d28..0000000000000 --- a/modules/core/src/test/resources/docker/run-wrapper.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -# Wrapper script for starting Ignite that doesn't die on SIGINT -IGNITE_SCRIPT="/opt/ignite/apache-ignite/run.sh" - -# Function to stop Java process -stop_ignite() { - echo "Received SIGINT/SIGTERM, stopping Ignite..." - pkill -f "org.apache.ignite.startup.cmdline.CommandLineStartup" - - if [ -n "$IGNITE_PID" ]; then - wait $IGNITE_PID 2>/dev/null - fi - echo "Ignite stopped." -} - -# Catch SIGTERM and SIGINT, but don't exit — only stop the Java process -trap 'stop_ignite' TERM INT - -# Start Ignite in the background -$IGNITE_SCRIPT & -IGNITE_PID=$! - -# Wait for Java process to be alive -wait $IGNITE_PID - -# If Java process died on its own, just wait for the next start -while true; do - sleep 1 -done From 3b203f91d0f47bb3f8ad3c39fc32df171a25dd96 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Mon, 1 Jun 2026 18:41:54 +0500 Subject: [PATCH 3/3] add logger impl --- modules/core/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/core/pom.xml b/modules/core/pom.xml index b7dd12064fca4..5c2f906a91edb 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -244,6 +244,13 @@ ${testcontainers.version} test + + + org.slf4j + slf4j-simple + 2.0.12 + test +