From b17ef7fce7fe577326e635abd7509d85bae2eaae Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Tue, 26 May 2026 15:40:39 +0500 Subject: [PATCH 01/28] 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 aebc94795a5f5..af237402b8917 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 164546afa1d78..a912b2b2aba82 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -110,6 +110,7 @@ 5.3.39 3.5.6 10.0.27 + 2.0.5 0.8.3 3.9.5 1.5.7-8 From a90e8c5597f6608006d69872ca1731dd51ebd196 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Mon, 1 Jun 2026 13:30:36 +0500 Subject: [PATCH 02/28] 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 af237402b8917..86eba2dd979ce 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 132b1d54cbd5b59ac2235adc9ed40b6556779964 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Mon, 1 Jun 2026 18:41:54 +0500 Subject: [PATCH 03/28] 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 86eba2dd979ce..af237402b8917 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 + From fceb8047a8f00539221b13b5835ec6efe68ec1df Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Mon, 8 Jun 2026 14:40:08 +0500 Subject: [PATCH 04/28] move to compatibility --- modules/compatibility/pom.xml | 14 ++++++++++++++ .../testcontainers/IgniteClusterContainer.java | 2 +- .../testcontainers/IgniteContainer.java | 2 +- .../IgniteRebalanceOnUpgradeTest.java | 8 ++++---- .../IgniteRollingUpgradeDockerTestSuite.java | 4 ++-- .../src/test/resources/docker/test-config.xml | 0 modules/core/pom.xml | 14 -------------- 7 files changed, 22 insertions(+), 22 deletions(-) rename modules/{core/src/test/java/org/apache/ignite => compatibility/src/test/java/org/apache/ignite/compatibility}/testcontainers/IgniteClusterContainer.java (98%) rename modules/{core/src/test/java/org/apache/ignite => compatibility/src/test/java/org/apache/ignite/compatibility}/testcontainers/IgniteContainer.java (99%) rename modules/{core/src/test/java/org/apache/ignite => compatibility/src/test/java/org/apache/ignite/compatibility}/testcontainers/IgniteRebalanceOnUpgradeTest.java (92%) rename modules/{core/src/test/java/org/apache/ignite => compatibility/src/test/java/org/apache/ignite/compatibility}/testsuites/IgniteRollingUpgradeDockerTestSuite.java (88%) rename modules/{core => compatibility}/src/test/resources/docker/test-config.xml (100%) diff --git a/modules/compatibility/pom.xml b/modules/compatibility/pom.xml index 232cd0b635f2b..fee5e8b1f8393 100644 --- a/modules/compatibility/pom.xml +++ b/modules/compatibility/pom.xml @@ -123,6 +123,20 @@ 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/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteClusterContainer.java similarity index 98% rename from modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java rename to modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteClusterContainer.java index 4b2802208cde2..d62cf760d2594 100644 --- a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteClusterContainer.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.ignite.testcontainers; +package org.apache.ignite.compatibility.testcontainers; import java.util.ArrayList; import java.util.List; diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteContainer.java similarity index 99% rename from modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java rename to modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteContainer.java index b22f3bdb162f9..530001be4edad 100644 --- a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteContainer.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.ignite.testcontainers; +package org.apache.ignite.compatibility.testcontainers; import java.io.IOException; import java.time.Duration; diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteRebalanceOnUpgradeTest.java similarity index 92% rename from modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java rename to modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteRebalanceOnUpgradeTest.java index 4617b8533f934..f194d8435996e 100644 --- a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteRebalanceOnUpgradeTest.java @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.apache.ignite.testcontainers; +package org.apache.ignite.compatibility.testcontainers; import java.io.File; import java.util.List; @@ -30,9 +30,9 @@ 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.apache.ignite.compatibility.testcontainers.IgniteContainer.LOCAL_WORK_DIR_PATH; +import static org.apache.ignite.compatibility.testcontainers.IgniteContainer.RollingUpgradeStatus.DISABLED; +import static org.apache.ignite.compatibility.testcontainers.IgniteContainer.RollingUpgradeStatus.ENABLED; import static org.junit.Assert.assertEquals; /** Smoke test for rolling upgrade with persistence. */ diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteRollingUpgradeDockerTestSuite.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteRollingUpgradeDockerTestSuite.java similarity index 88% rename from modules/core/src/test/java/org/apache/ignite/testsuites/IgniteRollingUpgradeDockerTestSuite.java rename to modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteRollingUpgradeDockerTestSuite.java index a542deaf7929d..41f4e68ecade9 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteRollingUpgradeDockerTestSuite.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteRollingUpgradeDockerTestSuite.java @@ -15,9 +15,9 @@ * limitations under the License. */ -package org.apache.ignite.testsuites; +package org.apache.ignite.compatibility.testsuites; -import org.apache.ignite.testcontainers.IgniteRebalanceOnUpgradeTest; +import org.apache.ignite.compatibility.testcontainers.IgniteRebalanceOnUpgradeTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; diff --git a/modules/core/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml similarity index 100% rename from modules/core/src/test/resources/docker/test-config.xml rename to modules/compatibility/src/test/resources/docker/test-config.xml diff --git a/modules/core/pom.xml b/modules/core/pom.xml index af237402b8917..aebc94795a5f5 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -237,20 +237,6 @@ 0.23.0 test - - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - - org.slf4j - slf4j-simple - 2.0.12 - test - From fffb2f776191af438f798bcf0177663941321f3c Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Mon, 8 Jun 2026 14:53:07 +0500 Subject: [PATCH 05/28] copy plugin to libs --- .../ru/IgniteRebalanceOnUpgradeTest.java | 253 ++++++++++++++++++ .../IgniteRebalanceOnUpgradeTest.java | 142 ---------- .../IgniteClusterContainer.java | 35 +-- .../testcontainers/IgniteContainer.java | 129 ++++----- .../IgniteRollingUpgradeDockerTestSuite.java | 2 +- .../src/test/resources/docker/test-config.xml | 19 +- 6 files changed, 337 insertions(+), 243 deletions(-) create mode 100644 modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java delete mode 100644 modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteRebalanceOnUpgradeTest.java rename modules/compatibility/src/test/java/org/apache/ignite/compatibility/{ => testframework}/testcontainers/IgniteClusterContainer.java (72%) rename modules/compatibility/src/test/java/org/apache/ignite/compatibility/{ => testframework}/testcontainers/IgniteContainer.java (70%) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java new file mode 100644 index 0000000000000..d8539b2ad6886 --- /dev/null +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -0,0 +1,253 @@ +/* + * 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.compatibility.ru; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.ignite.IgniteCache; +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.compatibility.testframework.junits.IgniteCompatibilityAbstractTest; +import org.apache.ignite.compatibility.testframework.testcontainers.IgniteClusterContainer; +import org.apache.ignite.compatibility.testframework.testcontainers.IgniteContainer; +import org.apache.ignite.configuration.ClientConfiguration; +import org.apache.ignite.configuration.DataRegionConfiguration; +import org.apache.ignite.configuration.DataStorageConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.util.typedef.internal.U; +import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; +import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; +import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNode; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.apache.ignite.compatibility.testframework.testcontainers.IgniteContainer.LOCAL_WORK_DIR_PATH; +import static org.apache.ignite.testframework.GridTestUtils.DFLT_TEST_TIMEOUT; +import static org.apache.ignite.testframework.GridTestUtils.waitForCondition; + +/** Smoke test for rolling upgrade with persistence. */ +public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { + /** 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" + ); + + /** Source version. */ + private static final String SOURCE_VER = "2.18.0"; + + /** 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; + + /** */ + private final List nodes = new ArrayList<>(); + + /** */ + private final Map addrs = new HashMap<>(); + + /** */ + public final TcpDiscoveryVmIpFinder ipFinder = new TcpDiscoveryVmIpFinder(); + + /** */ + @BeforeClass + public static void beforeClass() { + U.delete(LOCAL_WORK_DIR); + + // Установка свойства для предпочтения IPv4 адресов + System.setProperty("java.net.preferIPv4Stack", "true"); + + // Проверка, что свойство установлено корректно + String preferIPv4 = System.getProperty("java.net.preferIPv4Stack"); + if (!"true".equals(preferIPv4)) { + throw new IllegalStateException("Failed to set java.net.preferIPv4Stack property to true"); + } + + // Также устанавливаем свойство для принудительного использования IPv4 + System.setProperty("java.net.preferIPv6Addresses", "false"); + } + + /** */ + @AfterClass + public static void afterClass() { + U.delete(LOCAL_WORK_DIR); + } + + /** {@inheritDoc} */ + @Override protected boolean isMultiJvm() { + return false; + } + + /** {@inheritDoc} */ + @Override protected long getTestTimeout() { + return super.getTestTimeout() * 2; + } + + /** Basic RU test. */ + @Test + public void testRollingUpgrade() throws Exception { + try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_VER, NODE_IDS)) { + cluster.start(); + + for (IgniteContainer container : cluster.containers()) { + // Явно используем IPv4 адреса + String ipv4Addr = "127.0.0.1:" + container.getMappedPort(47500); + addrs.put(container.nodeId(), ipv4Addr); + } + + System.out.println(">>> Addresses=" + addrs); + + IgniteContainer node = cluster.firstNode(); + + node.activateCluster(); + + ClientCacheConfiguration cfg = new ClientCacheConfiguration() + .setName(CACHE_NAME) + .setBackups(1) + .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL); + + ClientCache cache = client(node.clientAddress()).createCache(cfg); + + for (int i = 0; i < 1000; i++) + cache.put(i, i); + + closeClient(); + + upgradeCluster(cluster); + + IgniteCache targetCache = nodes.get(0).cache(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)); + } + finally { + closeClient(); + } + } + + /** */ + private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception { + for (IgniteContainer container : srcCluster.containers()) { + System.out.println(">>> Upgrade " + container.nodeId()); + + container.stop(); + + addrs.remove(container.nodeId()); + + System.out.println(">>> CONNECT TO=" + addrs.values()); + + // Создаем список адресов с явным указанием IPv4 + Collection ipv4Addrs = new ArrayList<>(); + for (String addr : addrs.values()) { + // Преобразуем адреса в формат IPv4, если они указаны как IPv6 + if (addr.contains("/")) { + // Если адрес содержит IPv6 формат, преобразуем его в IPv4 + int colonIndex = addr.lastIndexOf(':'); + if (colonIndex > 0) { + String hostPart = addr.substring(0, colonIndex); + String portPart = addr.substring(colonIndex + 1); + // Заменяем IPv6 localhost на IPv4 localhost + if (hostPart.equals("0:0:0:0:0:0:0:1") || hostPart.equals("[0:0:0:0:0:0:0:1]")) { + ipv4Addrs.add("127.0.0.1:" + portPart); + } else { + ipv4Addrs.add(addr); + } + } else { + ipv4Addrs.add(addr); + } + } else { + ipv4Addrs.add(addr); + } + } + + // Если список пустой, то не передаем никакие адреса для подключения + // Это позволяет ноде самой выбрать порт и быть доступной для других нод + IgniteEx ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), + ipv4Addrs)); + + waitForCondition(() -> NODE_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); + + // Обновляем адреса для новой ноды + addrs.put(container.nodeId(), "127.0.0.1:" + ((TcpDiscoveryNode)ignite.localNode()).discoveryPort()); + + nodes.add(ignite); + + System.out.println(">>> Upgrade -> addresses=" + addrs); + } + } + + /** */ + private IgniteConfiguration configuration(String nodeId, String workDir, Collection addrs) { + DataRegionConfiguration dataRegionCfg = new DataRegionConfiguration() + .setName("testRegion") + .setInitialSize(1024L * 1024 * 1024) + .setMaxSize(10L * 1024 * 1024 * 1024) + .setPersistenceEnabled(true); + + TcpDiscoverySpi discoverySpi = new TcpDiscoverySpi() + .setLocalAddress("127.0.0.1") + .setIpFinder(ipFinder.setAddresses(addrs)) + .setLocalPort(47500) + .setNetworkTimeout(10000) + .setAckTimeout(5000) + .setJoinTimeout(10000); + + return new IgniteConfiguration() + .setConsistentId(nodeId) + .setWorkDirectory(workDir) + .setDataStorageConfiguration(new DataStorageConfiguration().setDataRegionConfigurations(dataRegionCfg)) + .setDiscoverySpi(discoverySpi); + } + + /** */ + 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/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteRebalanceOnUpgradeTest.java deleted file mode 100644 index f194d8435996e..0000000000000 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteRebalanceOnUpgradeTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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.compatibility.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.compatibility.testcontainers.IgniteContainer.LOCAL_WORK_DIR_PATH; -import static org.apache.ignite.compatibility.testcontainers.IgniteContainer.RollingUpgradeStatus.DISABLED; -import static org.apache.ignite.compatibility.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" - ); - - /** 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() { - try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_VER, NODE_IDS)) { - cluster.start(); - - IgniteContainer node = cluster.firstNode(); - - node.activateCluster(); - - ClientCacheConfiguration cfg = new ClientCacheConfiguration() - .setName(CACHE_NAME) - .setBackups(1) - .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL); - - ClientCache cache = client(node.clientAddress()).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()); - - closeClient(); - - cluster.upgrade(TARGET_VER); - - node = cluster.firstNode(); - - assertEquals(NODE_IDS.size(), node.nodesCountForVersion(TARGET_VER)); - - node.rollingUpgradeDisable(); - - assertEquals(DISABLED, node.rollingUpgradeStatus()); - - 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)); - - targetCache.put(1001, 1001); - - 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/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteClusterContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java similarity index 72% rename from modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteClusterContainer.java rename to modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java index d62cf760d2594..04a4a41b32c58 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteClusterContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java @@ -15,20 +15,16 @@ * limitations under the License. */ -package org.apache.ignite.compatibility.testcontainers; +package org.apache.ignite.compatibility.testframework.testcontainers; import java.util.ArrayList; 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; @@ -37,7 +33,6 @@ public class IgniteClusterContainer implements Startable { /** @param nodeIds Node ids. */ public IgniteClusterContainer(String ver, List nodeIds) { - this.ver = ver; containers = new ArrayList<>(nodeIds.size()); for (int i = 0; i < nodeIds.size(); i++) { @@ -62,31 +57,13 @@ public IgniteClusterContainer(String ver, List nodeIds) { net.close(); } + /** */ + public List containers() { + return containers; + } + /** @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/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java similarity index 70% rename from modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteContainer.java rename to modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 530001be4edad..8941ecb904914 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -15,9 +15,12 @@ * limitations under the License. */ -package org.apache.ignite.compatibility.testcontainers; +package org.apache.ignite.compatibility.testframework.testcontainers; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; @@ -33,6 +36,7 @@ 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; @@ -43,7 +47,7 @@ public class IgniteContainer extends GenericContainer { static final String LOCAL_WORK_DIR_PROP = "local.work.dir"; /** Local work directory. */ - static final String LOCAL_WORK_DIR_PATH = System.getProperty(LOCAL_WORK_DIR_PROP, + public static final String LOCAL_WORK_DIR_PATH = System.getProperty(LOCAL_WORK_DIR_PROP, System.getProperty("user.home") + "/test-ignite-work"); /** Logger. */ @@ -64,9 +68,6 @@ public class IgniteContainer extends GenericContainer { /** */ 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)"); - /** Default thin client port. */ private static final int THIN_CLIENT_PORT = 10800; @@ -76,31 +77,71 @@ public class IgniteContainer extends GenericContainer { /** Node ID. */ private final String nodeId; + /** Path to work directory. */ + private final String workDirPath; + /** Constructor. */ public IgniteContainer(String ver, Network net, String hostname, String nodeId) { super(DockerImageName.parse("apacheignite/ignite:" + ver)); this.hostname = hostname; this.nodeId = nodeId; + workDirPath = WORK_DIR_PATH + "/" + hostname; withEnv("CONFIG_URI", "file://" + CFG_PATH); withEnv("IGNITE_QUIET", "false"); withEnv("IGNITE_NODE_NAME", nodeId); - withEnv("IGNITE_WORK_DIR", WORK_DIR_PATH + "/" + hostname); + withEnv("IGNITE_WORK_DIR", workDirPath); + withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); + // Copy compatibility JAR if it exists + copyCompatibilityJar(); + withNetwork(net); withNetworkAliases(hostname); - withExposedPorts(THIN_CLIENT_PORT); + withExposedPorts(THIN_CLIENT_PORT, 47100, 47500); waitingFor(Wait.forLogMessage(".*Node started.*", 1) .withStartupTimeout(Duration.ofSeconds(60))); } + /** */ + private void copyCompatibilityJar() { + String projectDir = System.getProperty("user.dir"); + Path compatTarget = Paths.get(projectDir + "/target"); + + System.out.println(">>> KEK=" + compatTarget); + + if (Files.exists(compatTarget)) { + try { + Files.walk(compatTarget) + .filter(p -> { + System.out.println(">>> P=" + p); + + return p.toString().endsWith("tests.jar") && p.toString().contains("ignite-compatibility"); + }) + .findFirst() + .ifPresent(jarPath -> { + System.out.println(">>> JAR PATH=" + jarPath); + + withCopyFileToContainer(MountableFile.forHostPath(jarPath), ROOT_DIR_PATH + "libs/test-compatability.jar"); + + LOGGER.info("Bound compatibility JAR: " + jarPath); + }); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + else + throw new IllegalStateException("Please run `mvn clean install -DskipTests -pl modules/compatibility -am` first"); + } + /** @return Hostname. */ public String hostname() { return hostname; @@ -111,6 +152,11 @@ public String nodeId() { return nodeId; } + /** */ + public String localWorkDirectory() { + return LOCAL_WORK_DIR_PATH + "/" + hostname; + } + /** Activate cluster. */ public void activateCluster() { execControl("--set-state", "ACTIVE", "--yes"); @@ -132,65 +178,17 @@ public void activateCluster() { } } - /** @return Rolling upgrade status. */ - 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); - } - - /** 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"); - - // 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; - } - /** @return Client address. */ public String clientAddress() { return getHost() + ":" + getMappedPort(THIN_CLIENT_PORT); } + /** */ + public String discoveryAddress() { + // Возвращаем IPv4 адрес явно + return "127.0.0.1:" + getMappedPort(47500); + } + /** */ private String execControl(String... cmd) { String[] fullCmd = new String[cmd.length + 2]; @@ -216,13 +214,4 @@ private String execControl(String... cmd) { return result.getStdout(); } - - /** Rolling upgrade status. */ - public enum RollingUpgradeStatus { - /** */ - ENABLED, - - /** */ - DISABLED - } } diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteRollingUpgradeDockerTestSuite.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteRollingUpgradeDockerTestSuite.java index 41f4e68ecade9..e826e5cbb7a48 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteRollingUpgradeDockerTestSuite.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testsuites/IgniteRollingUpgradeDockerTestSuite.java @@ -17,7 +17,7 @@ package org.apache.ignite.compatibility.testsuites; -import org.apache.ignite.compatibility.testcontainers.IgniteRebalanceOnUpgradeTest; +import org.apache.ignite.compatibility.ru.IgniteRebalanceOnUpgradeTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index ce6f034dbdd5d..cec24ab612dab 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -30,7 +30,7 @@ - + @@ -38,5 +38,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file From 6c9b72459cf6c2ec843cc3fc20f8258b7df441ce Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Tue, 9 Jun 2026 17:01:21 +0500 Subject: [PATCH 06/28] add sh script --- .../ru/IgniteRebalanceOnUpgradeTest.java | 44 +------- .../IgniteClusterContainer.java | 6 +- .../testcontainers/IgniteContainer.java | 48 +------- .../resources/docker/build_docker_image.sh | 103 ++++++++++++++++++ 4 files changed, 112 insertions(+), 89 deletions(-) create mode 100755 modules/compatibility/src/test/resources/docker/build_docker_image.sh diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index d8539b2ad6886..7972d8e5e8c3a 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -29,7 +29,6 @@ import org.apache.ignite.client.ClientCache; import org.apache.ignite.client.ClientCacheConfiguration; import org.apache.ignite.client.IgniteClient; -import org.apache.ignite.compatibility.testframework.junits.IgniteCompatibilityAbstractTest; import org.apache.ignite.compatibility.testframework.testcontainers.IgniteClusterContainer; import org.apache.ignite.compatibility.testframework.testcontainers.IgniteContainer; import org.apache.ignite.configuration.ClientConfiguration; @@ -59,8 +58,8 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { "7b880b69-8a9e-4b84-b555-250d365e2e67" ); - /** Source version. */ - private static final String SOURCE_VER = "2.18.0"; + /** Source commit hash. */ + private static final String SOURCE_COMMIT_HASH = "6b172a8b"; /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; @@ -88,12 +87,6 @@ public static void beforeClass() { // Установка свойства для предпочтения IPv4 адресов System.setProperty("java.net.preferIPv4Stack", "true"); - // Проверка, что свойство установлено корректно - String preferIPv4 = System.getProperty("java.net.preferIPv4Stack"); - if (!"true".equals(preferIPv4)) { - throw new IllegalStateException("Failed to set java.net.preferIPv4Stack property to true"); - } - // Также устанавливаем свойство для принудительного использования IPv4 System.setProperty("java.net.preferIPv6Addresses", "false"); } @@ -117,7 +110,7 @@ public static void afterClass() { /** Basic RU test. */ @Test public void testRollingUpgrade() throws Exception { - try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_VER, NODE_IDS)) { + try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_COMMIT_HASH, NODE_IDS)) { cluster.start(); for (IgniteContainer container : cluster.containers()) { @@ -171,34 +164,7 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception System.out.println(">>> CONNECT TO=" + addrs.values()); - // Создаем список адресов с явным указанием IPv4 - Collection ipv4Addrs = new ArrayList<>(); - for (String addr : addrs.values()) { - // Преобразуем адреса в формат IPv4, если они указаны как IPv6 - if (addr.contains("/")) { - // Если адрес содержит IPv6 формат, преобразуем его в IPv4 - int colonIndex = addr.lastIndexOf(':'); - if (colonIndex > 0) { - String hostPart = addr.substring(0, colonIndex); - String portPart = addr.substring(colonIndex + 1); - // Заменяем IPv6 localhost на IPv4 localhost - if (hostPart.equals("0:0:0:0:0:0:0:1") || hostPart.equals("[0:0:0:0:0:0:0:1]")) { - ipv4Addrs.add("127.0.0.1:" + portPart); - } else { - ipv4Addrs.add(addr); - } - } else { - ipv4Addrs.add(addr); - } - } else { - ipv4Addrs.add(addr); - } - } - - // Если список пустой, то не передаем никакие адреса для подключения - // Это позволяет ноде самой выбрать порт и быть доступной для других нод - IgniteEx ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), - ipv4Addrs)); + IgniteEx ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), addrs.values())); waitForCondition(() -> NODE_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); @@ -220,9 +186,7 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setPersistenceEnabled(true); TcpDiscoverySpi discoverySpi = new TcpDiscoverySpi() - .setLocalAddress("127.0.0.1") .setIpFinder(ipFinder.setAddresses(addrs)) - .setLocalPort(47500) .setNetworkTimeout(10000) .setAckTimeout(5000) .setJoinTimeout(10000); diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java index 04a4a41b32c58..80a5a19a3cb15 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java @@ -31,14 +31,14 @@ public class IgniteClusterContainer implements Startable { /** Network. */ private final Network net = Network.newNetwork(); - /** @param nodeIds Node ids. */ - public IgniteClusterContainer(String ver, List nodeIds) { + /** @param commitHash Commit hash. */ + public IgniteClusterContainer(String commitHash, List nodeIds) { containers = new ArrayList<>(nodeIds.size()); for (int i = 0; i < nodeIds.size(); i++) { String hostname = "node" + (1 + i); - IgniteContainer ignite = new IgniteContainer(ver, net, hostname, nodeIds.get(i)); + IgniteContainer ignite = new IgniteContainer(commitHash, net, hostname, nodeIds.get(i)); containers.add(ignite); } diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 8941ecb904914..62a907197445c 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -18,9 +18,6 @@ package org.apache.ignite.compatibility.testframework.testcontainers; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; @@ -36,7 +33,6 @@ 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; @@ -81,8 +77,8 @@ public class IgniteContainer extends GenericContainer { private final String workDirPath; /** Constructor. */ - public IgniteContainer(String ver, Network net, String hostname, String nodeId) { - super(DockerImageName.parse("apacheignite/ignite:" + ver)); + public IgniteContainer(String commitHash, Network net, String hostname, String nodeId) { + super(DockerImageName.parse("apacheignite/ignite:" + commitHash)); this.hostname = hostname; this.nodeId = nodeId; @@ -99,9 +95,6 @@ public IgniteContainer(String ver, Network net, String hostname, String nodeId) withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); - // Copy compatibility JAR if it exists - copyCompatibilityJar(); - withNetwork(net); withNetworkAliases(hostname); withExposedPorts(THIN_CLIENT_PORT, 47100, 47500); @@ -110,43 +103,6 @@ public IgniteContainer(String ver, Network net, String hostname, String nodeId) .withStartupTimeout(Duration.ofSeconds(60))); } - /** */ - private void copyCompatibilityJar() { - String projectDir = System.getProperty("user.dir"); - Path compatTarget = Paths.get(projectDir + "/target"); - - System.out.println(">>> KEK=" + compatTarget); - - if (Files.exists(compatTarget)) { - try { - Files.walk(compatTarget) - .filter(p -> { - System.out.println(">>> P=" + p); - - return p.toString().endsWith("tests.jar") && p.toString().contains("ignite-compatibility"); - }) - .findFirst() - .ifPresent(jarPath -> { - System.out.println(">>> JAR PATH=" + jarPath); - - withCopyFileToContainer(MountableFile.forHostPath(jarPath), ROOT_DIR_PATH + "libs/test-compatability.jar"); - - LOGGER.info("Bound compatibility JAR: " + jarPath); - }); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - else - throw new IllegalStateException("Please run `mvn clean install -DskipTests -pl modules/compatibility -am` first"); - } - - /** @return Hostname. */ - public String hostname() { - return hostname; - } - /** @return Node ID. */ public String nodeId() { return nodeId; diff --git a/modules/compatibility/src/test/resources/docker/build_docker_image.sh b/modules/compatibility/src/test/resources/docker/build_docker_image.sh new file mode 100755 index 0000000000000..3e3225fb80661 --- /dev/null +++ b/modules/compatibility/src/test/resources/docker/build_docker_image.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +# Get the absolute path to the project root directory +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../../../" && pwd)" + +# Check that commit hash is provided +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +COMMIT_HASH=$1 + +# Change to the project root directory +cd "$PROJECT_ROOT" || exit 1 + +# 1) Perform git checkout to the specified commit +echo -e "\nPerforming git checkout to commit: $COMMIT_HASH" +git checkout "$COMMIT_HASH" + +# 2) Build the project +echo -e "\nBuilding the project: ./mvnw clean install -Pall-java,licenses -DskipTests" +./mvnw clean install -Pall-java,licenses -DskipTests + +# Check success of previous command +if [ $? -ne 0 ]; then + echo -e "\nError during project build" + exit 1 +fi + +# 3) Initialize the release +echo -e "\nInitializing release: ./mvnw initialize -Prelease" +./mvnw initialize -Prelease + +# Check success of previous command +if [ $? -ne 0 ]; then + echo -e "\nError during release initialization" + exit 1 +fi + +# 4) Copy and unpack the release archive +echo -e "\nCopying and unpacking the release archive" +TARGET_DIR="$PROJECT_ROOT/deliveries/docker/apache-ignite/x86_64" +SOURCE_ARCHIVE="$PROJECT_ROOT/target/bin/apache-ignite-*.zip" + +# Check if archive exists +if [ ! -f $(echo $SOURCE_ARCHIVE) ]; then + echo -e "\nArchive not found: $SOURCE_ARCHIVE" + exit 1 +fi + +# Copy archive to target directory +cp $(echo $SOURCE_ARCHIVE) "$TARGET_DIR/" + +# Unpack the archive +cd "$TARGET_DIR" +unzip "$(basename $(echo $SOURCE_ARCHIVE))" + +# Return to project root directory +cd "$PROJECT_ROOT" + +# 5) Copy the startup script +echo -e "\nCopying startup script" +cp "$PROJECT_ROOT/deliveries/docker/apache-ignite/run.sh" "$TARGET_DIR/" + +# 6) Copy compatibility tests jar file to libs directory +echo -e "\nCopying compatibility jar to libs directory" +# Find the tests jar file in the target directory +COMPATIBILITY_JAR="" +for file in "$PROJECT_ROOT/modules/compatibility/target"/ignite-compatibility-*-tests.jar; do + if [ -f "$file" ]; then + COMPATIBILITY_JAR="$file" + break + fi +done + +if [ -n "$COMPATIBILITY_JAR" ] && [ -f "$COMPATIBILITY_JAR" ]; then + # Get the actual jar file name + ACTUAL_JAR=$(basename "$COMPATIBILITY_JAR") + # Find the actual ignite directory name after unpacking + IGNITE_DIR=$(ls -d "$TARGET_DIR"/apache-ignite-* 2>/dev/null | head -n 1) + if [ -n "$IGNITE_DIR" ]; then + # Copy to libs directory inside unpacked distribution + cp "$COMPATIBILITY_JAR" "$IGNITE_DIR/libs/" + echo -e "Compatibility tests jar copied successfully to libs directory" + else + echo -e "\nIgnite directory not found" + exit 1 + fi +else + echo -e "\nCompatibility tests jar not found" + exit 1 +fi + +# 7) Build Docker image +echo -e "\nBuilding Docker image - apacheignite/ignite:$COMMIT_HASH" +cd "$TARGET_DIR" +docker build . -t apacheignite/ignite:"$COMMIT_HASH" + +# Return to project root directory +cd "$PROJECT_ROOT" + +echo -e "\nDocker image built successfully - apacheignite/ignite:$COMMIT_HASH" \ No newline at end of file From a02aa70c28ce19c8a3958fcbcb30ff6bf295339f Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Tue, 16 Jun 2026 13:27:59 +0500 Subject: [PATCH 07/28] vibe --- .../ru/IgniteRebalanceOnUpgradeTest.java | 42 ++++++++++-------- .../IgniteClusterContainer.java | 5 --- .../testcontainers/IgniteContainer.java | 14 +++--- .../src/test/resources/docker/test-config.xml | 43 ++++++++++++------- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 7972d8e5e8c3a..9e3142a68ca76 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -18,6 +18,7 @@ package org.apache.ignite.compatibility.ru; import java.io.File; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -37,9 +38,9 @@ import org.apache.ignite.configuration.IgniteConfiguration; import org.apache.ignite.internal.IgniteEx; import org.apache.ignite.internal.util.typedef.internal.U; +import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi; import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; -import org.apache.ignite.spi.discovery.tcp.internal.TcpDiscoveryNode; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -77,7 +78,9 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { private final Map addrs = new HashMap<>(); /** */ - public final TcpDiscoveryVmIpFinder ipFinder = new TcpDiscoveryVmIpFinder(); + public IgniteRebalanceOnUpgradeTest() { + // Конструктор остается пустым, так как ipFinder инициализируется в методе configuration + } /** */ @BeforeClass @@ -113,15 +116,12 @@ public void testRollingUpgrade() throws Exception { try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_COMMIT_HASH, NODE_IDS)) { cluster.start(); - for (IgniteContainer container : cluster.containers()) { - // Явно используем IPv4 адреса - String ipv4Addr = "127.0.0.1:" + container.getMappedPort(47500); - addrs.put(container.nodeId(), ipv4Addr); - } + for (IgniteContainer container : cluster.containers()) + addrs.put(container.nodeId(), container.discoveryAddress()); System.out.println(">>> Addresses=" + addrs); - IgniteContainer node = cluster.firstNode(); + IgniteContainer node = cluster.containers().get(0); node.activateCluster(); @@ -136,7 +136,7 @@ public void testRollingUpgrade() throws Exception { cache.put(i, i); closeClient(); - + upgradeCluster(cluster); IgniteCache targetCache = nodes.get(0).cache(CACHE_NAME); @@ -168,17 +168,14 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception waitForCondition(() -> NODE_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); - // Обновляем адреса для новой ноды - addrs.put(container.nodeId(), "127.0.0.1:" + ((TcpDiscoveryNode)ignite.localNode()).discoveryPort()); + addrs.put(container.nodeId(), ignite.cluster().localNode().addresses().stream().findFirst().orElseThrow()); nodes.add(ignite); - - System.out.println(">>> Upgrade -> addresses=" + addrs); } } /** */ - private IgniteConfiguration configuration(String nodeId, String workDir, Collection addrs) { + private IgniteConfiguration configuration(String nodeId, String workDir, Collection addrs0) throws UnknownHostException { DataRegionConfiguration dataRegionCfg = new DataRegionConfiguration() .setName("testRegion") .setInitialSize(1024L * 1024 * 1024) @@ -186,16 +183,27 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setPersistenceEnabled(true); TcpDiscoverySpi discoverySpi = new TcpDiscoverySpi() - .setIpFinder(ipFinder.setAddresses(addrs)) + .setIpFinder(new TcpDiscoveryVmIpFinder().setAddresses(addrs0)) .setNetworkTimeout(10000) .setAckTimeout(5000) - .setJoinTimeout(10000); + .setJoinTimeout(10000) + // Установим локальный адрес для связи с контейнерами + .setLocalAddress("127.0.0.1") + // Установим порты для дисковери + .setLocalPort(47520) + .setLocalPortRange(20); + + TcpCommunicationSpi commSpi = new TcpCommunicationSpi(); + //.setLocalAddress("127.0.0.1") + //.setLocalPort(47100) + //.setLocalPortRange(100); return new IgniteConfiguration() .setConsistentId(nodeId) .setWorkDirectory(workDir) .setDataStorageConfiguration(new DataStorageConfiguration().setDataRegionConfigurations(dataRegionCfg)) - .setDiscoverySpi(discoverySpi); + .setDiscoverySpi(discoverySpi) + .setCommunicationSpi(commSpi); } /** */ diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java index 80a5a19a3cb15..423c0ab22f593 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java @@ -61,9 +61,4 @@ public IgniteClusterContainer(String commitHash, List nodeIds) { public List containers() { return containers; } - - /** @return First started node in cluster. */ - public IgniteContainer firstNode() { - return containers.get(0); - } } diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 62a907197445c..a4087a71574af 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -25,7 +25,9 @@ import java.util.regex.Pattern; import org.apache.ignite.IgniteException; import org.apache.ignite.cluster.ClusterState; +import org.apache.ignite.configuration.ClientConnectorConfiguration; import org.apache.ignite.internal.IgniteInterruptedCheckedException; +import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.BindMode; @@ -64,9 +66,6 @@ public class IgniteContainer extends GenericContainer { /** */ private static final Pattern CLUSTER_STATE_PATTERN = Pattern.compile("Cluster state: (ACTIVE|INACTIVE)"); - /** Default thin client port. */ - private static final int THIN_CLIENT_PORT = 10800; - /** Hostname. */ private final String hostname; @@ -97,10 +96,10 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withNetwork(net); withNetworkAliases(hostname); - withExposedPorts(THIN_CLIENT_PORT, 47100, 47500); + withExposedPorts(ClientConnectorConfiguration.DFLT_PORT, 47100, TcpDiscoverySpi.DFLT_PORT); waitingFor(Wait.forLogMessage(".*Node started.*", 1) - .withStartupTimeout(Duration.ofSeconds(60))); + .withStartupTimeout(Duration.ofSeconds(600))); } /** @return Node ID. */ @@ -136,13 +135,12 @@ public void activateCluster() { /** @return Client address. */ public String clientAddress() { - return getHost() + ":" + getMappedPort(THIN_CLIENT_PORT); + return getHost() + ":" + getMappedPort(ClientConnectorConfiguration.DFLT_PORT); } /** */ public String discoveryAddress() { - // Возвращаем IPv4 адрес явно - return "127.0.0.1:" + getMappedPort(47500); + return getHost() + ":" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); } /** */ diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index cec24ab612dab..130d70c98fe3b 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -22,9 +22,6 @@ xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - @@ -38,20 +35,34 @@ - - + + - - - - - - - - - - - + + + + + + + node1:47500..47520 + node2:47500..47520 + node3:47500..47520 + 0.0.0.0:47520..47540 + localhost:47520..47540 + host.docker.internal:47520..47540 + 127.0.0.1:47520..47540 + + + + + + + + + + + + From 5ebd23da9cec8815d5a8404164ef7fb2642d0e6b Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Wed, 17 Jun 2026 12:19:22 +0500 Subject: [PATCH 08/28] WORK --- .../ru/IgniteRebalanceOnUpgradeTest.java | 43 +++++++++++++++---- .../IgniteClusterContainer.java | 4 +- .../testcontainers/IgniteContainer.java | 37 +++++++++++++--- .../src/test/resources/docker/test-config.xml | 8 ++-- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 9e3142a68ca76..50bf7abe02440 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -18,6 +18,7 @@ package org.apache.ignite.compatibility.ru; import java.io.File; +import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; @@ -40,6 +41,8 @@ import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi; import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; +import org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder; +import org.apache.ignite.spi.discovery.tcp.ipfinder.sharedfs.TcpDiscoverySharedFsIpFinder; import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.junit.AfterClass; @@ -164,9 +167,21 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception System.out.println(">>> CONNECT TO=" + addrs.values()); - IgniteEx ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), addrs.values())); + IgniteEx ignite = null; - waitForCondition(() -> NODE_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); + try { + Thread.sleep(5_000); + + ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), addrs.values())); + } + catch (Exception ex) { + System.out.println(">>> ERR=" + ex); + Thread.sleep(Long.MAX_VALUE); + } + + IgniteEx finalIgnite = ignite; + + waitForCondition(() -> NODE_IDS.size() == finalIgnite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); addrs.put(container.nodeId(), ignite.cluster().localNode().addresses().stream().findFirst().orElseThrow()); @@ -188,22 +203,23 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setAckTimeout(5000) .setJoinTimeout(10000) // Установим локальный адрес для связи с контейнерами - .setLocalAddress("127.0.0.1") + .setLocalAddress(InetAddress.getLocalHost().getHostAddress()) // Установим порты для дисковери - .setLocalPort(47520) - .setLocalPortRange(20); + .setLocalPort(47500); + //.setLocalPortRange(20); TcpCommunicationSpi commSpi = new TcpCommunicationSpi(); - //.setLocalAddress("127.0.0.1") + //.setLocalAddress("0.0.0.0"); //.setLocalPort(47100) //.setLocalPortRange(100); return new IgniteConfiguration() + //.setLocalHost(InetAddress.getLocalHost().getHostAddress()) .setConsistentId(nodeId) .setWorkDirectory(workDir) .setDataStorageConfiguration(new DataStorageConfiguration().setDataRegionConfigurations(dataRegionCfg)) - .setDiscoverySpi(discoverySpi) - .setCommunicationSpi(commSpi); + .setDiscoverySpi(discoverySpi); + //.setCommunicationSpi(commSpi); } /** */ @@ -223,3 +239,14 @@ private void closeClient() { } } } + + +/** + [23:47:56,251][INFO][tcp-disco-sock-reader-[]-#11-#113][TcpDiscoverySpi] Started serving remote node connection [rmtAddr=/192.168.65.1:47965, rmtPort=47965] + + [23:47:56,253][INFO][tcp-disco-sock-reader-[53822b8b 192.168.65.1:47965]-#11-#113][TcpDiscoverySpi] Initialized connection with remote server node [nodeId=53822b8b-42d2-4e41-9866-5fb49a60395b, rmtAddr=/192.168.65.1:47965] + + [23:47:56,971][WARNING][tcp-disco-sock-reader-[53822b8b 192.168.65.1:47965]-#11-#113][TcpDiscoverySpi] Failed to ping node [nodeId=53822b8b-42d2-4e41-9866-5fb49a60395b, address=/127.0.0.1:47520]. Node has left or is leaving topology. Cause: Connection refused + + [23:47:56,972][WARNING][tcp-disco-sock-reader-[53822b8b 192.168.65.1:47965]-#11-#113][TcpDiscoverySpi] Failed to ping joining node, closing connection. [node=TcpDiscoveryNode [id=53822b8b-42d2-4e41-9866-5fb49a60395b, consistentId=ad26bff6-5ff5-49f1-9a61-425a827953ed, addrs=ArrayList [127.0.0.1], sockAddrs=HashSet [/127.0.0.1:47520], discPort=47520, order=0, intOrder=0, loc=false, ver=2.19.0#19700101-sha1:00000000, isClient=false, dataCenterId=null]] + */ diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java index 423c0ab22f593..f672541d1125c 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java @@ -17,6 +17,8 @@ package org.apache.ignite.compatibility.testframework.testcontainers; +import java.io.IOException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import org.testcontainers.containers.Network; @@ -32,7 +34,7 @@ public class IgniteClusterContainer implements Startable { private final Network net = Network.newNetwork(); /** @param commitHash Commit hash. */ - public IgniteClusterContainer(String commitHash, List nodeIds) { + public IgniteClusterContainer(String commitHash, List nodeIds) throws IOException { containers = new ArrayList<>(nodeIds.size()); for (int i = 0; i < nodeIds.size(); i++) { diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index a4087a71574af..715a9ec09e932 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -18,6 +18,10 @@ package org.apache.ignite.compatibility.testframework.testcontainers; import java.io.IOException; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; @@ -27,6 +31,7 @@ import org.apache.ignite.cluster.ClusterState; import org.apache.ignite.configuration.ClientConnectorConfiguration; import org.apache.ignite.internal.IgniteInterruptedCheckedException; +import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi; import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,6 +43,7 @@ import static org.apache.ignite.testframework.GridTestUtils.waitForCondition; import static org.testcontainers.utility.MountableFile.forClasspathResource; +import static org.testcontainers.utility.MountableFile.forHostPath; /** Ignite container. */ public class IgniteContainer extends GenericContainer { @@ -76,7 +82,7 @@ public class IgniteContainer extends GenericContainer { private final String workDirPath; /** Constructor. */ - public IgniteContainer(String commitHash, Network net, String hostname, String nodeId) { + public IgniteContainer(String commitHash, Network net, String hostname, String nodeId) throws IOException { super(DockerImageName.parse("apacheignite/ignite:" + commitHash)); this.hostname = hostname; @@ -87,16 +93,25 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withEnv("IGNITE_QUIET", "false"); withEnv("IGNITE_NODE_NAME", nodeId); withEnv("IGNITE_WORK_DIR", workDirPath); - withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); + //withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); + //getPortBindings().add("47500:47500"); + //getPortBindings().add("47100:47100"); + withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); - withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); + String configTemplate = Files.readString(Paths.get("src/test/resources/docker/test-config.xml")); + String configXml = configTemplate.replace("${LOCAL_IP}", InetAddress.getLocalHost().getHostAddress()); + + Path tempConfigFile = Files.createTempFile("ignite-config-", ".xml"); + Files.write(tempConfigFile, configXml.getBytes()); + + withCopyFileToContainer(forHostPath(tempConfigFile), CFG_PATH); withNetwork(net); withNetworkAliases(hostname); - withExposedPorts(ClientConnectorConfiguration.DFLT_PORT, 47100, TcpDiscoverySpi.DFLT_PORT); + withExposedPorts(ClientConnectorConfiguration.DFLT_PORT, TcpCommunicationSpi.DFLT_PORT, TcpDiscoverySpi.DFLT_PORT); waitingFor(Wait.forLogMessage(".*Node started.*", 1) .withStartupTimeout(Duration.ofSeconds(600))); @@ -135,12 +150,22 @@ public void activateCluster() { /** @return Client address. */ public String clientAddress() { - return getHost() + ":" + getMappedPort(ClientConnectorConfiguration.DFLT_PORT); + return address(ClientConnectorConfiguration.DFLT_PORT); } /** */ public String discoveryAddress() { - return getHost() + ":" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); + return address(TcpDiscoverySpi.DFLT_PORT); + +// return "0.0.0.0:" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); + +// return getContainerInfo().getNetworkSettings().getNetworks().values() +// .iterator().next().getIpAddress() + ":" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); + } + + /** */ + private String address(int port) { + return getHost() + ":" + getMappedPort(port); } /** */ diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index 130d70c98fe3b..ca2970861e179 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -37,7 +37,6 @@ - @@ -47,10 +46,9 @@ node1:47500..47520 node2:47500..47520 node3:47500..47520 - 0.0.0.0:47520..47540 - localhost:47520..47540 - host.docker.internal:47520..47540 - 127.0.0.1:47520..47540 + ${LOCAL_IP}:47500 + ${LOCAL_IP}:47501 + ${LOCAL_IP}:47502 From 71f01ca744eee0dd8011f9493c7f9c6275b7f0cb Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Wed, 17 Jun 2026 15:45:17 +0500 Subject: [PATCH 09/28] def data region fix --- .../ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 50bf7abe02440..5dcb95e9ac82b 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -217,7 +217,7 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect //.setLocalHost(InetAddress.getLocalHost().getHostAddress()) .setConsistentId(nodeId) .setWorkDirectory(workDir) - .setDataStorageConfiguration(new DataStorageConfiguration().setDataRegionConfigurations(dataRegionCfg)) + .setDataStorageConfiguration(new DataStorageConfiguration().setDefaultDataRegionConfiguration(dataRegionCfg)) .setDiscoverySpi(discoverySpi); //.setCommunicationSpi(commSpi); } From 3c4c0d07348efb47868073118634f2c9c2e14767 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Wed, 17 Jun 2026 18:54:29 +0500 Subject: [PATCH 10/28] WIP --- .../ru/IgniteRebalanceOnUpgradeTest.java | 35 +++-------------- .../IgniteClusterContainer.java | 3 +- .../testcontainers/IgniteContainer.java | 38 ++++++++++--------- .../src/test/resources/docker/test-config.xml | 12 ++---- .../spi/discovery/tcp/TcpDiscoverySpi.java | 2 +- 5 files changed, 32 insertions(+), 58 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 5dcb95e9ac82b..5e5b15c588e1e 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -41,8 +41,6 @@ import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi; import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; -import org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder; -import org.apache.ignite.spi.discovery.tcp.ipfinder.sharedfs.TcpDiscoverySharedFsIpFinder; import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.junit.AfterClass; @@ -80,20 +78,12 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { /** */ private final Map addrs = new HashMap<>(); - /** */ - public IgniteRebalanceOnUpgradeTest() { - // Конструктор остается пустым, так как ipFinder инициализируется в методе configuration - } - /** */ @BeforeClass public static void beforeClass() { U.delete(LOCAL_WORK_DIR); - // Установка свойства для предпочтения IPv4 адресов System.setProperty("java.net.preferIPv4Stack", "true"); - - // Также устанавливаем свойство для принудительного использования IPv4 System.setProperty("java.net.preferIPv6Addresses", "false"); } @@ -110,7 +100,7 @@ public static void afterClass() { /** {@inheritDoc} */ @Override protected long getTestTimeout() { - return super.getTestTimeout() * 2; + return super.getTestTimeout() * 3; } /** Basic RU test. */ @@ -124,16 +114,12 @@ public void testRollingUpgrade() throws Exception { System.out.println(">>> Addresses=" + addrs); - IgniteContainer node = cluster.containers().get(0); - - node.activateCluster(); - ClientCacheConfiguration cfg = new ClientCacheConfiguration() .setName(CACHE_NAME) .setBackups(1) .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL); - ClientCache cache = client(node.clientAddress()).createCache(cfg); + ClientCache cache = client(cluster.containers().get(0).clientAddress()).createCache(cfg); for (int i = 0; i < 1000; i++) cache.put(i, i); @@ -170,7 +156,7 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception IgniteEx ignite = null; try { - Thread.sleep(5_000); + Thread.sleep(20_000); ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), addrs.values())); } @@ -205,8 +191,8 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect // Установим локальный адрес для связи с контейнерами .setLocalAddress(InetAddress.getLocalHost().getHostAddress()) // Установим порты для дисковери - .setLocalPort(47500); - //.setLocalPortRange(20); + .setLocalPort(48500) + .setLocalPortRange(20); TcpCommunicationSpi commSpi = new TcpCommunicationSpi(); //.setLocalAddress("0.0.0.0"); @@ -239,14 +225,3 @@ private void closeClient() { } } } - - -/** - [23:47:56,251][INFO][tcp-disco-sock-reader-[]-#11-#113][TcpDiscoverySpi] Started serving remote node connection [rmtAddr=/192.168.65.1:47965, rmtPort=47965] - - [23:47:56,253][INFO][tcp-disco-sock-reader-[53822b8b 192.168.65.1:47965]-#11-#113][TcpDiscoverySpi] Initialized connection with remote server node [nodeId=53822b8b-42d2-4e41-9866-5fb49a60395b, rmtAddr=/192.168.65.1:47965] - - [23:47:56,971][WARNING][tcp-disco-sock-reader-[53822b8b 192.168.65.1:47965]-#11-#113][TcpDiscoverySpi] Failed to ping node [nodeId=53822b8b-42d2-4e41-9866-5fb49a60395b, address=/127.0.0.1:47520]. Node has left or is leaving topology. Cause: Connection refused - - [23:47:56,972][WARNING][tcp-disco-sock-reader-[53822b8b 192.168.65.1:47965]-#11-#113][TcpDiscoverySpi] Failed to ping joining node, closing connection. [node=TcpDiscoveryNode [id=53822b8b-42d2-4e41-9866-5fb49a60395b, consistentId=ad26bff6-5ff5-49f1-9a61-425a827953ed, addrs=ArrayList [127.0.0.1], sockAddrs=HashSet [/127.0.0.1:47520], discPort=47520, order=0, intOrder=0, loc=false, ver=2.19.0#19700101-sha1:00000000, isClient=false, dataCenterId=null]] - */ diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java index f672541d1125c..0bfc28f1474d7 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java @@ -18,7 +18,6 @@ package org.apache.ignite.compatibility.testframework.testcontainers; import java.io.IOException; -import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import org.testcontainers.containers.Network; @@ -49,6 +48,8 @@ public IgniteClusterContainer(String commitHash, List nodeIds) throws IO /** {@inheritDoc} */ @Override public void start() { Startables.deepStart(containers).join(); + + containers.get(0).activateCluster(containers.size()); } /** {@inheritDoc} */ diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 715a9ec09e932..90cf7ac1572e0 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -19,9 +19,6 @@ import java.io.IOException; import java.net.InetAddress; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; @@ -43,7 +40,6 @@ import static org.apache.ignite.testframework.GridTestUtils.waitForCondition; import static org.testcontainers.utility.MountableFile.forClasspathResource; -import static org.testcontainers.utility.MountableFile.forHostPath; /** Ignite container. */ public class IgniteContainer extends GenericContainer { @@ -96,21 +92,13 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n //withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); - //getPortBindings().add("47500:47500"); - //getPortBindings().add("47100:47100"); - withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); - - String configTemplate = Files.readString(Paths.get("src/test/resources/docker/test-config.xml")); - String configXml = configTemplate.replace("${LOCAL_IP}", InetAddress.getLocalHost().getHostAddress()); - - Path tempConfigFile = Files.createTempFile("ignite-config-", ".xml"); - Files.write(tempConfigFile, configXml.getBytes()); - - withCopyFileToContainer(forHostPath(tempConfigFile), CFG_PATH); + withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); withNetwork(net); withNetworkAliases(hostname); + + withExtraHost("localnode", InetAddress.getLocalHost().getHostAddress()); withExposedPorts(ClientConnectorConfiguration.DFLT_PORT, TcpCommunicationSpi.DFLT_PORT, TcpDiscoverySpi.DFLT_PORT); waitingFor(Wait.forLogMessage(".*Node started.*", 1) @@ -128,11 +116,11 @@ public String localWorkDirectory() { } /** Activate cluster. */ - public void activateCluster() { + public void activateCluster(int nodeCnt) { execControl("--set-state", "ACTIVE", "--yes"); try { - waitForCondition(() -> { + boolean success = waitForCondition(() -> { String out = execControl("--state"); Matcher matcher = CLUSTER_STATE_PATTERN.matcher(out); @@ -142,6 +130,20 @@ public void activateCluster() { return false; }, 30_000); + + if (!success) + throw new IllegalStateException("Failed to set state ACTIVE"); + + success = waitForCondition(() -> { + String out = execControl("--baseline"); + + System.out.println(">>> Out=" + out); + + return out.contains("Number of baseline nodes: " + nodeCnt); + }, 30_000, 5_000); + + if (!success) + throw new IllegalStateException("Check cluster count failed"); } catch (IgniteInterruptedCheckedException e) { throw new IgniteException(e); @@ -155,7 +157,7 @@ public String clientAddress() { /** */ public String discoveryAddress() { - return address(TcpDiscoverySpi.DFLT_PORT); + return address(TcpDiscoverySpi.DFLT_PORT).replace("localhost", "0.0.0.0"); // return "0.0.0.0:" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index ca2970861e179..31a106fe4ceb9 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -38,17 +38,14 @@ - - node1:47500..47520 - node2:47500..47520 - node3:47500..47520 - ${LOCAL_IP}:47500 - ${LOCAL_IP}:47501 - ${LOCAL_IP}:47502 + node1:47500 + node2:47500 + node3:47500 + localnode:48500..48520 @@ -58,7 +55,6 @@ - diff --git a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java index eb868103ad1c2..e28efcfb56c91 100644 --- a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java @@ -1269,7 +1269,7 @@ LinkedHashSet getEffectiveNodeAddresses(TcpDiscoveryNode node // Do not give own loopback to avoid requesting current node. if (!node.equals(locNode)) - addrs.removeIf(addr -> addr.getAddress().isLoopbackAddress() && locNode.socketAddresses().contains(addr)); + addrs.removeIf(addr -> addr.getAddress() == null || addr.getAddress().isLoopbackAddress() && locNode.socketAddresses().contains(addr)); addrs.sort(U.inetAddressesComparator(sameHost)); From 1b354959e4a5bf932e6183d3a722c64acf964c07 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 18 Jun 2026 02:18:47 +0500 Subject: [PATCH 11/28] WIP --- .../compatibility/ru/IgniteRebalanceOnUpgradeTest.java | 6 ++++-- .../testframework/testcontainers/IgniteContainer.java | 4 ++-- .../compatibility/src/test/resources/docker/test-config.xml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 5e5b15c588e1e..c670ef5afefce 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -189,7 +189,9 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setAckTimeout(5000) .setJoinTimeout(10000) // Установим локальный адрес для связи с контейнерами - .setLocalAddress(InetAddress.getLocalHost().getHostAddress()) + //.setLocalAddress(InetAddress.getLocalHost().getHostAddress()) +// .setAddressFilter(addrs -> !(addrs.getHostString().contains("0.0.0.0") +// || addrs.getHostString().contains("127.0.0.1"))) // Установим порты для дисковери .setLocalPort(48500) .setLocalPortRange(20); @@ -200,7 +202,7 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect //.setLocalPortRange(100); return new IgniteConfiguration() - //.setLocalHost(InetAddress.getLocalHost().getHostAddress()) + .setLocalHost("0.0.0.0") .setConsistentId(nodeId) .setWorkDirectory(workDir) .setDataStorageConfiguration(new DataStorageConfiguration().setDefaultDataRegionConfiguration(dataRegionCfg)) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 90cf7ac1572e0..c1d8ce1990167 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -89,7 +89,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withEnv("IGNITE_QUIET", "false"); withEnv("IGNITE_NODE_NAME", nodeId); withEnv("IGNITE_WORK_DIR", workDirPath); - //withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); + withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); @@ -98,7 +98,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withNetwork(net); withNetworkAliases(hostname); - withExtraHost("localnode", InetAddress.getLocalHost().getHostAddress()); + withExtraHost("host-gateway", "host-gateway"); // InetAddress.getLocalHost().getHostAddress()); withExposedPorts(ClientConnectorConfiguration.DFLT_PORT, TcpCommunicationSpi.DFLT_PORT, TcpDiscoverySpi.DFLT_PORT); waitingFor(Wait.forLogMessage(".*Node started.*", 1) diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index 31a106fe4ceb9..fe7c6f3f0b8f5 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -45,7 +45,7 @@ node1:47500 node2:47500 node3:47500 - localnode:48500..48520 + host-gateway:48500..48520 From 7c79071b7e4074351827d5f0bbc380ce31dcd3fa Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 18 Jun 2026 11:30:31 +0500 Subject: [PATCH 12/28] WIP --- .../compatibility/ru/IgniteRebalanceOnUpgradeTest.java | 6 ++---- .../compatibility/src/test/resources/docker/test-config.xml | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index c670ef5afefce..e63df3073038d 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -18,7 +18,7 @@ package org.apache.ignite.compatibility.ru; import java.io.File; -import java.net.InetAddress; +//import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; @@ -61,7 +61,7 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { ); /** Source commit hash. */ - private static final String SOURCE_COMMIT_HASH = "6b172a8b"; + private static final String SOURCE_COMMIT_HASH = "f239499b"; //"6b172a8b"; /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; @@ -188,11 +188,9 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setNetworkTimeout(10000) .setAckTimeout(5000) .setJoinTimeout(10000) - // Установим локальный адрес для связи с контейнерами //.setLocalAddress(InetAddress.getLocalHost().getHostAddress()) // .setAddressFilter(addrs -> !(addrs.getHostString().contains("0.0.0.0") // || addrs.getHostString().contains("127.0.0.1"))) - // Установим порты для дисковери .setLocalPort(48500) .setLocalPortRange(20); diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index fe7c6f3f0b8f5..855b15f43cd76 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -46,6 +46,8 @@ node2:47500 node3:47500 host-gateway:48500..48520 + 0.0.0.0:48500..48520 + 127.0.0.1:48500..48520 From ac295b2d7cf56426d09ab80cdf3ca942685714b6 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 18 Jun 2026 12:31:30 +0500 Subject: [PATCH 13/28] qwen --- .../ru/HostNodeAddressResolver.java | 74 +++++++++++++++ .../ru/IgniteRebalanceOnUpgradeTest.java | 35 ++++--- .../testcontainers/CustomAddressResolver.java | 94 +++++++++++++++++++ .../testcontainers/IgniteContainer.java | 16 +++- .../src/test/resources/docker/test-config.xml | 9 +- 5 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/HostNodeAddressResolver.java create mode 100644 modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/CustomAddressResolver.java diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/HostNodeAddressResolver.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/HostNodeAddressResolver.java new file mode 100644 index 0000000000000..50b906e419408 --- /dev/null +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/HostNodeAddressResolver.java @@ -0,0 +1,74 @@ +/* + * 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.compatibility.ru; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.configuration.AddressResolver; + +/** + * Address resolver for a local (non-container) node joining a cluster with Docker containers. + *

+ * On macOS with Docker Desktop, containers run inside a VM and cannot reach the host's + * real IP addresses (e.g., 192.168.x.x). The only way to reach the host from a container + * is via the special DNS name {@code host.docker.internal}, which Docker resolves to the + * host gateway inside the VM. + *

+ * This resolver maps internal addresses to {@code host.docker.internal} using a resolved + * InetAddress so that Ignite discovery messages can serialize them properly. + */ +public class HostNodeAddressResolver implements AddressResolver { + /** Special Docker DNS name that resolves to the host machine from inside containers. */ + private static final String HOST_DOCKER_INTERNAL = "host.docker.internal"; + + /** + * Creates a resolved InetAddress for host.docker.internal using a non-loopback dummy IP (255.255.255.255). + * Using a non-loopback IP ensures that the address is NOT filtered out by TcpDiscoverySpi's + * loopback address removal logic in getEffectiveNodeAddresses. + * The hostname is preserved as host.docker.internal so that containers will resolve it + * via Docker's DNS to the actual host gateway. + */ + private static InetAddress createHostDockerAddress() { + try { + // Use 255.255.255.255 (broadcast) as dummy IP — it's non-loopback, + // but the hostname "host.docker.internal" will be used for DNS resolution by containers. + return InetAddress.getByAddress(HOST_DOCKER_INTERNAL, new byte[]{(byte) 255, (byte) 255, (byte) 255, (byte) 255}); + } + catch (UnknownHostException e) { + throw new RuntimeException("Failed to create " + HOST_DOCKER_INTERNAL + " address", e); + } + } + + /** */ + private static final InetAddress HOST_DOCKER_ADDR = createHostDockerAddress(); + + /** {@inheritDoc} */ + @Override public Collection getExternalAddresses(InetSocketAddress addr) throws IgniteCheckedException { + if (addr == null) + return Collections.emptyList(); + + // Map all local addresses to host.docker.internal so containers have a reachable address. + // Local nodes will try the internal 192.168.x.x first (which works for local-to-local). + return List.of(new InetSocketAddress(HOST_DOCKER_ADDR, addr.getPort())); + } +} diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index e63df3073038d..b0c5e55d408f2 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -18,7 +18,6 @@ package org.apache.ignite.compatibility.ru; import java.io.File; -//import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; @@ -61,7 +60,7 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { ); /** Source commit hash. */ - private static final String SOURCE_COMMIT_HASH = "f239499b"; //"6b172a8b"; + private static final String SOURCE_COMMIT_HASH = "4afd6dfc"; //"6b172a8b"; /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; @@ -169,7 +168,20 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception waitForCondition(() -> NODE_IDS.size() == finalIgnite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); - addrs.put(container.nodeId(), ignite.cluster().localNode().addresses().stream().findFirst().orElseThrow()); + // Get discovery port via reflection on protected getSpi() of GridDiscoveryManager. + Object discoMgr = ignite.context().discovery(); + java.lang.reflect.Method getSpiMethod = discoMgr.getClass().getSuperclass().getDeclaredMethod("getSpi"); + boolean acc = getSpiMethod.canAccess(discoMgr); + getSpiMethod.setAccessible(true); + TcpDiscoverySpi localDiscoverySpi = (TcpDiscoverySpi) getSpiMethod.invoke(discoMgr); + if (!acc) + getSpiMethod.setAccessible(false); + + String hostAddr = IgniteContainer.hostMachineIpAddress() + ":" + localDiscoverySpi.getLocalPort(); + + System.out.println(">>> Local node external address: " + hostAddr); + + addrs.put(container.nodeId(), hostAddr); nodes.add(ignite); } @@ -188,24 +200,23 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setNetworkTimeout(10000) .setAckTimeout(5000) .setJoinTimeout(10000) - //.setLocalAddress(InetAddress.getLocalHost().getHostAddress()) -// .setAddressFilter(addrs -> !(addrs.getHostString().contains("0.0.0.0") -// || addrs.getHostString().contains("127.0.0.1"))) + .setLocalAddress("0.0.0.0") .setLocalPort(48500) .setLocalPortRange(20); - TcpCommunicationSpi commSpi = new TcpCommunicationSpi(); - //.setLocalAddress("0.0.0.0"); - //.setLocalPort(47100) - //.setLocalPortRange(100); + TcpCommunicationSpi commSpi = new TcpCommunicationSpi() + .setLocalAddress("0.0.0.0") + .setLocalPort(48400) + .setLocalPortRange(100); return new IgniteConfiguration() .setLocalHost("0.0.0.0") .setConsistentId(nodeId) .setWorkDirectory(workDir) .setDataStorageConfiguration(new DataStorageConfiguration().setDefaultDataRegionConfiguration(dataRegionCfg)) - .setDiscoverySpi(discoverySpi); - //.setCommunicationSpi(commSpi); + .setDiscoverySpi(discoverySpi) + .setCommunicationSpi(commSpi) + .setAddressResolver(new HostNodeAddressResolver()); } /** */ diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/CustomAddressResolver.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/CustomAddressResolver.java new file mode 100644 index 0000000000000..d944a774199a5 --- /dev/null +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/CustomAddressResolver.java @@ -0,0 +1,94 @@ +/* + * 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.compatibility.testframework.testcontainers; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.apache.ignite.configuration.AddressResolver; + +/** + * Address resolver for container-to-host communication. + *

+ * When a container receives a discovery/communication address from a local (non-container) node, + * that address points to the host machine's real IP (e.g., 192.168.x.x), which from inside + * a Docker container on macOS (Docker Desktop VM) is not reachable. This resolver maps + * such addresses to {@code host.docker.internal}, a special Docker DNS name that resolves + * to the host machine from within containers. + */ +public class CustomAddressResolver implements AddressResolver { + /** + * Special Docker DNS name that resolves to the host machine from inside containers. + */ + private static final String HOST_DOCKER_INTERNAL = "host.docker.internal"; + + /** {@inheritDoc} */ + @Override public Collection getExternalAddresses(InetSocketAddress addr) { + if (addr == null) + return Collections.emptyList(); + + InetAddress internalAddr = addr.getAddress(); + + if (internalAddr == null) { + // Unresolved address — try to resolve it + try { + internalAddr = InetAddress.getByName(addr.getHostName()); + } + catch (UnknownHostException ignored) { + return List.of(addr); + } + } + + String ip = internalAddr.getHostAddress(); + + // Map loopback to host.docker.internal. + // When a local (non-container) node joins with setLocalHost("127.0.0.1"), + // from inside the container 127.0.0.1 means the container itself, not the host. + if (ip.equals("127.0.0.1")) { + try { + return List.of(new InetSocketAddress( + InetAddress.getByName(HOST_DOCKER_INTERNAL), addr.getPort())); + } + catch (UnknownHostException e) { + return List.of(new InetSocketAddress(HOST_DOCKER_INTERNAL, addr.getPort())); + } + } + + // Map the host machine's private IP to host.docker.internal. + // Docker containers on macOS (Docker Desktop VM) cannot reach the host's real IP (192.168.x.x). + // Container IPs in Testcontainers network are typically 172.16.x.x-172.31.x.x, so we only map + // 192.168.x.x which is typical for macOS host interfaces. + if (ip.startsWith("192.168.")) { + try { + return List.of(new InetSocketAddress(InetAddress.getByName(HOST_DOCKER_INTERNAL), addr.getPort())); + } + catch (UnknownHostException e) { + return List.of(new InetSocketAddress(HOST_DOCKER_INTERNAL, addr.getPort())); + } + } + + // All other addresses (e.g., Docker network 172.x.x.x) are returned as-is. + // All other addresses are returned as-is. + // If the local node advertises its real IP (e.g., 192.168.x.x), containers will try to connect directly. + // If direct connection fails, they will fall back to other addresses including external ones. + return List.of(addr); + } +} diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index c1d8ce1990167..6f5ab94a58682 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -19,9 +19,12 @@ import java.io.IOException; import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; +import java.util.Enumeration; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ignite.IgniteException; @@ -68,6 +71,14 @@ public class IgniteContainer extends GenericContainer { /** */ private static final Pattern CLUSTER_STATE_PATTERN = Pattern.compile("Cluster state: (ACTIVE|INACTIVE)"); + /** + * Returns the host machine gateway address reachable from Docker containers. + * Uses the Docker special DNS name that resolves to the host machine. + */ + public static String hostMachineIpAddress() { + return "host.docker.internal"; + } + /** Hostname. */ private final String hostname; @@ -89,7 +100,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withEnv("IGNITE_QUIET", "false"); withEnv("IGNITE_NODE_NAME", nodeId); withEnv("IGNITE_WORK_DIR", workDirPath); - withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); + //withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); @@ -98,7 +109,6 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withNetwork(net); withNetworkAliases(hostname); - withExtraHost("host-gateway", "host-gateway"); // InetAddress.getLocalHost().getHostAddress()); withExposedPorts(ClientConnectorConfiguration.DFLT_PORT, TcpCommunicationSpi.DFLT_PORT, TcpDiscoverySpi.DFLT_PORT); waitingFor(Wait.forLogMessage(".*Node started.*", 1) @@ -157,7 +167,7 @@ public String clientAddress() { /** */ public String discoveryAddress() { - return address(TcpDiscoverySpi.DFLT_PORT).replace("localhost", "0.0.0.0"); + return address(TcpDiscoverySpi.DFLT_PORT); // return "0.0.0.0:" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index 855b15f43cd76..d63576a2ef3a5 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -37,7 +37,11 @@ + + + + @@ -45,9 +49,7 @@ node1:47500 node2:47500 node3:47500 - host-gateway:48500..48520 - 0.0.0.0:48500..48520 - 127.0.0.1:48500..48520 + host-machine:48500..48520 @@ -56,6 +58,7 @@ + From bdbbfcaa89b0a767e425c395750ad88aca91fbb2 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Tue, 23 Jun 2026 17:34:00 +0500 Subject: [PATCH 14/28] Revert "qwen" This reverts commit 0b6e0ef76322da42f259ba711428f20627186b71. --- .../ru/HostNodeAddressResolver.java | 74 --------------- .../ru/IgniteRebalanceOnUpgradeTest.java | 35 +++---- .../testcontainers/CustomAddressResolver.java | 94 ------------------- .../testcontainers/IgniteContainer.java | 16 +--- .../src/test/resources/docker/test-config.xml | 9 +- 5 files changed, 18 insertions(+), 210 deletions(-) delete mode 100644 modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/HostNodeAddressResolver.java delete mode 100644 modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/CustomAddressResolver.java diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/HostNodeAddressResolver.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/HostNodeAddressResolver.java deleted file mode 100644 index 50b906e419408..0000000000000 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/HostNodeAddressResolver.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.compatibility.ru; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import org.apache.ignite.IgniteCheckedException; -import org.apache.ignite.configuration.AddressResolver; - -/** - * Address resolver for a local (non-container) node joining a cluster with Docker containers. - *

- * On macOS with Docker Desktop, containers run inside a VM and cannot reach the host's - * real IP addresses (e.g., 192.168.x.x). The only way to reach the host from a container - * is via the special DNS name {@code host.docker.internal}, which Docker resolves to the - * host gateway inside the VM. - *

- * This resolver maps internal addresses to {@code host.docker.internal} using a resolved - * InetAddress so that Ignite discovery messages can serialize them properly. - */ -public class HostNodeAddressResolver implements AddressResolver { - /** Special Docker DNS name that resolves to the host machine from inside containers. */ - private static final String HOST_DOCKER_INTERNAL = "host.docker.internal"; - - /** - * Creates a resolved InetAddress for host.docker.internal using a non-loopback dummy IP (255.255.255.255). - * Using a non-loopback IP ensures that the address is NOT filtered out by TcpDiscoverySpi's - * loopback address removal logic in getEffectiveNodeAddresses. - * The hostname is preserved as host.docker.internal so that containers will resolve it - * via Docker's DNS to the actual host gateway. - */ - private static InetAddress createHostDockerAddress() { - try { - // Use 255.255.255.255 (broadcast) as dummy IP — it's non-loopback, - // but the hostname "host.docker.internal" will be used for DNS resolution by containers. - return InetAddress.getByAddress(HOST_DOCKER_INTERNAL, new byte[]{(byte) 255, (byte) 255, (byte) 255, (byte) 255}); - } - catch (UnknownHostException e) { - throw new RuntimeException("Failed to create " + HOST_DOCKER_INTERNAL + " address", e); - } - } - - /** */ - private static final InetAddress HOST_DOCKER_ADDR = createHostDockerAddress(); - - /** {@inheritDoc} */ - @Override public Collection getExternalAddresses(InetSocketAddress addr) throws IgniteCheckedException { - if (addr == null) - return Collections.emptyList(); - - // Map all local addresses to host.docker.internal so containers have a reachable address. - // Local nodes will try the internal 192.168.x.x first (which works for local-to-local). - return List.of(new InetSocketAddress(HOST_DOCKER_ADDR, addr.getPort())); - } -} diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index b0c5e55d408f2..e63df3073038d 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -18,6 +18,7 @@ package org.apache.ignite.compatibility.ru; import java.io.File; +//import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; @@ -60,7 +61,7 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { ); /** Source commit hash. */ - private static final String SOURCE_COMMIT_HASH = "4afd6dfc"; //"6b172a8b"; + private static final String SOURCE_COMMIT_HASH = "f239499b"; //"6b172a8b"; /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; @@ -168,20 +169,7 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception waitForCondition(() -> NODE_IDS.size() == finalIgnite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); - // Get discovery port via reflection on protected getSpi() of GridDiscoveryManager. - Object discoMgr = ignite.context().discovery(); - java.lang.reflect.Method getSpiMethod = discoMgr.getClass().getSuperclass().getDeclaredMethod("getSpi"); - boolean acc = getSpiMethod.canAccess(discoMgr); - getSpiMethod.setAccessible(true); - TcpDiscoverySpi localDiscoverySpi = (TcpDiscoverySpi) getSpiMethod.invoke(discoMgr); - if (!acc) - getSpiMethod.setAccessible(false); - - String hostAddr = IgniteContainer.hostMachineIpAddress() + ":" + localDiscoverySpi.getLocalPort(); - - System.out.println(">>> Local node external address: " + hostAddr); - - addrs.put(container.nodeId(), hostAddr); + addrs.put(container.nodeId(), ignite.cluster().localNode().addresses().stream().findFirst().orElseThrow()); nodes.add(ignite); } @@ -200,23 +188,24 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setNetworkTimeout(10000) .setAckTimeout(5000) .setJoinTimeout(10000) - .setLocalAddress("0.0.0.0") + //.setLocalAddress(InetAddress.getLocalHost().getHostAddress()) +// .setAddressFilter(addrs -> !(addrs.getHostString().contains("0.0.0.0") +// || addrs.getHostString().contains("127.0.0.1"))) .setLocalPort(48500) .setLocalPortRange(20); - TcpCommunicationSpi commSpi = new TcpCommunicationSpi() - .setLocalAddress("0.0.0.0") - .setLocalPort(48400) - .setLocalPortRange(100); + TcpCommunicationSpi commSpi = new TcpCommunicationSpi(); + //.setLocalAddress("0.0.0.0"); + //.setLocalPort(47100) + //.setLocalPortRange(100); return new IgniteConfiguration() .setLocalHost("0.0.0.0") .setConsistentId(nodeId) .setWorkDirectory(workDir) .setDataStorageConfiguration(new DataStorageConfiguration().setDefaultDataRegionConfiguration(dataRegionCfg)) - .setDiscoverySpi(discoverySpi) - .setCommunicationSpi(commSpi) - .setAddressResolver(new HostNodeAddressResolver()); + .setDiscoverySpi(discoverySpi); + //.setCommunicationSpi(commSpi); } /** */ diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/CustomAddressResolver.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/CustomAddressResolver.java deleted file mode 100644 index d944a774199a5..0000000000000 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/CustomAddressResolver.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.compatibility.testframework.testcontainers; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import org.apache.ignite.configuration.AddressResolver; - -/** - * Address resolver for container-to-host communication. - *

- * When a container receives a discovery/communication address from a local (non-container) node, - * that address points to the host machine's real IP (e.g., 192.168.x.x), which from inside - * a Docker container on macOS (Docker Desktop VM) is not reachable. This resolver maps - * such addresses to {@code host.docker.internal}, a special Docker DNS name that resolves - * to the host machine from within containers. - */ -public class CustomAddressResolver implements AddressResolver { - /** - * Special Docker DNS name that resolves to the host machine from inside containers. - */ - private static final String HOST_DOCKER_INTERNAL = "host.docker.internal"; - - /** {@inheritDoc} */ - @Override public Collection getExternalAddresses(InetSocketAddress addr) { - if (addr == null) - return Collections.emptyList(); - - InetAddress internalAddr = addr.getAddress(); - - if (internalAddr == null) { - // Unresolved address — try to resolve it - try { - internalAddr = InetAddress.getByName(addr.getHostName()); - } - catch (UnknownHostException ignored) { - return List.of(addr); - } - } - - String ip = internalAddr.getHostAddress(); - - // Map loopback to host.docker.internal. - // When a local (non-container) node joins with setLocalHost("127.0.0.1"), - // from inside the container 127.0.0.1 means the container itself, not the host. - if (ip.equals("127.0.0.1")) { - try { - return List.of(new InetSocketAddress( - InetAddress.getByName(HOST_DOCKER_INTERNAL), addr.getPort())); - } - catch (UnknownHostException e) { - return List.of(new InetSocketAddress(HOST_DOCKER_INTERNAL, addr.getPort())); - } - } - - // Map the host machine's private IP to host.docker.internal. - // Docker containers on macOS (Docker Desktop VM) cannot reach the host's real IP (192.168.x.x). - // Container IPs in Testcontainers network are typically 172.16.x.x-172.31.x.x, so we only map - // 192.168.x.x which is typical for macOS host interfaces. - if (ip.startsWith("192.168.")) { - try { - return List.of(new InetSocketAddress(InetAddress.getByName(HOST_DOCKER_INTERNAL), addr.getPort())); - } - catch (UnknownHostException e) { - return List.of(new InetSocketAddress(HOST_DOCKER_INTERNAL, addr.getPort())); - } - } - - // All other addresses (e.g., Docker network 172.x.x.x) are returned as-is. - // All other addresses are returned as-is. - // If the local node advertises its real IP (e.g., 192.168.x.x), containers will try to connect directly. - // If direct connection fails, they will fall back to other addresses including external ones. - return List.of(addr); - } -} diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 6f5ab94a58682..c1d8ce1990167 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -19,12 +19,9 @@ import java.io.IOException; import java.net.InetAddress; -import java.net.InterfaceAddress; -import java.net.NetworkInterface; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; -import java.util.Enumeration; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ignite.IgniteException; @@ -71,14 +68,6 @@ public class IgniteContainer extends GenericContainer { /** */ private static final Pattern CLUSTER_STATE_PATTERN = Pattern.compile("Cluster state: (ACTIVE|INACTIVE)"); - /** - * Returns the host machine gateway address reachable from Docker containers. - * Uses the Docker special DNS name that resolves to the host machine. - */ - public static String hostMachineIpAddress() { - return "host.docker.internal"; - } - /** Hostname. */ private final String hostname; @@ -100,7 +89,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withEnv("IGNITE_QUIET", "false"); withEnv("IGNITE_NODE_NAME", nodeId); withEnv("IGNITE_WORK_DIR", workDirPath); - //withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); + withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); @@ -109,6 +98,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withNetwork(net); withNetworkAliases(hostname); + withExtraHost("host-gateway", "host-gateway"); // InetAddress.getLocalHost().getHostAddress()); withExposedPorts(ClientConnectorConfiguration.DFLT_PORT, TcpCommunicationSpi.DFLT_PORT, TcpDiscoverySpi.DFLT_PORT); waitingFor(Wait.forLogMessage(".*Node started.*", 1) @@ -167,7 +157,7 @@ public String clientAddress() { /** */ public String discoveryAddress() { - return address(TcpDiscoverySpi.DFLT_PORT); + return address(TcpDiscoverySpi.DFLT_PORT).replace("localhost", "0.0.0.0"); // return "0.0.0.0:" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index d63576a2ef3a5..855b15f43cd76 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -37,11 +37,7 @@ - - - - @@ -49,7 +45,9 @@ node1:47500 node2:47500 node3:47500 - host-machine:48500..48520 + host-gateway:48500..48520 + 0.0.0.0:48500..48520 + 127.0.0.1:48500..48520 @@ -58,7 +56,6 @@ - From 386c34edf29b03082e29b8db6c0039c61c88ac13 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Tue, 23 Jun 2026 18:28:32 +0500 Subject: [PATCH 15/28] fix --- .gitignore | 2 + .../ru/IgniteRebalanceOnUpgradeTest.java | 86 +++++++------- .../ContainerAddressResolver.java | 45 ++++++++ .../IgniteClusterContainer.java | 5 +- .../testcontainers/IgniteContainer.java | 105 ++++++++++++++++-- .../src/test/resources/docker/test-config.xml | 16 +-- 6 files changed, 202 insertions(+), 57 deletions(-) create mode 100644 modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java diff --git a/.gitignore b/.gitignore index 06151abef9940..5b1aca3fd998a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ git-patch-prop-local.sh **/dotnet/libs/ *.classname* *.exe +deliveries/docker/apache-ignite/x86_64/run.sh +deliveries/docker/apache-ignite/x86_64/apache-ignite-* #Ignore all Intellij IDEA files (except default inspections config) .idea/ diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index e63df3073038d..4c9a62afc89cb 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -18,10 +18,10 @@ package org.apache.ignite.compatibility.ru; import java.io.File; -//import java.net.InetAddress; -import java.net.UnknownHostException; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -100,7 +100,7 @@ public static void afterClass() { /** {@inheritDoc} */ @Override protected long getTestTimeout() { - return super.getTestTimeout() * 3; + return super.getTestTimeout() * 2; } /** Basic RU test. */ @@ -112,8 +112,6 @@ public void testRollingUpgrade() throws Exception { for (IgniteContainer container : cluster.containers()) addrs.put(container.nodeId(), container.discoveryAddress()); - System.out.println(">>> Addresses=" + addrs); - ClientCacheConfiguration cfg = new ClientCacheConfiguration() .setName(CACHE_NAME) .setBackups(1) @@ -145,38 +143,28 @@ public void testRollingUpgrade() throws Exception { /** */ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception { for (IgniteContainer container : srcCluster.containers()) { - System.out.println(">>> Upgrade " + container.nodeId()); + log.info(">>> Upgrade node=" + container.nodeId()); + + String hostIp = container.execInContainer("sh", "-c", + "getent ahostsv4 host.docker.internal | awk '{print $1}' | head -1").getStdout().trim(); container.stop(); addrs.remove(container.nodeId()); - System.out.println(">>> CONNECT TO=" + addrs.values()); - - IgniteEx ignite = null; - - try { - Thread.sleep(20_000); + IgniteEx ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), addrs.values(), hostIp)); - ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), addrs.values())); - } - catch (Exception ex) { - System.out.println(">>> ERR=" + ex); - Thread.sleep(Long.MAX_VALUE); - } + waitForCondition(() -> NODE_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); - IgniteEx finalIgnite = ignite; - - waitForCondition(() -> NODE_IDS.size() == finalIgnite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); - - addrs.put(container.nodeId(), ignite.cluster().localNode().addresses().stream().findFirst().orElseThrow()); + // Already-upgraded host nodes live in this JVM on localhost within the discovery port range. + addrs.put(container.nodeId(), "127.0.0.1:48500..48599"); nodes.add(ignite); } } /** */ - private IgniteConfiguration configuration(String nodeId, String workDir, Collection addrs0) throws UnknownHostException { + private IgniteConfiguration configuration(String nodeId, String workDir, Collection addrs0, String ip) { DataRegionConfiguration dataRegionCfg = new DataRegionConfiguration() .setName("testRegion") .setInitialSize(1024L * 1024 * 1024) @@ -184,28 +172,50 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setPersistenceEnabled(true); TcpDiscoverySpi discoverySpi = new TcpDiscoverySpi() + .setLocalAddress("0.0.0.0") .setIpFinder(new TcpDiscoveryVmIpFinder().setAddresses(addrs0)) - .setNetworkTimeout(10000) + // Short socket timeout: unreachable container-internal addresses must fail fast before the + // host-reachable 127.0.0.1: (advertised by the containers) is tried. + .setSocketTimeout(1000) + .setNetworkTimeout(20000) .setAckTimeout(5000) - .setJoinTimeout(10000) - //.setLocalAddress(InetAddress.getLocalHost().getHostAddress()) -// .setAddressFilter(addrs -> !(addrs.getHostString().contains("0.0.0.0") -// || addrs.getHostString().contains("127.0.0.1"))) + .setJoinTimeout(30000) .setLocalPort(48500) - .setLocalPortRange(20); - - TcpCommunicationSpi commSpi = new TcpCommunicationSpi(); - //.setLocalAddress("0.0.0.0"); - //.setLocalPort(47100) - //.setLocalPortRange(100); + .setLocalPortRange(100); + + // Bind communication to loopback (discovery stays on 0.0.0.0 to satisfy Ignite's non-loopback join + // check) so the node advertises only 127.0.0.1 + the resolver-mapped Docker host address -- no + // unreachable host LAN IPs for the containers to stall on. A short connect timeout makes the node's + // own outgoing attempts to unreachable container-internal (172.x) addresses give up in ~1s (they + // otherwise hang in SYN_SENT) and fall through to the reachable 127.0.0.1:. + TcpCommunicationSpi commSpi = new TcpCommunicationSpi() + .setLocalAddress("127.0.0.1") + .setLocalPort(49100) + .setConnectTimeout(1000) + .setMaxConnectTimeout(10000) + // The NIO connect to a blackholed container-internal (172.x) address is not aborted by + // connectTimeout on macOS (it hangs in SYN_SENT for the OS timeout, ~75s), stalling the exchange. + // Pre-filter unreachable addresses so only the reachable 127.0.0.1: is used. + .setFilterReachableAddresses(true); return new IgniteConfiguration() - .setLocalHost("0.0.0.0") + .setIgniteInstanceName(nodeId) .setConsistentId(nodeId) .setWorkDirectory(workDir) .setDataStorageConfiguration(new DataStorageConfiguration().setDefaultDataRegionConfiguration(dataRegionCfg)) - .setDiscoverySpi(discoverySpi); - //.setCommunicationSpi(commSpi); + .setDiscoverySpi(discoverySpi) + .setAddressResolver(addr -> { + int port = addr.getPort(); + + // Each sequentially started host node binds the next port in the discovery (48500+) and + // communication (47100+) ranges; map them all to the Docker host address so the containers + // can reach every host JVM node. + if ((port >= 48500 && port < 48600) || (port >= 49100 && port < 49200)) + return Collections.singleton(new InetSocketAddress(ip, port)); + + return Collections.singleton(addr); + }) + .setCommunicationSpi(commSpi); } /** */ diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java new file mode 100644 index 0000000000000..1fa6e0b9e289c --- /dev/null +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java @@ -0,0 +1,45 @@ +/* + * 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.compatibility.testframework.testcontainers; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.Collections; +import org.apache.ignite.configuration.AddressResolver; + +/** + * Advertises a containerized node by the host-published port so a host-JVM node can reach it on macOS, + * where container-internal addresses are not routable. External address per bound port is taken from the + * {@code external.address.} system property (e.g. {@code -Dexternal.address.47500=127.0.0.1:50500}). + */ +public class ContainerAddressResolver implements AddressResolver { + /** Prefix of the system property that holds the external {@code host:port} for a given bound port. */ + public static final String EXT_ADDR_PROP_PREFIX = "external.address."; + + /** {@inheritDoc} */ + @Override public Collection getExternalAddresses(InetSocketAddress addr) { + String ext = System.getProperty(EXT_ADDR_PROP_PREFIX + addr.getPort()); + + if (ext == null) + return Collections.singletonList(addr); + + int sep = ext.lastIndexOf(':'); + + return Collections.singletonList(new InetSocketAddress(ext.substring(0, sep), Integer.parseInt(ext.substring(sep + 1)))); + } +} diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java index 0bfc28f1474d7..7cec032d329f0 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.testcontainers.containers.Network; import org.testcontainers.lifecycle.Startable; @@ -39,7 +40,7 @@ public IgniteClusterContainer(String commitHash, List nodeIds) throws IO for (int i = 0; i < nodeIds.size(); i++) { String hostname = "node" + (1 + i); - IgniteContainer ignite = new IgniteContainer(commitHash, net, hostname, nodeIds.get(i)); + IgniteContainer ignite = new IgniteContainer(commitHash, net, hostname, nodeIds.get(i), i); containers.add(ignite); } @@ -62,6 +63,6 @@ public IgniteClusterContainer(String commitHash, List nodeIds) throws IO /** */ public List containers() { - return containers; + return Collections.unmodifiableList(containers); } } diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index c1d8ce1990167..863a132d4c7f0 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -17,11 +17,15 @@ package org.apache.ignite.compatibility.testframework.testcontainers; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; -import java.net.InetAddress; +import java.io.InputStream; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ignite.IgniteException; @@ -38,8 +42,11 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; +import static org.apache.ignite.compatibility.testframework.testcontainers.ContainerAddressResolver.EXT_ADDR_PROP_PREFIX; import static org.apache.ignite.testframework.GridTestUtils.waitForCondition; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; import static org.testcontainers.utility.MountableFile.forClasspathResource; +import static org.testcontainers.utility.MountableFile.forHostPath; /** Ignite container. */ public class IgniteContainer extends GenericContainer { @@ -68,6 +75,18 @@ public class IgniteContainer extends GenericContainer { /** */ private static final Pattern CLUSTER_STATE_PATTERN = Pattern.compile("Cluster state: (ACTIVE|INACTIVE)"); + /** Base host port for the published discovery port (node index added). Kept clear of the host-node ports. */ + private static final int DISCO_HOST_PORT_BASE = 50500; + + /** Base host port for the published communication port (node index added). */ + private static final int COMM_HOST_PORT_BASE = 50100; + + /** Base host port for the published thin-client port (node index added). */ + private static final int CLIENT_HOST_PORT_BASE = 50800; + + /** Jar holding {@link ContainerAddressResolver}, injected so the old image can load it. */ + private static volatile File resolverJar; + /** Hostname. */ private final String hostname; @@ -78,13 +97,16 @@ public class IgniteContainer extends GenericContainer { private final String workDirPath; /** Constructor. */ - public IgniteContainer(String commitHash, Network net, String hostname, String nodeId) throws IOException { + public IgniteContainer(String commitHash, Network net, String hostname, String nodeId, int idx) throws IOException { super(DockerImageName.parse("apacheignite/ignite:" + commitHash)); this.hostname = hostname; this.nodeId = nodeId; workDirPath = WORK_DIR_PATH + "/" + hostname; + int discoHostPort = DISCO_HOST_PORT_BASE + idx; + int commHostPort = COMM_HOST_PORT_BASE + idx; + withEnv("CONFIG_URI", "file://" + CFG_PATH); withEnv("IGNITE_QUIET", "false"); withEnv("IGNITE_NODE_NAME", nodeId); @@ -92,19 +114,55 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); + // On macOS the host JVM cannot reach container-internal addresses, so each node advertises its + // host-published discovery/communication ports (127.0.0.1:hostPort) via ContainerAddressResolver. + // node.consistent.id pins the node's consistent id (and thus its persistence folder) to nodeId so the + // upgraded host node, started with the same consistent id, inherits this node's persisted data. + withEnv("JVM_OPTS", "-Xms512m -Xmx1g" + " -Dnode.consistent.id=" + nodeId + + " -D" + EXT_ADDR_PROP_PREFIX + TcpDiscoverySpi.DFLT_PORT + "=127.0.0.1:" + discoHostPort + + " -D" + EXT_ADDR_PROP_PREFIX + TcpCommunicationSpi.DFLT_PORT + "=127.0.0.1:" + commHostPort); + withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); + withCopyFileToContainer(forHostPath(resolverJar().getAbsolutePath()), ROOT_DIR_PATH + "libs/container-addr-resolver.jar"); withNetwork(net); withNetworkAliases(hostname); - withExtraHost("host-gateway", "host-gateway"); // InetAddress.getLocalHost().getHostAddress()); - withExposedPorts(ClientConnectorConfiguration.DFLT_PORT, TcpCommunicationSpi.DFLT_PORT, TcpDiscoverySpi.DFLT_PORT); + // Fixed host ports so the host JVM node can target each container deterministically. + addFixedExposedPort(CLIENT_HOST_PORT_BASE + idx, ClientConnectorConfiguration.DFLT_PORT); + addFixedExposedPort(commHostPort, TcpCommunicationSpi.DFLT_PORT); + addFixedExposedPort(discoHostPort, TcpDiscoverySpi.DFLT_PORT); waitingFor(Wait.forLogMessage(".*Node started.*", 1) .withStartupTimeout(Duration.ofSeconds(600))); } + /** {@inheritDoc} */ + @Override public void stop() { + if (isRunning()) { + try { + LOGGER.info("Sending SIGTERM to Ignite node {} for graceful shutdown...", hostname); + + getDockerClient().killContainerCmd(getContainerId()) + .withSignal("TERM") + .exec(); + + await() + .atMost(Duration.ofSeconds(20)) + .pollInterval(Duration.ofMillis(500)) + .until(() -> !isRunning()); + } + catch (Exception e) { + LOGGER.warn("Graceful shutdown failed for node {}. Proceeding with forceful stop.", hostname, e); + } + } + + LOGGER.info("Ignite node {} shut down gracefully.", hostname); + + super.stop(); + } + /** @return Node ID. */ public String nodeId() { return nodeId; @@ -157,12 +215,7 @@ public String clientAddress() { /** */ public String discoveryAddress() { - return address(TcpDiscoverySpi.DFLT_PORT).replace("localhost", "0.0.0.0"); - -// return "0.0.0.0:" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); - -// return getContainerInfo().getNetworkSettings().getNetworks().values() -// .iterator().next().getIpAddress() + ":" + getMappedPort(TcpDiscoverySpi.DFLT_PORT); + return address(TcpDiscoverySpi.DFLT_PORT); } /** */ @@ -195,4 +248,36 @@ private String execControl(String... cmd) { return result.getStdout(); } + + /** @return Jar with {@link ContainerAddressResolver}, built once and reused for all containers. */ + private static File resolverJar() throws IOException { + File jar = resolverJar; + + if (jar != null) + return jar; + + synchronized (IgniteContainer.class) { + if (resolverJar != null) + return resolverJar; + + String clsPath = ContainerAddressResolver.class.getName().replace('.', '/') + ".class"; + + jar = File.createTempFile("container-addr-resolver", ".jar"); + jar.deleteOnExit(); + + try (InputStream in = IgniteContainer.class.getClassLoader().getResourceAsStream(clsPath); + JarOutputStream out = new JarOutputStream(new FileOutputStream(jar))) { + if (in == null) + throw new IOException("Resolver class not found on classpath: " + clsPath); + + out.putNextEntry(new JarEntry(clsPath)); + + in.transferTo(out); + + out.closeEntry(); + } + + return resolverJar = jar; + } + } } diff --git a/modules/compatibility/src/test/resources/docker/test-config.xml b/modules/compatibility/src/test/resources/docker/test-config.xml index 855b15f43cd76..951856c61567b 100644 --- a/modules/compatibility/src/test/resources/docker/test-config.xml +++ b/modules/compatibility/src/test/resources/docker/test-config.xml @@ -23,6 +23,13 @@ http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + + + + + + + @@ -38,6 +45,7 @@ + @@ -45,9 +53,6 @@ node1:47500 node2:47500 node3:47500 - host-gateway:48500..48520 - 0.0.0.0:48500..48520 - 127.0.0.1:48500..48520 @@ -59,8 +64,5 @@ - - - - \ No newline at end of file + From 5ab41febd3f7d29c04056629483c3595cb8ce599 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 25 Jun 2026 12:50:06 +0500 Subject: [PATCH 16/28] cleanup --- .gitignore | 2 + modules/compatibility/DEVNOTES.md | 63 +++++++ modules/compatibility/pom.xml | 5 +- .../ru/IgniteRebalanceOnUpgradeTest.java | 45 +++-- .../ContainerAddressResolver.java | 2 +- .../IgniteClusterContainer.java | 13 +- .../testcontainers/IgniteContainer.java | 75 ++++---- .../resources/docker/build_docker_image.sh | 167 ++++++++++++------ .../src/test/resources/log4j2-test.xml | 34 ++++ .../spi/discovery/tcp/TcpDiscoverySpi.java | 2 +- 10 files changed, 285 insertions(+), 123 deletions(-) create mode 100644 modules/compatibility/DEVNOTES.md create mode 100644 modules/compatibility/src/test/resources/log4j2-test.xml diff --git a/.gitignore b/.gitignore index 5b1aca3fd998a..b6f57f8f709be 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ git-patch-prop-local.sh **/dotnet/libs/ *.classname* *.exe +deliveries/docker/apache-ignite/arm64/run.sh +deliveries/docker/apache-ignite/arm64/apache-ignite-* deliveries/docker/apache-ignite/x86_64/run.sh deliveries/docker/apache-ignite/x86_64/apache-ignite-* diff --git a/modules/compatibility/DEVNOTES.md b/modules/compatibility/DEVNOTES.md new file mode 100644 index 0000000000000..fd308d9937141 --- /dev/null +++ b/modules/compatibility/DEVNOTES.md @@ -0,0 +1,63 @@ + + +# Compatibility Module — Developer Notes + +## Prerequisites + +- [Docker](https://www.docker.com/get-started) (Docker Desktop or Docker Engine) must be installed and running. + +## Running IgniteRebalanceOnUpgradeTest + +This test verifies that data rebalancing works correctly when upgrading Ignite from a specific version to the current codebase. It uses Docker containers running the "old" Ignite version. + +### Step 1. Build a local Docker image for the source (old) version + +Run the following script from the **project root**, passing the commit hash of the version you want to test against: + +```bash +./modules/compatibility/src/test/resources/docker/build_docker_image.sh +``` + +> **Note:** If you omit ``, the script will use the hash of the latest commit in the current branch. + +> **Note:** If your corporate network uses SSL inspection and you get `TLS: server certificate not trusted`, the script will automatically patch Dockerfiles to use HTTP repositories and restore them after the build. + +The script will: +1. Checkout the specified commit. +2. Build the project (`./mvnw clean install -T1C -Pall-java,licenses -DskipTests`). +3. Initialize the release (`./mvnw initialize -Prelease`). +4. Build a Docker image tagged as `apacheignite/ignite:`. +5. Restore the original git state. + +> **Note:** If a distribution archive already exists in `target/bin/`, the build steps will be skipped. + +### Step 2. Set the commit hash in the test + +Open `org.apache.ignite.compatibility.ru.IgniteRebalanceOnUpgradeTest` and set `SOURCE_COMMIT_HASH` to the **same commit hash** used in Step 1: + +```java +private static final String SOURCE_COMMIT_HASH = ""; +``` + +### Step 3. Run the test + +Run `IgniteRebalanceOnUpgradeTest` from your IDE or via Maven: + +```bash +./mvnw test -pl modules/compatibility -Dtest=IgniteRebalanceOnUpgradeTest +``` diff --git a/modules/compatibility/pom.xml b/modules/compatibility/pom.xml index fee5e8b1f8393..9a18c1c9957dc 100644 --- a/modules/compatibility/pom.xml +++ b/modules/compatibility/pom.xml @@ -131,9 +131,8 @@ - org.slf4j - slf4j-simple - 2.0.12 + org.apache.logging.log4j + log4j-slf4j2-impl test diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 4c9a62afc89cb..834ceeadaa147 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -21,10 +21,10 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.ignite.IgniteCache; import org.apache.ignite.Ignition; import org.apache.ignite.cache.CacheAtomicityMode; @@ -53,15 +53,15 @@ /** Smoke test for rolling upgrade with persistence. */ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { - /** Node IDs. */ - private static final List NODE_IDS = List.of( + /** Consistent ID's. */ + private static final List CONSISTENT_IDS = List.of( "ad26bff6-5ff5-49f1-9a61-425a827953ed", "c1099d16-e7d7-49f4-925c-53329286c444", "7b880b69-8a9e-4b84-b555-250d365e2e67" ); - /** Source commit hash. */ - private static final String SOURCE_COMMIT_HASH = "f239499b"; //"6b172a8b"; + /** Source commit hash. Used for docker image tag. */ + private static final String SOURCE_COMMIT_HASH = "f239499b"; /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; @@ -69,22 +69,19 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { /** Local work directory. */ private static final File LOCAL_WORK_DIR = new File(LOCAL_WORK_DIR_PATH); - /** Thin client. */ - private IgniteClient client; - - /** */ + /** Local nodes. */ private final List nodes = new ArrayList<>(); - /** */ + /** Consistent ID -> discovery address. */ private final Map addrs = new HashMap<>(); + /** Thin client. */ + private IgniteClient client; + /** */ @BeforeClass public static void beforeClass() { U.delete(LOCAL_WORK_DIR); - - System.setProperty("java.net.preferIPv4Stack", "true"); - System.setProperty("java.net.preferIPv6Addresses", "false"); } /** */ @@ -106,11 +103,11 @@ public static void afterClass() { /** Basic RU test. */ @Test public void testRollingUpgrade() throws Exception { - try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_COMMIT_HASH, NODE_IDS)) { + try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_COMMIT_HASH, CONSISTENT_IDS)) { cluster.start(); for (IgniteContainer container : cluster.containers()) - addrs.put(container.nodeId(), container.discoveryAddress()); + addrs.put(container.consistentId(), container.discoveryAddress()); ClientCacheConfiguration cfg = new ClientCacheConfiguration() .setName(CACHE_NAME) @@ -143,21 +140,21 @@ public void testRollingUpgrade() throws Exception { /** */ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception { for (IgniteContainer container : srcCluster.containers()) { - log.info(">>> Upgrade node=" + container.nodeId()); + log.info(">>> Upgrade node=" + container.consistentId()); String hostIp = container.execInContainer("sh", "-c", "getent ahostsv4 host.docker.internal | awk '{print $1}' | head -1").getStdout().trim(); container.stop(); - addrs.remove(container.nodeId()); + addrs.remove(container.consistentId()); - IgniteEx ignite = startGrid(configuration(container.nodeId(), container.localWorkDirectory(), addrs.values(), hostIp)); + IgniteEx ignite = startGrid(configuration(container.consistentId(), container.localWorkDirectory(), addrs.values(), hostIp)); - waitForCondition(() -> NODE_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); + waitForCondition(() -> CONSISTENT_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); // Already-upgraded host nodes live in this JVM on localhost within the discovery port range. - addrs.put(container.nodeId(), "127.0.0.1:48500..48599"); + addrs.put(container.consistentId(), "127.0.0.1:48500..48599"); nodes.add(ignite); } @@ -178,10 +175,8 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect // host-reachable 127.0.0.1: (advertised by the containers) is tried. .setSocketTimeout(1000) .setNetworkTimeout(20000) - .setAckTimeout(5000) .setJoinTimeout(30000) - .setLocalPort(48500) - .setLocalPortRange(100); + .setLocalPort(48500); // Bind communication to loopback (discovery stays on 0.0.0.0 to satisfy Ignite's non-loopback join // check) so the node advertises only 127.0.0.1 + the resolver-mapped Docker host address -- no @@ -211,9 +206,9 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect // communication (47100+) ranges; map them all to the Docker host address so the containers // can reach every host JVM node. if ((port >= 48500 && port < 48600) || (port >= 49100 && port < 49200)) - return Collections.singleton(new InetSocketAddress(ip, port)); + return Set.of(new InetSocketAddress(ip, port)); - return Collections.singleton(addr); + return Set.of(addr); }) .setCommunicationSpi(commSpi); } diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java index 1fa6e0b9e289c..d841456600a78 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java @@ -29,7 +29,7 @@ */ public class ContainerAddressResolver implements AddressResolver { /** Prefix of the system property that holds the external {@code host:port} for a given bound port. */ - public static final String EXT_ADDR_PROP_PREFIX = "external.address."; + static final String EXT_ADDR_PROP_PREFIX = "external.address."; /** {@inheritDoc} */ @Override public Collection getExternalAddresses(InetSocketAddress addr) { diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java index 7cec032d329f0..705b7c289303b 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteClusterContainer.java @@ -33,14 +33,17 @@ public class IgniteClusterContainer implements Startable { /** Network. */ private final Network net = Network.newNetwork(); - /** @param commitHash Commit hash. */ - public IgniteClusterContainer(String commitHash, List nodeIds) throws IOException { - containers = new ArrayList<>(nodeIds.size()); + /** + * @param commitHash Commit hash. + * @param consistentIds Consistent ID's. + */ + public IgniteClusterContainer(String commitHash, List consistentIds) throws IOException { + containers = new ArrayList<>(consistentIds.size()); - for (int i = 0; i < nodeIds.size(); i++) { + for (int i = 0; i < consistentIds.size(); i++) { String hostname = "node" + (1 + i); - IgniteContainer ignite = new IgniteContainer(commitHash, net, hostname, nodeIds.get(i), i); + IgniteContainer ignite = new IgniteContainer(commitHash, net, hostname, consistentIds.get(i), i); containers.add(ignite); } diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 863a132d4c7f0..2e63b57ac95a1 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -24,12 +24,14 @@ import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; +import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ignite.IgniteException; import org.apache.ignite.cluster.ClusterState; +import org.apache.ignite.compatibility.testframework.plugins.TestCompatibilityPluginProvider; import org.apache.ignite.configuration.ClientConnectorConfiguration; import org.apache.ignite.internal.IgniteInterruptedCheckedException; import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi; @@ -51,11 +53,11 @@ /** Ignite container. */ public class IgniteContainer extends GenericContainer { /** Property for local work directory. */ - static final String LOCAL_WORK_DIR_PROP = "local.work.dir"; + private static final String LOCAL_WORK_DIR_PROP = "local.work.dir"; /** Local work directory. */ public static final String LOCAL_WORK_DIR_PATH = System.getProperty(LOCAL_WORK_DIR_PROP, - System.getProperty("user.home") + "/test-ignite-work"); + System.getProperty("user.dir") + "/target/test-ignite-work"); /** Logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(IgniteContainer.class); @@ -84,24 +86,30 @@ public class IgniteContainer extends GenericContainer { /** Base host port for the published thin-client port (node index added). */ private static final int CLIENT_HOST_PORT_BASE = 50800; - /** Jar holding {@link ContainerAddressResolver}, injected so the old image can load it. */ - private static volatile File resolverJar; + /** Custom classes used by node in containers. */ + private static final List TEST_CLASSES = List.of( + ContainerAddressResolver.class.getName(), + TestCompatibilityPluginProvider.class.getName() + ); + + /** Jar holding {@link #TEST_CLASSES}, injected so the old image can load it. */ + private static volatile File testClassesJar; /** Hostname. */ private final String hostname; - /** Node ID. */ - private final String nodeId; + /** Consistent ID. */ + private final String consistentId; /** Path to work directory. */ private final String workDirPath; /** Constructor. */ - public IgniteContainer(String commitHash, Network net, String hostname, String nodeId, int idx) throws IOException { + public IgniteContainer(String commitHash, Network net, String hostname, String consistentId, int idx) throws IOException { super(DockerImageName.parse("apacheignite/ignite:" + commitHash)); this.hostname = hostname; - this.nodeId = nodeId; + this.consistentId = consistentId; workDirPath = WORK_DIR_PATH + "/" + hostname; int discoHostPort = DISCO_HOST_PORT_BASE + idx; @@ -109,26 +117,28 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n withEnv("CONFIG_URI", "file://" + CFG_PATH); withEnv("IGNITE_QUIET", "false"); - withEnv("IGNITE_NODE_NAME", nodeId); withEnv("IGNITE_WORK_DIR", workDirPath); withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); // On macOS the host JVM cannot reach container-internal addresses, so each node advertises its // host-published discovery/communication ports (127.0.0.1:hostPort) via ContainerAddressResolver. - // node.consistent.id pins the node's consistent id (and thus its persistence folder) to nodeId so the + // node.consistent.id pins the node's consistent id (and thus its persistence folder) to consistentId so the // upgraded host node, started with the same consistent id, inherits this node's persisted data. - withEnv("JVM_OPTS", "-Xms512m -Xmx1g" + " -Dnode.consistent.id=" + nodeId + withEnv("JVM_OPTS", "-Xms512m -Xmx1g" + " -Dnode.consistent.id=" + consistentId + " -D" + EXT_ADDR_PROP_PREFIX + TcpDiscoverySpi.DFLT_PORT + "=127.0.0.1:" + discoHostPort + " -D" + EXT_ADDR_PROP_PREFIX + TcpCommunicationSpi.DFLT_PORT + "=127.0.0.1:" + commHostPort); withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); - withCopyFileToContainer(forHostPath(resolverJar().getAbsolutePath()), ROOT_DIR_PATH + "libs/container-addr-resolver.jar"); + withCopyFileToContainer(forHostPath(testClassesJar().getAbsolutePath()), ROOT_DIR_PATH + "libs/test-classes.jar"); withNetwork(net); withNetworkAliases(hostname); + // Stream container logs to stdout so they appear in the IDE test runner. + withLogConsumer(frame -> System.out.println("[" + consistentId + "] " + frame.getUtf8String().trim())); + // Fixed host ports so the host JVM node can target each container deterministically. addFixedExposedPort(CLIENT_HOST_PORT_BASE + idx, ClientConnectorConfiguration.DFLT_PORT); addFixedExposedPort(commHostPort, TcpCommunicationSpi.DFLT_PORT); @@ -163,9 +173,9 @@ public IgniteContainer(String commitHash, Network net, String hostname, String n super.stop(); } - /** @return Node ID. */ - public String nodeId() { - return nodeId; + /** @return Consistent ID. */ + public String consistentId() { + return consistentId; } /** */ @@ -249,35 +259,38 @@ private String execControl(String... cmd) { return result.getStdout(); } - /** @return Jar with {@link ContainerAddressResolver}, built once and reused for all containers. */ - private static File resolverJar() throws IOException { - File jar = resolverJar; + /** @return Jar with {@link #TEST_CLASSES}, built once and reused for all containers. */ + private static File testClassesJar() throws IOException { + File jar = testClassesJar; if (jar != null) return jar; synchronized (IgniteContainer.class) { - if (resolverJar != null) - return resolverJar; + if (testClassesJar != null) + return testClassesJar; - String clsPath = ContainerAddressResolver.class.getName().replace('.', '/') + ".class"; - - jar = File.createTempFile("container-addr-resolver", ".jar"); + jar = File.createTempFile("test-classes", ".jar"); jar.deleteOnExit(); - try (InputStream in = IgniteContainer.class.getClassLoader().getResourceAsStream(clsPath); - JarOutputStream out = new JarOutputStream(new FileOutputStream(jar))) { - if (in == null) - throw new IOException("Resolver class not found on classpath: " + clsPath); + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar))) { + for (String cls : TEST_CLASSES) { + String clsPath = cls.replace('.', '/') + ".class"; + + try (InputStream in = IgniteContainer.class.getClassLoader().getResourceAsStream(clsPath)) { + if (in == null) + throw new IOException("Class not found on classpath: " + clsPath); - out.putNextEntry(new JarEntry(clsPath)); + out.putNextEntry(new JarEntry(clsPath)); - in.transferTo(out); + in.transferTo(out); - out.closeEntry(); + out.closeEntry(); + } + } } - return resolverJar = jar; + return testClassesJar = jar; } } } diff --git a/modules/compatibility/src/test/resources/docker/build_docker_image.sh b/modules/compatibility/src/test/resources/docker/build_docker_image.sh index 3e3225fb80661..7aed7f5b52643 100755 --- a/modules/compatibility/src/test/resources/docker/build_docker_image.sh +++ b/modules/compatibility/src/test/resources/docker/build_docker_image.sh @@ -3,45 +3,96 @@ # Get the absolute path to the project root directory PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../../../" && pwd)" -# Check that commit hash is provided -if [ $# -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -COMMIT_HASH=$1 - # Change to the project root directory cd "$PROJECT_ROOT" || exit 1 -# 1) Perform git checkout to the specified commit -echo -e "\nPerforming git checkout to commit: $COMMIT_HASH" -git checkout "$COMMIT_HASH" +# Save current git state (branch name and commit hash) +ORIGINAL_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "") +ORIGINAL_COMMIT=$(git rev-parse HEAD) -# 2) Build the project -echo -e "\nBuilding the project: ./mvnw clean install -Pall-java,licenses -DskipTests" -./mvnw clean install -Pall-java,licenses -DskipTests +# Track whether Dockerfiles were patched (for cleanup on exit) +DOCKERFILES_PATCHED=0 +PATCHED_DOCKERFILES="" -# Check success of previous command -if [ $? -ne 0 ]; then - echo -e "\nError during project build" +# Function to restore original Dockerfiles +restore_dockerfiles() { + if [ "$DOCKERFILES_PATCHED" -eq 1 ] && [ -n "$PATCHED_DOCKERFILES" ]; then + echo -e "\nRestoring patched Dockerfiles" + git checkout "$ORIGINAL_COMMIT" -- $PATCHED_DOCKERFILES + fi +} + +# Function to restore original git state +restore_git_state() { + if [ -n "$ORIGINAL_BRANCH" ]; then + echo -e "\nRestoring git state to branch: $ORIGINAL_BRANCH" + git checkout "$ORIGINAL_BRANCH" 2>/dev/null + else + echo -e "\nRestoring git state to detached commit: $ORIGINAL_COMMIT" + git checkout "$ORIGINAL_COMMIT" 2>/dev/null + fi + restore_dockerfiles +} + +# Set trap to restore git state on exit (success or failure) +trap restore_git_state EXIT + +# Check that commit hash is provided, or use the latest commit in current branch +if [ $# -eq 1 ]; then + COMMIT_HASH=$1 +elif [ $# -eq 0 ]; then + COMMIT_HASH=$(git rev-parse HEAD) +else + echo "Usage: $0 [commit_hash]" exit 1 fi -# 3) Initialize the release -echo -e "\nInitializing release: ./mvnw initialize -Prelease" -./mvnw initialize -Prelease +# Perform git checkout to the specified commit +echo -e "\nPerforming git checkout to commit: $COMMIT_HASH" +git checkout "$COMMIT_HASH" -# Check success of previous command -if [ $? -ne 0 ]; then - echo -e "\nError during release initialization" - exit 1 +# Build the project (skip if distribution archive already exists) +SOURCE_ARCHIVE="$PROJECT_ROOT/target/bin/apache-ignite-*.zip" + +if ls $SOURCE_ARCHIVE 1> /dev/null 2>&1; then + echo -e "\nDistribution archive already found, skipping build steps" +else + echo -e "\nBuilding the project: ./mvnw clean install -T1C -Pall-java,licenses -DskipTests" + ./mvnw clean install -T1C -Pall-java,licenses -DskipTests + + # Check success of previous command + if [ $? -ne 0 ]; then + echo -e "\nError during project build" + exit 1 + fi + + # Initialize the release + echo -e "\nInitializing release: ./mvnw initialize -Prelease" + ./mvnw initialize -Prelease + + # Check success of previous command + if [ $? -ne 0 ]; then + echo -e "\nError during release initialization" + exit 1 + fi fi -# 4) Copy and unpack the release archive +# Copy and unpack the release archive echo -e "\nCopying and unpacking the release archive" -TARGET_DIR="$PROJECT_ROOT/deliveries/docker/apache-ignite/x86_64" -SOURCE_ARCHIVE="$PROJECT_ROOT/target/bin/apache-ignite-*.zip" +# Detect CPU architecture +ARCH=$(uname -m) +case "$ARCH" in + arm64|aarch64) + TARGET_DIR="$PROJECT_ROOT/deliveries/docker/apache-ignite/arm64" + ;; + x86_64) + TARGET_DIR="$PROJECT_ROOT/deliveries/docker/apache-ignite/x86_64" + ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; +esac # Check if archive exists if [ ! -f $(echo $SOURCE_ARCHIVE) ]; then @@ -54,49 +105,51 @@ cp $(echo $SOURCE_ARCHIVE) "$TARGET_DIR/" # Unpack the archive cd "$TARGET_DIR" -unzip "$(basename $(echo $SOURCE_ARCHIVE))" +unzip -o "$(basename $(echo $SOURCE_ARCHIVE))" # Return to project root directory cd "$PROJECT_ROOT" -# 5) Copy the startup script +# Copy the startup script echo -e "\nCopying startup script" cp "$PROJECT_ROOT/deliveries/docker/apache-ignite/run.sh" "$TARGET_DIR/" -# 6) Copy compatibility tests jar file to libs directory -echo -e "\nCopying compatibility jar to libs directory" -# Find the tests jar file in the target directory -COMPATIBILITY_JAR="" -for file in "$PROJECT_ROOT/modules/compatibility/target"/ignite-compatibility-*-tests.jar; do - if [ -f "$file" ]; then - COMPATIBILITY_JAR="$file" - break - fi +# Patch Dockerfiles: switch apk repos to HTTP (bypasses TLS cert issues in corporate networks) +PATCHED_DOCKERFILES=$(find "$PROJECT_ROOT/deliveries/docker/apache-ignite" -name Dockerfile) +for DF in $PATCHED_DOCKERFILES; do + echo -e "\nPatching $DF (Debian base image instead of Alpine)" + awk '{ + # Replace Alpine base image with Debian (avoids TLS cert issues in corporate networks) + if (/jre-alpine/) { + sub(/jre-alpine/, "jre") + print + next + } + # Skip the apk block entirely (Debian has bash built-in) + if (/^RUN apk/) { + skip = 1 + next + } + if (skip && /^[[:space:]]*add bash/) { + skip = 0 + next + } + print + }' "$DF" > "${DF}.tmp" && mv "${DF}.tmp" "$DF" done +DOCKERFILES_PATCHED=1 -if [ -n "$COMPATIBILITY_JAR" ] && [ -f "$COMPATIBILITY_JAR" ]; then - # Get the actual jar file name - ACTUAL_JAR=$(basename "$COMPATIBILITY_JAR") - # Find the actual ignite directory name after unpacking - IGNITE_DIR=$(ls -d "$TARGET_DIR"/apache-ignite-* 2>/dev/null | head -n 1) - if [ -n "$IGNITE_DIR" ]; then - # Copy to libs directory inside unpacked distribution - cp "$COMPATIBILITY_JAR" "$IGNITE_DIR/libs/" - echo -e "Compatibility tests jar copied successfully to libs directory" - else - echo -e "\nIgnite directory not found" - exit 1 - fi -else - echo -e "\nCompatibility tests jar not found" - exit 1 -fi - -# 7) Build Docker image +# Build Docker image echo -e "\nBuilding Docker image - apacheignite/ignite:$COMMIT_HASH" cd "$TARGET_DIR" docker build . -t apacheignite/ignite:"$COMMIT_HASH" +# Check success of previous command +if [ $? -ne 0 ]; then + echo -e "\nError during Docker image build" + exit 1 +fi + # Return to project root directory cd "$PROJECT_ROOT" diff --git a/modules/compatibility/src/test/resources/log4j2-test.xml b/modules/compatibility/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000000..10d539531882b --- /dev/null +++ b/modules/compatibility/src/test/resources/log4j2-test.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java index e28efcfb56c91..eb868103ad1c2 100644 --- a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java @@ -1269,7 +1269,7 @@ LinkedHashSet getEffectiveNodeAddresses(TcpDiscoveryNode node // Do not give own loopback to avoid requesting current node. if (!node.equals(locNode)) - addrs.removeIf(addr -> addr.getAddress() == null || addr.getAddress().isLoopbackAddress() && locNode.socketAddresses().contains(addr)); + addrs.removeIf(addr -> addr.getAddress().isLoopbackAddress() && locNode.socketAddresses().contains(addr)); addrs.sort(U.inetAddressesComparator(sameHost)); From 75bd37ef56b2ea533b474e99d00b06524ffc063f Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 25 Jun 2026 21:36:26 +0500 Subject: [PATCH 17/28] WIP --- .../org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java index eb868103ad1c2..51a8f2a47b0f1 100644 --- a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java @@ -1269,7 +1269,8 @@ LinkedHashSet getEffectiveNodeAddresses(TcpDiscoveryNode node // Do not give own loopback to avoid requesting current node. if (!node.equals(locNode)) - addrs.removeIf(addr -> addr.getAddress().isLoopbackAddress() && locNode.socketAddresses().contains(addr)); + addrs.removeIf(addr -> addr.getAddress() != null && addr.getAddress().isLoopbackAddress() + && locNode.socketAddresses().contains(addr)); addrs.sort(U.inetAddressesComparator(sameHost)); From 9ae1437c91497f301e13cee689870462f705c995 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 25 Jun 2026 21:48:38 +0500 Subject: [PATCH 18/28] WIP --- .../ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 834ceeadaa147..81f9664ed7eca 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -61,7 +61,7 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { ); /** Source commit hash. Used for docker image tag. */ - private static final String SOURCE_COMMIT_HASH = "f239499b"; + private static final String SOURCE_COMMIT_HASH = "75bd37ef56b2ea533b474e99d00b06524ffc063f"; /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; From 0ad4656eef09acda288cbad96f80f0138732d94a Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 25 Jun 2026 21:52:50 +0500 Subject: [PATCH 19/28] WIP --- .../org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java index 51a8f2a47b0f1..a4b81d292fbb3 100644 --- a/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySpi.java @@ -1269,8 +1269,8 @@ LinkedHashSet getEffectiveNodeAddresses(TcpDiscoveryNode node // Do not give own loopback to avoid requesting current node. if (!node.equals(locNode)) - addrs.removeIf(addr -> addr.getAddress() != null && addr.getAddress().isLoopbackAddress() - && locNode.socketAddresses().contains(addr)); + addrs.removeIf(addr -> addr.getAddress() == null || + (addr.getAddress().isLoopbackAddress() && locNode.socketAddresses().contains(addr))); addrs.sort(U.inetAddressesComparator(sameHost)); From 833455864e1c5dbf9b4eb932138b20091eedb18e Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 25 Jun 2026 22:00:05 +0500 Subject: [PATCH 20/28] WIP --- .../ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 81f9664ed7eca..f705684ddd503 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -61,7 +61,7 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { ); /** Source commit hash. Used for docker image tag. */ - private static final String SOURCE_COMMIT_HASH = "75bd37ef56b2ea533b474e99d00b06524ffc063f"; + private static final String SOURCE_COMMIT_HASH = "0ad4656eef09acda288cbad96f80f0138732d94a"; /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; From 4028bb2f7504abca53056301eebc370951bfeeee Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 25 Jun 2026 22:05:48 +0500 Subject: [PATCH 21/28] WIP --- .../testframework/testcontainers/IgniteContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 2e63b57ac95a1..f9db812d81a12 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -159,7 +159,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c .exec(); await() - .atMost(Duration.ofSeconds(20)) + .atMost(Duration.ofSeconds(60)) .pollInterval(Duration.ofMillis(500)) .until(() -> !isRunning()); } From c7b4eb96ee9a3e9c95439529346ccc76319cb1af Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 25 Jun 2026 22:14:11 +0500 Subject: [PATCH 22/28] fix checkstyle --- .../testcontainers/ContainerAddressResolver.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java index d841456600a78..e4630ca4f4e63 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/ContainerAddressResolver.java @@ -33,13 +33,13 @@ public class ContainerAddressResolver implements AddressResolver { /** {@inheritDoc} */ @Override public Collection getExternalAddresses(InetSocketAddress addr) { - String ext = System.getProperty(EXT_ADDR_PROP_PREFIX + addr.getPort()); + String ext = System.getProperty(EXT_ADDR_PROP_PREFIX + addr.getPort()); - if (ext == null) - return Collections.singletonList(addr); + if (ext == null) + return Collections.singletonList(addr); - int sep = ext.lastIndexOf(':'); + int sep = ext.lastIndexOf(':'); - return Collections.singletonList(new InetSocketAddress(ext.substring(0, sep), Integer.parseInt(ext.substring(sep + 1)))); + return Collections.singletonList(new InetSocketAddress(ext.substring(0, sep), Integer.parseInt(ext.substring(sep + 1)))); } } From ae1ea9c6bbf59e2fc0dfe39b256a8f230966f087 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Fri, 26 Jun 2026 09:39:08 +0300 Subject: [PATCH 23/28] IGNITE-28738 RU testcontainers test: fix class injection, Linux fast-path, faster shutdown (#1) - testClassesJar: inject each listed class together with its nested classes (the plugin provider's anonymous classes) and add DisabledRollingUpgradeProcessor / DisabledValidationProcessor. Without this the containerized nodes fail to start (NoClassDefFoundError) and the test times out after 600s on a clean checkout. - OS-aware networking. Keep the macOS path (host<->container via published ports + ContainerAddressResolver + host.docker.internal) and add a direct Linux path: containers are reached at their Docker bridge IP, the host node is reached at the Docker network gateway. Removes the Docker-Desktop VM-proxy latency on Linux/CI. - Container shutdown wait 60s -> 10s. Graceful SIGTERM does not complete (the image entrypoint does not forward it), so the node is force-stopped anyway; this saved ~50s per node (~423s -> ~250s end to end on macOS). - SOURCE_COMMIT_HASH overridable via -Dru.source.commit.hash. - assertTrue on the post-upgrade topology waitForCondition (was silently ignored); compare verified values as boxed Integer to avoid an NPE on a missing key. Verified green on macOS. The Linux path compiles and its addressing primitives are validated, but a full Linux/CI run is still required. Co-authored-by: Claude Opus 4.8 --- .../ru/IgniteRebalanceOnUpgradeTest.java | 22 +++-- .../testcontainers/IgniteContainer.java | 95 ++++++++++++++----- 2 files changed, 88 insertions(+), 29 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index f705684ddd503..f43c0f191d892 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -60,8 +60,9 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { "7b880b69-8a9e-4b84-b555-250d365e2e67" ); - /** Source commit hash. Used for docker image tag. */ - private static final String SOURCE_COMMIT_HASH = "0ad4656eef09acda288cbad96f80f0138732d94a"; + /** Source version image tag, overridable via {@code -Dru.source.commit.hash}. */ + private static final String SOURCE_COMMIT_HASH = + System.getProperty("ru.source.commit.hash", "0ad4656eef09acda288cbad96f80f0138732d94a"); /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; @@ -126,7 +127,7 @@ public void testRollingUpgrade() throws Exception { IgniteCache targetCache = nodes.get(0).cache(CACHE_NAME); for (int i = 0; i < 1000; i++) - assertEquals("Data mismatch after upgrade at key: " + i, i, (int)targetCache.get(i)); + assertEquals("Data mismatch after upgrade at key: " + i, (Integer)i, targetCache.get(i)); targetCache.put(1001, 1001); @@ -142,8 +143,12 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception for (IgniteContainer container : srcCluster.containers()) { log.info(">>> Upgrade node=" + container.consistentId()); - String hostIp = container.execInContainer("sh", "-c", - "getent ahostsv4 host.docker.internal | awk '{print $1}' | head -1").getStdout().trim(); + // Address containers use to reach this (host JVM) node: the Docker bridge gateway on Linux, the + // host.docker.internal alias on macOS. + String hostIp = IgniteContainer.LINUX + ? container.gatewayIp() + : container.execInContainer("sh", "-c", + "getent ahostsv4 host.docker.internal | awk '{print $1}' | head -1").getStdout().trim(); container.stop(); @@ -151,7 +156,8 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception IgniteEx ignite = startGrid(configuration(container.consistentId(), container.localWorkDirectory(), addrs.values(), hostIp)); - waitForCondition(() -> CONSISTENT_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT); + assertTrue("Upgraded node did not rejoin the full topology in time", + waitForCondition(() -> CONSISTENT_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT)); // Already-upgraded host nodes live in this JVM on localhost within the discovery port range. addrs.put(container.consistentId(), "127.0.0.1:48500..48599"); @@ -184,7 +190,9 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect // own outgoing attempts to unreachable container-internal (172.x) addresses give up in ~1s (they // otherwise hang in SYN_SENT) and fall through to the reachable 127.0.0.1:. TcpCommunicationSpi commSpi = new TcpCommunicationSpi() - .setLocalAddress("127.0.0.1") + // macOS: bind comm to loopback (advertised to containers via the resolver as the Docker-host address). + // Linux: bind to all interfaces so containers reach this host node at the Docker bridge gateway IP. + .setLocalAddress(IgniteContainer.LINUX ? "0.0.0.0" : "127.0.0.1") .setLocalPort(49100) .setConnectTimeout(1000) .setMaxConnectTimeout(10000) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index f9db812d81a12..75fe08b0c2ccf 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -17,10 +17,13 @@ package org.apache.ignite.compatibility.testframework.testcontainers; +import com.github.dockerjava.api.model.ContainerNetwork; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; @@ -62,6 +65,13 @@ public class IgniteContainer extends GenericContainer { /** Logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(IgniteContainer.class); + /** + * {@code true} on Linux, where the host shares the Docker bridge and reaches containers directly. Elsewhere + * (macOS/Windows Docker Desktop) the host talks to containers through a VM proxy, so the address hacks + * (published ports + ContainerAddressResolver + host.docker.internal) are used instead. + */ + public static final boolean LINUX = System.getProperty("os.name", "").toLowerCase().contains("linux"); + /** Ignite root directory in container. */ private static final String ROOT_DIR_PATH = "/opt/ignite/apache-ignite/"; @@ -86,10 +96,12 @@ public class IgniteContainer extends GenericContainer { /** Base host port for the published thin-client port (node index added). */ private static final int CLIENT_HOST_PORT_BASE = 50800; - /** Custom classes used by node in containers. */ + /** Custom classes (with their nested classes) used by node in containers. */ private static final List TEST_CLASSES = List.of( ContainerAddressResolver.class.getName(), - TestCompatibilityPluginProvider.class.getName() + TestCompatibilityPluginProvider.class.getName(), + "org.apache.ignite.compatibility.testframework.plugins.DisabledRollingUpgradeProcessor", + "org.apache.ignite.compatibility.testframework.plugins.DisabledValidationProcessor" ); /** Jar holding {@link #TEST_CLASSES}, injected so the old image can load it. */ @@ -121,13 +133,19 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c withEnv("IGNITE_LOCAL_HOST", "0.0.0.0"); withEnv("TZ", ZoneId.systemDefault().toString()); - // On macOS the host JVM cannot reach container-internal addresses, so each node advertises its - // host-published discovery/communication ports (127.0.0.1:hostPort) via ContainerAddressResolver. - // node.consistent.id pins the node's consistent id (and thus its persistence folder) to consistentId so the - // upgraded host node, started with the same consistent id, inherits this node's persisted data. - withEnv("JVM_OPTS", "-Xms512m -Xmx1g" + " -Dnode.consistent.id=" + consistentId - + " -D" + EXT_ADDR_PROP_PREFIX + TcpDiscoverySpi.DFLT_PORT + "=127.0.0.1:" + discoHostPort - + " -D" + EXT_ADDR_PROP_PREFIX + TcpCommunicationSpi.DFLT_PORT + "=127.0.0.1:" + commHostPort); + // node.consistent.id pins the node's consistent id (and thus its persistence folder) so the upgraded host + // node, started with the same consistent id, inherits this node's persisted data. + String jvmOpts = "-Xms512m -Xmx1g -Dnode.consistent.id=" + consistentId; + + // Proxy-networking hosts (macOS/Windows) can't reach container-internal addresses, so each node advertises + // its host-published ports (127.0.0.1:hostPort) via ContainerAddressResolver. On Linux containers are + // directly routable and advertise their real address, so no override is needed. + if (!LINUX) { + jvmOpts += " -D" + EXT_ADDR_PROP_PREFIX + TcpDiscoverySpi.DFLT_PORT + "=127.0.0.1:" + discoHostPort + + " -D" + EXT_ADDR_PROP_PREFIX + TcpCommunicationSpi.DFLT_PORT + "=127.0.0.1:" + commHostPort; + } + + withEnv("JVM_OPTS", jvmOpts); withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); @@ -139,10 +157,13 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c // Stream container logs to stdout so they appear in the IDE test runner. withLogConsumer(frame -> System.out.println("[" + consistentId + "] " + frame.getUtf8String().trim())); - // Fixed host ports so the host JVM node can target each container deterministically. - addFixedExposedPort(CLIENT_HOST_PORT_BASE + idx, ClientConnectorConfiguration.DFLT_PORT); - addFixedExposedPort(commHostPort, TcpCommunicationSpi.DFLT_PORT); - addFixedExposedPort(discoHostPort, TcpDiscoverySpi.DFLT_PORT); + // Proxy-networking hosts only: publish fixed host ports so the host JVM node can target each container at + // 127.0.0.1:. On Linux the host reaches containers at their bridge IP directly, so nothing is published. + if (!LINUX) { + addFixedExposedPort(CLIENT_HOST_PORT_BASE + idx, ClientConnectorConfiguration.DFLT_PORT); + addFixedExposedPort(commHostPort, TcpCommunicationSpi.DFLT_PORT); + addFixedExposedPort(discoHostPort, TcpDiscoverySpi.DFLT_PORT); + } waitingFor(Wait.forLogMessage(".*Node started.*", 1) .withStartupTimeout(Duration.ofSeconds(600))); @@ -159,7 +180,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c .exec(); await() - .atMost(Duration.ofSeconds(60)) + .atMost(Duration.ofSeconds(10)) .pollInterval(Duration.ofMillis(500)) .until(() -> !isRunning()); } @@ -228,9 +249,24 @@ public String discoveryAddress() { return address(TcpDiscoverySpi.DFLT_PORT); } - /** */ + /** @return Address the host JVM uses to reach this container's {@code port}. */ private String address(int port) { - return getHost() + ":" + getMappedPort(port); + return LINUX ? bridgeIp() + ":" + port : getHost() + ":" + getMappedPort(port); + } + + /** @return Gateway IP of the test Docker network — the address containers use to reach the host JVM on Linux. */ + public String gatewayIp() { + return network().getGateway(); + } + + /** @return This container's IP on the test Docker network (directly routable from the host on Linux). */ + private String bridgeIp() { + return network().getIpAddress(); + } + + /** @return This container's attachment to the single test Docker network. */ + private ContainerNetwork network() { + return getContainerInfo().getNetworkSettings().getNetworks().values().iterator().next(); } /** */ @@ -277,13 +313,28 @@ private static File testClassesJar() throws IOException { for (String cls : TEST_CLASSES) { String clsPath = cls.replace('.', '/') + ".class"; - try (InputStream in = IgniteContainer.class.getClassLoader().getResourceAsStream(clsPath)) { - if (in == null) - throw new IOException("Class not found on classpath: " + clsPath); + URL url = IgniteContainer.class.getClassLoader().getResource(clsPath); + + if (url == null) + throw new IOException("Class not found on classpath: " + clsPath); + + File dir; + + try { + dir = new File(url.toURI()).getParentFile(); + } + catch (URISyntaxException e) { + throw new IOException(e); + } + + String pkg = clsPath.substring(0, clsPath.lastIndexOf('/') + 1); + String simple = cls.substring(cls.lastIndexOf('.') + 1); - out.putNextEntry(new JarEntry(clsPath)); + // Include the class and its nested classes (e.g. the provider's anonymous $1). + for (File f : dir.listFiles((d, name) -> name.equals(simple + ".class") || name.startsWith(simple + '$'))) { + out.putNextEntry(new JarEntry(pkg + f.getName())); - in.transferTo(out); + Files.copy(f.toPath(), out); out.closeEntry(); } From 916ad1c7684c06a19d60bad1be0ceb203d6c9f5c Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Sat, 27 Jun 2026 19:23:21 +0300 Subject: [PATCH 24/28] IGNITE-28738 RU test: small code-quality cleanups (#2) - IgniteContainer.stop(): drop the unconditional "shut down gracefully" log. The image entrypoint does not forward SIGTERM, so the await times out and the node is force-stopped via super.stop(); the message contradicted the preceding "Proceeding with forceful stop." warning. - IgniteContainer.testClassesJar(): null-check File.listFiles() (returns null on an unreadable dir) so a missing class directory fails with a clear IOException instead of an NPE in the loop. - IgniteRebalanceOnUpgradeTest: compare the post-put value as a boxed Integer too (the get(1000)-loop already does), so a missing key fails the assertion instead of NPE-ing on unboxing. - Scope the "bind communication to loopback" comment to macOS (on Linux it binds to 0.0.0.0) and drop a trailing-whitespace blank line. No behavioral change on the macOS path. Co-authored-by: Claude Opus 4.8 --- .../compatibility/ru/IgniteRebalanceOnUpgradeTest.java | 8 ++++---- .../testframework/testcontainers/IgniteContainer.java | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index f43c0f191d892..94c19a78d7924 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -121,7 +121,7 @@ public void testRollingUpgrade() throws Exception { cache.put(i, i); closeClient(); - + upgradeCluster(cluster); IgniteCache targetCache = nodes.get(0).cache(CACHE_NAME); @@ -131,7 +131,7 @@ public void testRollingUpgrade() throws Exception { targetCache.put(1001, 1001); - assertEquals(1001, (int)targetCache.get(1001)); + assertEquals((Integer)1001, targetCache.get(1001)); } finally { closeClient(); @@ -184,8 +184,8 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setJoinTimeout(30000) .setLocalPort(48500); - // Bind communication to loopback (discovery stays on 0.0.0.0 to satisfy Ignite's non-loopback join - // check) so the node advertises only 127.0.0.1 + the resolver-mapped Docker host address -- no + // On macOS communication binds to loopback (discovery stays on 0.0.0.0 to satisfy Ignite's non-loopback + // join check) so the node advertises only 127.0.0.1 + the resolver-mapped Docker host address -- no // unreachable host LAN IPs for the containers to stall on. A short connect timeout makes the node's // own outgoing attempts to unreachable container-internal (172.x) addresses give up in ~1s (they // otherwise hang in SYN_SENT) and fall through to the reachable 127.0.0.1:. diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 75fe08b0c2ccf..ca612bd67a17d 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -189,8 +189,6 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c } } - LOGGER.info("Ignite node {} shut down gracefully.", hostname); - super.stop(); } @@ -331,7 +329,13 @@ private static File testClassesJar() throws IOException { String simple = cls.substring(cls.lastIndexOf('.') + 1); // Include the class and its nested classes (e.g. the provider's anonymous $1). - for (File f : dir.listFiles((d, name) -> name.equals(simple + ".class") || name.startsWith(simple + '$'))) { + File[] clsFiles = dir.listFiles((d, name) -> + name.equals(simple + ".class") || name.startsWith(simple + '$')); + + if (clsFiles == null) + throw new IOException("Cannot list class directory: " + dir); + + for (File f : clsFiles) { out.putNextEntry(new JarEntry(pkg + f.getName())); Files.copy(f.toPath(), out); From c62397115065c7cf63addb5dec006208f04bb3e6 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Tue, 30 Jun 2026 13:51:15 +0500 Subject: [PATCH 25/28] add docker upgrade mode --- modules/compatibility/DEVNOTES.md | 82 ++++++++-- modules/compatibility/pom.xml | 30 ++++ .../ru/IgniteRebalanceOnUpgradeTest.java | 135 ++++++++++++----- .../testcontainers/IgniteContainer.java | 141 +++++++++++++----- .../test/resources/docker/build_distrib.sh | 73 +++++++++ 5 files changed, 383 insertions(+), 78 deletions(-) create mode 100755 modules/compatibility/src/test/resources/docker/build_distrib.sh diff --git a/modules/compatibility/DEVNOTES.md b/modules/compatibility/DEVNOTES.md index fd308d9937141..731d523681cb8 100644 --- a/modules/compatibility/DEVNOTES.md +++ b/modules/compatibility/DEVNOTES.md @@ -23,7 +23,11 @@ ## Running IgniteRebalanceOnUpgradeTest -This test verifies that data rebalancing works correctly when upgrading Ignite from a specific version to the current codebase. It uses Docker containers running the "old" Ignite version. +This test verifies that data rebalancing works correctly when upgrading Ignite from a specific version to the current codebase. +It supports two upgrade modes: + +- **DOCKER** (default) — all nodes stay in Docker containers; each node is upgraded in-place by swapping its `libs/` directory and restarting. +- **LOCAL** — the source cluster runs in Docker containers, then nodes are upgraded to local host-JVM instances. ### Step 1. Build a local Docker image for the source (old) version @@ -35,8 +39,6 @@ Run the following script from the **project root**, passing the commit hash of t > **Note:** If you omit ``, the script will use the hash of the latest commit in the current branch. -> **Note:** If your corporate network uses SSL inspection and you get `TLS: server certificate not trusted`, the script will automatically patch Dockerfiles to use HTTP repositories and restore them after the build. - The script will: 1. Checkout the specified commit. 2. Build the project (`./mvnw clean install -T1C -Pall-java,licenses -DskipTests`). @@ -46,18 +48,78 @@ The script will: > **Note:** If a distribution archive already exists in `target/bin/`, the build steps will be skipped. -### Step 2. Set the commit hash in the test +### Step 2. Run the test -Open `org.apache.ignite.compatibility.ru.IgniteRebalanceOnUpgradeTest` and set `SOURCE_COMMIT_HASH` to the **same commit hash** used in Step 1: +Run `IgniteRebalanceOnUpgradeTest` from your IDE or via Maven: -```java -private static final String SOURCE_COMMIT_HASH = ""; +```bash +./mvnw test -pl modules/compatibility -Dtest=IgniteRebalanceOnUpgradeTest -Psurefire-fork-count-1 ``` -### Step 3. Run the test +--- -Run `IgniteRebalanceOnUpgradeTest` from your IDE or via Maven: +## Upgrade Modes + +### DOCKER mode (default) + +All nodes stay in Docker containers throughout the test. Each node is upgraded in-place: +1. The container is gracefully stopped (`docker stop`). +2. Source jars in `/opt/ignite/apache-ignite/libs/` are replaced by target jars from the host. +3. The container is restarted (`docker start`). + +The Docker image for the source cluster is the same as in LOCAL mode — only one image is needed. +The target-version jars are provided from the host filesystem. + +**Option A: Automatic (recommended)** — use the `compatibility-docker` profile: + +```bash +./mvnw test -pl modules/compatibility -Dtest=IgniteRebalanceOnUpgradeTest \ + -Dru.source.commit.hash=0ad4656eef09acda288cbad96f80f0138732d94a \ + -Psurefire-fork-count-1,compatibility-docker +``` + +The profile will automatically: +1. Check if `project/target/ignite-target-libs` symlink exists. +2. If not, check for a distribution ZIP in `project/target/bin/`. +3. If the ZIP is missing, build the project and distribution (`mvn install` + `mvn initialize -Prelease`). +4. Extract the ZIP into `project/target/bin/` (the distribution lands in `project/target/bin/apache-ignite-*-bin/`). +5. Create a symlink `project/target/ignite-target-libs` → `project/target/bin/apache-ignite-*-bin/libs/`. + +> **Note:** Subsequent runs will skip the build if the symlink or the distribution ZIP already exists. + +**Option B: Manual** — build, extract, and specify the libs directory: + +```bash +./mvnw clean install -T1C -Pall-java -DskipTests +./mvnw initialize -Prelease +cd target/bin && unzip apache-ignite-*-bin.zip && cd ../.. + +./mvnw test -pl modules/compatibility -Dtest=IgniteRebalanceOnUpgradeTest \ + -Dru.source.commit.hash=0ad4656eef09acda288cbad96f80f0138732d94a \ + -Dru.target.libs.dir=target/bin/apache-ignite--bin/libs \ + -Psurefire-fork-count-1 +``` + +### LOCAL mode + +The source (old-version) cluster starts in Docker containers. During rolling upgrade each container is stopped and replaced by a local host-JVM node with the same `consistentId` and persistence directory. + +- Controlled by `-Dru.upgrade.mode=LOCAL`. ```bash -./mvnw test -pl modules/compatibility -Dtest=IgniteRebalanceOnUpgradeTest +./mvnw test -pl modules/compatibility -Dtest=IgniteRebalanceOnUpgradeTest \ + -Dru.upgrade.mode=LOCAL \ + -Dru.source.commit.hash=0ad4656eef09acda288cbad96f80f0138732d94a \ + -Psurefire-fork-count-1 ``` + +--- + +## System Properties + +| Property | Default | Class | Description | +|----------|-------------------------------------------|-------|-------------| +| `ru.upgrade.mode` | `DOCKER` | `IgniteRebalanceOnUpgradeTest` | Upgrade mode: `LOCAL` or `DOCKER` | +| `ru.source.commit.hash` | `0ad4656eef09acda288cbad96f80f0138732d94a` | `IgniteRebalanceOnUpgradeTest` | Commit hash for the source (old-version) Docker image `apacheignite/ignite:` | +| `ru.target.libs.dir` | `/target/ignite-target-libs` | `IgniteContainer` | Host directory with target-version jars (DOCKER mode only) | +| `ru.local.work.dir` | `/target/test-ignite-work` | `IgniteContainer` | Local directory bind-mounted as Ignite work directory (persists across container restarts) | diff --git a/modules/compatibility/pom.xml b/modules/compatibility/pom.xml index 9a18c1c9957dc..284e943ebb0e6 100644 --- a/modules/compatibility/pom.xml +++ b/modules/compatibility/pom.xml @@ -171,4 +171,34 @@ + + + + compatibility-docker + + + + org.codehaus.mojo + exec-maven-plugin + + + prepare-docker-target-libs + + exec + + generate-test-resources + + bash + + ${project.basedir}/src/test/resources/docker/build_distrib.sh + ${project.basedir}/../.. + + + + + + + + + diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 94c19a78d7924..6464172d488f0 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -61,8 +61,12 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { ); /** Source version image tag, overridable via {@code -Dru.source.commit.hash}. */ - private static final String SOURCE_COMMIT_HASH = - System.getProperty("ru.source.commit.hash", "0ad4656eef09acda288cbad96f80f0138732d94a"); + private static final String SOURCE_COMMIT_HASH = System.getProperty("ru.source.commit.hash", + "0ad4656eef09acda288cbad96f80f0138732d94a"); + + /** Upgrade mode. */ + private static final UpgradeMode UPGRADE_MODE = UpgradeMode.valueOf(System.getProperty("ru.upgrade.mode", + UpgradeMode.DOCKER.name())); /** Cache name. */ private static final String CACHE_NAME = "ru-test-cache"; @@ -70,7 +74,7 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { /** Local work directory. */ private static final File LOCAL_WORK_DIR = new File(LOCAL_WORK_DIR_PATH); - /** Local nodes. */ + /** Local host-JVM nodes (LOCAL mode only). */ private final List nodes = new ArrayList<>(); /** Consistent ID -> discovery address. */ @@ -107,9 +111,6 @@ public void testRollingUpgrade() throws Exception { try (IgniteClusterContainer cluster = new IgniteClusterContainer(SOURCE_COMMIT_HASH, CONSISTENT_IDS)) { cluster.start(); - for (IgniteContainer container : cluster.containers()) - addrs.put(container.consistentId(), container.discoveryAddress()); - ClientCacheConfiguration cfg = new ClientCacheConfiguration() .setName(CACHE_NAME) .setBackups(1) @@ -124,50 +125,92 @@ public void testRollingUpgrade() throws Exception { upgradeCluster(cluster); - IgniteCache targetCache = nodes.get(0).cache(CACHE_NAME); - - for (int i = 0; i < 1000; i++) - assertEquals("Data mismatch after upgrade at key: " + i, (Integer)i, targetCache.get(i)); - - targetCache.put(1001, 1001); - - assertEquals((Integer)1001, targetCache.get(1001)); + if (UPGRADE_MODE == UpgradeMode.DOCKER) + verifyViaDockerNodes(cluster); + else + verifyViaLocalNodes(); } finally { closeClient(); + + if (UPGRADE_MODE == UpgradeMode.LOCAL) + stopLocalNodes(); } } + /** Verify data via local host-JVM nodes. */ + private void verifyViaLocalNodes() { + IgniteCache targetCache = nodes.get(0).cache(CACHE_NAME); + + for (int i = 0; i < 1000; i++) + assertEquals("Data mismatch after upgrade at key: " + i, (Integer)i, targetCache.get(i)); + + targetCache.put(1001, 1001); + + assertEquals((Integer)1001, targetCache.get(1001)); + } + + /** Verify data via thin client connected to upgraded Docker nodes. */ + private void verifyViaDockerNodes(IgniteClusterContainer cluster) { + IgniteContainer con = cluster.containers().get(0); + + con.checkNodeCount(cluster.containers().size()); + + ClientCache targetCache = client(con.clientAddress()).getOrCreateCache(CACHE_NAME); + + for (int i = 0; i < 1000; i++) + assertEquals("Data mismatch after upgrade at key: " + i, (Integer)i, targetCache.get(i)); + + targetCache.put(1001, 1001); + + assertEquals((Integer)1001, targetCache.get(1001)); + } + /** */ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception { - for (IgniteContainer container : srcCluster.containers()) { - log.info(">>> Upgrade node=" + container.consistentId()); + List srcContainers = srcCluster.containers(); + + if (UPGRADE_MODE == UpgradeMode.LOCAL) + for (IgniteContainer con : srcContainers) + addrs.put(con.consistentId(), con.discoveryAddress()); - // Address containers use to reach this (host JVM) node: the Docker bridge gateway on Linux, the - // host.docker.internal alias on macOS. - String hostIp = IgniteContainer.LINUX - ? container.gatewayIp() - : container.execInContainer("sh", "-c", - "getent ahostsv4 host.docker.internal | awk '{print $1}' | head -1").getStdout().trim(); + for (int i = 0; i < srcContainers.size(); i++) { + IgniteContainer con = srcContainers.get(i); - container.stop(); + log.info(">>> Upgrade node=" + con.consistentId() + " (mode=" + UPGRADE_MODE + ")"); - addrs.remove(container.consistentId()); + if (UPGRADE_MODE == UpgradeMode.DOCKER) + con.upgradeAndRestart(srcContainers.size()); + else + upgradeLocally(con, i); + } + } - IgniteEx ignite = startGrid(configuration(container.consistentId(), container.localWorkDirectory(), addrs.values(), hostIp)); + /** Stop container, start a local host-JVM node with the same consistent ID. */ + private void upgradeLocally(IgniteContainer con, int idx) throws Exception { + // Address containers use to reach this (host JVM) node: the Docker bridge gateway on Linux, the + // host.docker.internal alias on macOS. + String hostIp = IgniteContainer.LINUX + ? con.gatewayIp() + : con.execInContainer("sh", "-c", + "getent ahostsv4 host.docker.internal | awk '{print $1}' | head -1").getStdout().trim(); - assertTrue("Upgraded node did not rejoin the full topology in time", - waitForCondition(() -> CONSISTENT_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT)); + con.stop(); - // Already-upgraded host nodes live in this JVM on localhost within the discovery port range. - addrs.put(container.consistentId(), "127.0.0.1:48500..48599"); + addrs.remove(con.consistentId()); - nodes.add(ignite); - } + IgniteEx ignite = startGrid(configuration(con.consistentId(), con.localWorkDirectory(), addrs.values(), hostIp, idx)); + + assertTrue("Upgraded node did not rejoin the full topology in time", + waitForCondition(() -> CONSISTENT_IDS.size() == ignite.cluster().nodes().size(), DFLT_TEST_TIMEOUT)); + + addrs.put(con.consistentId(), "127.0.0.1:" + (48500 + idx)); + + nodes.add(ignite); } /** */ - private IgniteConfiguration configuration(String nodeId, String workDir, Collection addrs0, String ip) { + private IgniteConfiguration configuration(String nodeId, String workDir, Collection addrs0, String ip, int idx) { DataRegionConfiguration dataRegionCfg = new DataRegionConfiguration() .setName("testRegion") .setInitialSize(1024L * 1024 * 1024) @@ -182,7 +225,7 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect .setSocketTimeout(1000) .setNetworkTimeout(20000) .setJoinTimeout(30000) - .setLocalPort(48500); + .setLocalPort(48500 + idx); // On macOS communication binds to loopback (discovery stays on 0.0.0.0 to satisfy Ignite's non-loopback // join check) so the node advertises only 127.0.0.1 + the resolver-mapped Docker host address -- no @@ -193,7 +236,7 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect // macOS: bind comm to loopback (advertised to containers via the resolver as the Docker-host address). // Linux: bind to all interfaces so containers reach this host node at the Docker bridge gateway IP. .setLocalAddress(IgniteContainer.LINUX ? "0.0.0.0" : "127.0.0.1") - .setLocalPort(49100) + .setLocalPort(49100 + idx) .setConnectTimeout(1000) .setMaxConnectTimeout(10000) // The NIO connect to a blackholed container-internal (172.x) address is not aborted by @@ -237,4 +280,28 @@ private void closeClient() { client = null; } } + + /** */ + private void stopLocalNodes() { + for (IgniteEx node : nodes) + if (node != null) + Ignition.stop(node.name(), false); + + nodes.clear(); + } + + /** + * Upgrade mode, overridable via {@code -Dru.upgrade.mode} (DOCKER|LOCAL). + *

    + *
  • DOCKER — all nodes stay in Docker; in-place upgrade by swapping libs inside containers (default)
  • + *
  • LOCAL — source cluster in Docker, upgraded to local host-JVM nodes
  • + *
+ */ + private enum UpgradeMode { + /** */ + DOCKER, + + /** */ + LOCAL + } } diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index ca612bd67a17d..42719c9595d02 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -17,13 +17,13 @@ package org.apache.ignite.compatibility.testframework.testcontainers; -import com.github.dockerjava.api.model.ContainerNetwork; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.time.ZoneId; import java.util.Arrays; @@ -32,11 +32,15 @@ import java.util.jar.JarOutputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.github.dockerjava.api.model.ContainerNetwork; import org.apache.ignite.IgniteException; import org.apache.ignite.cluster.ClusterState; +import org.apache.ignite.compatibility.testframework.plugins.DisabledRollingUpgradeProcessor; +import org.apache.ignite.compatibility.testframework.plugins.DisabledValidationProcessor; import org.apache.ignite.compatibility.testframework.plugins.TestCompatibilityPluginProvider; import org.apache.ignite.configuration.ClientConnectorConfiguration; import org.apache.ignite.internal.IgniteInterruptedCheckedException; +import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.spi.communication.tcp.TcpCommunicationSpi; import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; import org.slf4j.Logger; @@ -48,22 +52,17 @@ import org.testcontainers.utility.DockerImageName; import static org.apache.ignite.compatibility.testframework.testcontainers.ContainerAddressResolver.EXT_ADDR_PROP_PREFIX; +import static org.apache.ignite.testframework.GridTestUtils.DFLT_TEST_TIMEOUT; import static org.apache.ignite.testframework.GridTestUtils.waitForCondition; -import static org.testcontainers.shaded.org.awaitility.Awaitility.await; +import static org.junit.Assert.assertTrue; import static org.testcontainers.utility.MountableFile.forClasspathResource; import static org.testcontainers.utility.MountableFile.forHostPath; /** Ignite container. */ public class IgniteContainer extends GenericContainer { - /** Property for local work directory. */ - private static final String LOCAL_WORK_DIR_PROP = "local.work.dir"; - /** Local work directory. */ - public static final String LOCAL_WORK_DIR_PATH = System.getProperty(LOCAL_WORK_DIR_PROP, - System.getProperty("user.dir") + "/target/test-ignite-work"); - - /** Logger. */ - private static final Logger LOGGER = LoggerFactory.getLogger(IgniteContainer.class); + public static final String LOCAL_WORK_DIR_PATH = System.getProperty("ru.local.work.dir", + U.getIgniteHome() + "/target/test-ignite-work"); /** * {@code true} on Linux, where the host shares the Docker bridge and reaches containers directly. Elsewhere @@ -72,18 +71,25 @@ public class IgniteContainer extends GenericContainer { */ public static final boolean LINUX = System.getProperty("os.name", "").toLowerCase().contains("linux"); + /** Host directory with target-version jars for DOCKER upgrade mode, overridable via {@code -Dru.target.libs.dir}. */ + private static final Path TARGET_LIBS_DIR = Path.of(System.getProperty("ru.target.libs.dir", + U.getIgniteHome() + "/target/ignite-target-libs")); + + /** Logger. */ + private static final Logger LOGGER = LoggerFactory.getLogger(IgniteContainer.class); + /** Ignite root directory in container. */ private static final String ROOT_DIR_PATH = "/opt/ignite/apache-ignite/"; + /** Ignite libs directory in container. */ + private static final String LIBS_DIR_PATH = ROOT_DIR_PATH + "libs/"; + /** Ignite work directory in container. */ private static final String WORK_DIR_PATH = ROOT_DIR_PATH + "work"; /** 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"; - /** */ private static final Pattern CLUSTER_STATE_PATTERN = Pattern.compile("Cluster state: (ACTIVE|INACTIVE)"); @@ -100,8 +106,8 @@ public class IgniteContainer extends GenericContainer { private static final List TEST_CLASSES = List.of( ContainerAddressResolver.class.getName(), TestCompatibilityPluginProvider.class.getName(), - "org.apache.ignite.compatibility.testframework.plugins.DisabledRollingUpgradeProcessor", - "org.apache.ignite.compatibility.testframework.plugins.DisabledValidationProcessor" + DisabledRollingUpgradeProcessor.class.getName(), + DisabledValidationProcessor.class.getName() ); /** Jar holding {@link #TEST_CLASSES}, injected so the old image can load it. */ @@ -116,7 +122,10 @@ public class IgniteContainer extends GenericContainer { /** Path to work directory. */ private final String workDirPath; - /** Constructor. */ + /** + * Constructor with a commit hash (image tag). + * Uses {@code apacheignite/ignite:} as the Docker image. + */ public IgniteContainer(String commitHash, Network net, String hostname, String consistentId, int idx) throws IOException { super(DockerImageName.parse("apacheignite/ignite:" + commitHash)); @@ -149,12 +158,11 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); - withCopyFileToContainer(forHostPath(testClassesJar().getAbsolutePath()), ROOT_DIR_PATH + "libs/test-classes.jar"); + withCopyFileToContainer(forHostPath(testClassesJar().getAbsolutePath()), LIBS_DIR_PATH + "test-classes.jar"); withNetwork(net); withNetworkAliases(hostname); - // Stream container logs to stdout so they appear in the IDE test runner. withLogConsumer(frame -> System.out.println("[" + consistentId + "] " + frame.getUtf8String().trim())); // Proxy-networking hosts only: publish fixed host ports so the host JVM node can target each container at @@ -173,16 +181,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c @Override public void stop() { if (isRunning()) { try { - LOGGER.info("Sending SIGTERM to Ignite node {} for graceful shutdown...", hostname); - - getDockerClient().killContainerCmd(getContainerId()) - .withSignal("TERM") - .exec(); - - await() - .atMost(Duration.ofSeconds(10)) - .pollInterval(Duration.ofMillis(500)) - .until(() -> !isRunning()); + stopGraceful(); } catch (Exception e) { LOGGER.warn("Graceful shutdown failed for node {}. Proceeding with forceful stop.", hostname, e); @@ -192,6 +191,71 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c super.stop(); } + /** In-place upgrade inside Docker: graceful stop → swap libs → restart. */ + public void upgradeAndRestart(int nodeCnt) throws Exception { + stopGraceful(); + + restartWithTargetLibs(TARGET_LIBS_DIR); + + assertTrue("Upgraded Docker node is not running", isRunning()); + } + + /** + * Stop the container gracefully without removing it (container stays in "Exited" state). + * Call this before {@link #restartWithTargetLibs(Path)}. + * + *

Uses {@code docker stop} (SIGTERM + wait + SIGKILL after timeout) via the Docker API. + * This gives Ignite time to flush persistence data, and falls back to SIGKILL if needed.

+ */ + private void stopGraceful() { + if (!isRunning()) + return; + + LOGGER.info("Graceful stop of node {}", hostname); + + getDockerClient().stopContainerCmd(getContainerId()) + .withTimeout(30) + .exec(); + + LOGGER.info("Node {} stopped", hostname); + } + + /** + * Restart the stopped container after swapping its {@code libs/} directory. + *

+ * Copies all jars from the provided host directory into the container's {@code /opt/ignite/apache-ignite/libs/}, + * then re-injects the test-classes jar and starts the container. + *

+ * + * @param targetLibsHostDir Host directory containing target-version jars. + */ + private void restartWithTargetLibs(Path targetLibsHostDir) throws Exception { + LOGGER.info("Replacing libs in container {} with jars from {}", hostname, targetLibsHostDir); + + for (Path file : Files.list(targetLibsHostDir).toArray(Path[]::new)) + if (Files.isRegularFile(file)) + copyFileToContainer(forHostPath(file.toAbsolutePath().toString()), LIBS_DIR_PATH + file.getFileName().toString()); + + // Re-inject the test-classes jar. + copyFileToContainer(forHostPath(testClassesJar().getAbsolutePath()), LIBS_DIR_PATH + "test-classes.jar"); + + LOGGER.info("Starting container {} with target libraries...", hostname); + + getDockerClient().startContainerCmd(getContainerId()).exec(); + + // isRunning() may cache stale state — inspect directly. + assertTrue(waitForCondition(() -> { + try { + return "running".equals(getDockerClient().inspectContainerCmd(getContainerId()).exec().getState().getStatus()); + } + catch (Exception e) { + return false; + } + }, DFLT_TEST_TIMEOUT)); + + LOGGER.info("Restarted node {} with target libraries", hostname); + } + /** @return Consistent ID. */ public String consistentId() { return consistentId; @@ -221,10 +285,20 @@ public void activateCluster(int nodeCnt) { if (!success) throw new IllegalStateException("Failed to set state ACTIVE"); - success = waitForCondition(() -> { + checkNodeCount(nodeCnt); + } + catch (IgniteInterruptedCheckedException e) { + throw new IgniteException(e); + } + } + + /** Check node count in cluster.*/ + public void checkNodeCount(int nodeCnt) { + try { + boolean success = waitForCondition(() -> { String out = execControl("--baseline"); - System.out.println(">>> Out=" + out); + LOGGER.debug(">>> Baseline output={}", out); return out.contains("Number of baseline nodes: " + nodeCnt); }, 30_000, 5_000); @@ -233,7 +307,7 @@ public void activateCluster(int nodeCnt) { throw new IllegalStateException("Check cluster count failed"); } catch (IgniteInterruptedCheckedException e) { - throw new IgniteException(e); + throw new RuntimeException(e); } } @@ -269,12 +343,11 @@ private ContainerNetwork network() { /** */ private String execControl(String... cmd) { - String[] fullCmd = new String[cmd.length + 2]; + String[] fullCmd = new String[cmd.length + 1]; fullCmd[0] = ROOT_DIR_PATH + "bin/control.sh"; - fullCmd[1] = ENABLE_EXPERIMENTAL_FLAG; - System.arraycopy(cmd, 0, fullCmd, 2, cmd.length); + System.arraycopy(cmd, 0, fullCmd, 1, cmd.length); ExecResult result; diff --git a/modules/compatibility/src/test/resources/docker/build_distrib.sh b/modules/compatibility/src/test/resources/docker/build_distrib.sh new file mode 100755 index 0000000000000..e27ef73039fb0 --- /dev/null +++ b/modules/compatibility/src/test/resources/docker/build_distrib.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# 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. +# +# Prepares target libs for DOCKER upgrade mode: +# 1. Builds the distribution ZIP if missing. +# 2. Extracts the ZIP into target/bin/. +# 3. Creates a symlink project/target/ignite-target-libs -> project/target/bin/apache-ignite-*-bin/libs. +# +# Arguments: +# $1 - path to project root (where mvnw is located) + +set -e + +PROJECT_ROOT="$1" +LIBS_SYMLINK="${PROJECT_ROOT}/target/ignite-target-libs" + +# If symlink already points to a valid directory — nothing to do +if [ -L "$LIBS_SYMLINK" ] && [ -d "$LIBS_SYMLINK" ]; then + echo "[compatibility-docker] Target libs symlink exists at ${LIBS_SYMLINK} — skipping" + exit 0 +fi + +DIST_ZIP_DIR="${PROJECT_ROOT}/target/bin" + +# Build distribution if ZIP archive is missing +if [ ! -d "$DIST_ZIP_DIR" ] || [ -z "$(ls "$DIST_ZIP_DIR"/apache-ignite-*-bin.zip 2>/dev/null)" ]; then + echo "[compatibility-docker] Distribution ZIP not found. Building project and distribution..." + cd "$PROJECT_ROOT" + ./mvnw clean install -T1C -Pall-java -DskipTests + ./mvnw initialize -Prelease +fi + +# Find the distribution ZIP +DIST_ZIP=$(ls -1 "$DIST_ZIP_DIR"/apache-ignite-*-bin.zip 2>/dev/null | head -1) + +if [ -z "$DIST_ZIP" ]; then + echo "[compatibility-docker] ERROR: Distribution ZIP not found in ${DIST_ZIP_DIR}" >&2 + exit 1 +fi + +# Extract ZIP into target/bin/ +echo "[compatibility-docker] Extracting ${DIST_ZIP} into ${DIST_ZIP_DIR}..." +unzip -q -o "$DIST_ZIP" -d "$DIST_ZIP_DIR" + +# Find the unpacked libs directory (structure: apache-ignite-*/libs/) +LIBS_DIR=$(find "$DIST_ZIP_DIR" -mindepth 2 -maxdepth 2 -type d -name libs | head -1) + +if [ -z "$LIBS_DIR" ]; then + echo "[compatibility-docker] ERROR: libs/ directory not found after extraction" >&2 + exit 1 +fi + +# Remove stale symlink if it exists +rm -f "$LIBS_SYMLINK" + +# Create symlink: target/ignite-target-libs -> target/bin/apache-ignite-*-bin/libs +ln -s "$LIBS_DIR" "$LIBS_SYMLINK" + +echo "[compatibility-docker] Done. Symlink: ${LIBS_SYMLINK} -> ${LIBS_DIR}" From 99da189ec82446b099e84166866ad15fa436b269 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Tue, 30 Jun 2026 23:06:53 +0500 Subject: [PATCH 26/28] minor --- .../ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java | 2 +- .../testframework/testcontainers/IgniteContainer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 6464172d488f0..2029ad5a711b4 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -180,7 +180,7 @@ private void upgradeCluster(IgniteClusterContainer srcCluster) throws Exception log.info(">>> Upgrade node=" + con.consistentId() + " (mode=" + UPGRADE_MODE + ")"); if (UPGRADE_MODE == UpgradeMode.DOCKER) - con.upgradeAndRestart(srcContainers.size()); + con.upgradeAndRestart(); else upgradeLocally(con, i); } diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 42719c9595d02..c4dec87f70490 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -192,7 +192,7 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c } /** In-place upgrade inside Docker: graceful stop → swap libs → restart. */ - public void upgradeAndRestart(int nodeCnt) throws Exception { + public void upgradeAndRestart() throws Exception { stopGraceful(); restartWithTargetLibs(TARGET_LIBS_DIR); From 75f31170d5190ad426613a834ebed8535af90760 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Wed, 1 Jul 2026 11:52:57 +0500 Subject: [PATCH 27/28] fix claude comments --- modules/compatibility/DEVNOTES.md | 8 +++++--- .../ru/IgniteRebalanceOnUpgradeTest.java | 12 +++++++++--- .../testcontainers/IgniteContainer.java | 15 +++++++++++---- .../src/test/resources/docker/build_distrib.sh | 1 + .../test/resources/docker/build_docker_image.sh | 16 ++++++++++++++++ 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/modules/compatibility/DEVNOTES.md b/modules/compatibility/DEVNOTES.md index 731d523681cb8..b14b399edfa8d 100644 --- a/modules/compatibility/DEVNOTES.md +++ b/modules/compatibility/DEVNOTES.md @@ -50,10 +50,12 @@ The script will: ### Step 2. Run the test -Run `IgniteRebalanceOnUpgradeTest` from your IDE or via Maven: +Run `IgniteRebalanceOnUpgradeTest` from your IDE or via Maven. The source version commit hash **must** be explicitly provided via `-Dru.source.commit.hash`: ```bash -./mvnw test -pl modules/compatibility -Dtest=IgniteRebalanceOnUpgradeTest -Psurefire-fork-count-1 +./mvnw test -pl modules/compatibility -Dtest=IgniteRebalanceOnUpgradeTest \ + -Dru.source.commit.hash= \ + -Psurefire-fork-count-1 ``` --- @@ -120,6 +122,6 @@ The source (old-version) cluster starts in Docker containers. During rolling upg | Property | Default | Class | Description | |----------|-------------------------------------------|-------|-------------| | `ru.upgrade.mode` | `DOCKER` | `IgniteRebalanceOnUpgradeTest` | Upgrade mode: `LOCAL` or `DOCKER` | -| `ru.source.commit.hash` | `0ad4656eef09acda288cbad96f80f0138732d94a` | `IgniteRebalanceOnUpgradeTest` | Commit hash for the source (old-version) Docker image `apacheignite/ignite:` | +| `ru.source.commit.hash` | - | `IgniteRebalanceOnUpgradeTest` | Commit hash for the source (old-version) Docker image `apacheignite/ignite:` | | `ru.target.libs.dir` | `/target/ignite-target-libs` | `IgniteContainer` | Host directory with target-version jars (DOCKER mode only) | | `ru.local.work.dir` | `/target/test-ignite-work` | `IgniteContainer` | Local directory bind-mounted as Ignite work directory (persists across container restarts) | diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java index 2029ad5a711b4..46aa0fa1911aa 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/ru/IgniteRebalanceOnUpgradeTest.java @@ -44,8 +44,10 @@ import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; +import org.testcontainers.DockerClientFactory; import static org.apache.ignite.compatibility.testframework.testcontainers.IgniteContainer.LOCAL_WORK_DIR_PATH; import static org.apache.ignite.testframework.GridTestUtils.DFLT_TEST_TIMEOUT; @@ -61,8 +63,7 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { ); /** Source version image tag, overridable via {@code -Dru.source.commit.hash}. */ - private static final String SOURCE_COMMIT_HASH = System.getProperty("ru.source.commit.hash", - "0ad4656eef09acda288cbad96f80f0138732d94a"); + private static final String SOURCE_COMMIT_HASH = System.getProperty("ru.source.commit.hash"); /** Upgrade mode. */ private static final UpgradeMode UPGRADE_MODE = UpgradeMode.valueOf(System.getProperty("ru.upgrade.mode", @@ -86,6 +87,11 @@ public class IgniteRebalanceOnUpgradeTest extends GridCommonAbstractTest { /** */ @BeforeClass public static void beforeClass() { + Assume.assumeTrue("Docker is required for this test", DockerClientFactory.instance().isDockerAvailable()); + + if (SOURCE_COMMIT_HASH == null) + throw new RuntimeException("Source version image tag must be specified via `-Dru.source.commit.hash`"); + U.delete(LOCAL_WORK_DIR); } @@ -254,7 +260,7 @@ private IgniteConfiguration configuration(String nodeId, String workDir, Collect int port = addr.getPort(); // Each sequentially started host node binds the next port in the discovery (48500+) and - // communication (47100+) ranges; map them all to the Docker host address so the containers + // communication (49100+) ranges; map them all to the Docker host address so the containers // can reach every host JVM node. if ((port >= 48500 && port < 48600) || (port >= 49100 && port < 49200)) return Set.of(new InetSocketAddress(ip, port)); diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index c4dec87f70490..53811e7ac7a9f 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -32,6 +32,7 @@ import java.util.jar.JarOutputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; import com.github.dockerjava.api.model.ContainerNetwork; import org.apache.ignite.IgniteException; import org.apache.ignite.cluster.ClusterState; @@ -191,8 +192,12 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c super.stop(); } - /** In-place upgrade inside Docker: graceful stop → swap libs → restart. */ + /** In-place upgrade inside Docker: clean libs → graceful stop → swap libs → restart. */ public void upgradeAndRestart() throws Exception { + LOGGER.info("Cleaning up old libs in container {}", hostname); + + execInContainer("rm", "-f", LIBS_DIR_PATH + "*"); + stopGraceful(); restartWithTargetLibs(TARGET_LIBS_DIR); @@ -232,9 +237,11 @@ private void stopGraceful() { private void restartWithTargetLibs(Path targetLibsHostDir) throws Exception { LOGGER.info("Replacing libs in container {} with jars from {}", hostname, targetLibsHostDir); - for (Path file : Files.list(targetLibsHostDir).toArray(Path[]::new)) - if (Files.isRegularFile(file)) - copyFileToContainer(forHostPath(file.toAbsolutePath().toString()), LIBS_DIR_PATH + file.getFileName().toString()); + try (Stream files = Files.list(targetLibsHostDir)) { + for (Path file : files.toArray(Path[]::new)) + if (Files.isRegularFile(file)) + copyFileToContainer(forHostPath(file.toAbsolutePath().toString()), LIBS_DIR_PATH + file.getFileName().toString()); + } // Re-inject the test-classes jar. copyFileToContainer(forHostPath(testClassesJar().getAbsolutePath()), LIBS_DIR_PATH + "test-classes.jar"); diff --git a/modules/compatibility/src/test/resources/docker/build_distrib.sh b/modules/compatibility/src/test/resources/docker/build_distrib.sh index e27ef73039fb0..28ebc10757018 100755 --- a/modules/compatibility/src/test/resources/docker/build_distrib.sh +++ b/modules/compatibility/src/test/resources/docker/build_distrib.sh @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # + # Prepares target libs for DOCKER upgrade mode: # 1. Builds the distribution ZIP if missing. # 2. Extracts the ZIP into target/bin/. diff --git a/modules/compatibility/src/test/resources/docker/build_docker_image.sh b/modules/compatibility/src/test/resources/docker/build_docker_image.sh index 7aed7f5b52643..868fde3823604 100755 --- a/modules/compatibility/src/test/resources/docker/build_docker_image.sh +++ b/modules/compatibility/src/test/resources/docker/build_docker_image.sh @@ -1,4 +1,20 @@ #!/bin/bash +# +# 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. +# # Get the absolute path to the project root directory PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../../../" && pwd)" From 89bd83f748ebadcf4dca2388e5238065843aefce Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Wed, 1 Jul 2026 14:27:02 +0500 Subject: [PATCH 28/28] fix claude comments v2 --- .../testcontainers/IgniteContainer.java | 7 +- .../discovery/tcp/TcpDiscoverySelfTest.java | 75 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java index 53811e7ac7a9f..0c9600a371ff4 100644 --- a/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java +++ b/modules/compatibility/src/test/java/org/apache/ignite/compatibility/testframework/testcontainers/IgniteContainer.java @@ -196,7 +196,10 @@ public IgniteContainer(String commitHash, Network net, String hostname, String c public void upgradeAndRestart() throws Exception { LOGGER.info("Cleaning up old libs in container {}", hostname); - execInContainer("rm", "-f", LIBS_DIR_PATH + "*"); + ExecResult result = execInContainer("sh", "-c", "rm -f " + LIBS_DIR_PATH + "*"); + + if (result.getExitCode() != 0) + throw new IllegalStateException("Failed to clean libs: " + result.getStderr()); stopGraceful(); @@ -368,7 +371,7 @@ private String execControl(String... cmd) { } if (result.getExitCode() != 0) - throw new IllegalStateException(result.toString()); + throw new IllegalStateException(result.getStderr()); return result.getStdout(); } diff --git a/modules/core/src/test/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySelfTest.java b/modules/core/src/test/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySelfTest.java index 4920d029538a6..102ee0cc76dc9 100644 --- a/modules/core/src/test/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/spi/discovery/tcp/TcpDiscoverySelfTest.java @@ -115,6 +115,7 @@ import static org.apache.ignite.internal.GridComponent.DiscoveryDataExchangeType.MARSHALLER_PROC; import static org.apache.ignite.internal.MarshallerPlatformIds.JAVA_ID; import static org.apache.ignite.spi.IgnitePortProtocol.UDP; +import static org.junit.Assert.assertNotEquals; /** * Test for {@link TcpDiscoverySpi}. @@ -2363,6 +2364,57 @@ public void testCheckRingLatency() throws Exception { } } + /** + * Verifies that {@link TcpDiscoverySpi#getEffectiveNodeAddresses(TcpDiscoveryNode, boolean)} + * does not throw NPE when a far node has an unresolved {@link InetSocketAddress} + * (i.e., {@code getAddress() == null}) and that such addresses are filtered out. + * + * @throws Exception If failed. + */ + @Test + public void testGetEffectiveNodeAddressesFiltersUnresolvedAddressWithoutNpe() throws Exception { + try { + IgniteEx ignite1 = startGrid(1); + IgniteEx ignite2 = startGrid(2); + + TcpDiscoverySpi spi1 = (TcpDiscoverySpi)ignite1.configuration().getDiscoverySpi(); + + TcpDiscoveryNode realNode = (TcpDiscoveryNode)spi1.getNode(ignite2.localNode().id()); + + assertNotNull(realNode); + + // Wrap real node so that socketAddresses() returns an unresolved InetSocketAddress. + TcpDiscoveryNode nodeWithUnresolvedAddr = new TestTcpDiscoveryNodeWithUnresolvedAddress(realNode); + + // Ensure the wrapped node is considered different from the local node by UUID. + assertNotEquals(ignite1.localNode().id(), nodeWithUnresolvedAddr.id()); + + // Verify test node has at least one unresolved address. + boolean hasUnresolved = false; + + for (InetSocketAddress addr : nodeWithUnresolvedAddr.socketAddresses()) { + if (addr.getAddress() == null) { + hasUnresolved = true; + + break; + } + } + + assertTrue("Test node should have at least one unresolved address", hasUnresolved); + + // Should not throw NPE; unresolved addresses must be filtered out. + LinkedHashSet effAddrs = spi1.getEffectiveNodeAddresses(nodeWithUnresolvedAddr, false); + + assertNotNull(effAddrs); + + for (InetSocketAddress addr : effAddrs) + assertNotNull("Unresolved address (getAddress() == null) should have been filtered out", addr.getAddress()); + } + finally { + stopAllGrids(); + } + } + /** * @param nodeName Node name. * @throws Exception If failed. @@ -2917,4 +2969,27 @@ private Ignite startGridNoOptimize(int idx) throws Exception { private Ignite startGridNoOptimize(String igniteInstanceName) throws Exception { return G.start(getConfiguration(igniteInstanceName)); } + + /** Test node wrapper that injects an unresolved {@link InetSocketAddress} into the addresses list. */ + private static class TestTcpDiscoveryNodeWithUnresolvedAddress extends TcpDiscoveryNode { + /** */ + public TestTcpDiscoveryNodeWithUnresolvedAddress() { + // No-op. + } + + /** @param delegate Original node to delegate to. */ + public TestTcpDiscoveryNodeWithUnresolvedAddress(TcpDiscoveryNode delegate) { + super(delegate); + } + + /** {@inheritDoc} */ + @Override public Collection socketAddresses() { + List addrs = new ArrayList<>(super.socketAddresses()); + + // Unresolved address: getAddress() returns null. + addrs.add(new InetSocketAddress("unresolved-host-name-that-does-not-resolve-xyz", 47500)); + + return addrs; + } + } }