diff --git a/modules/core/pom.xml b/modules/core/pom.xml index 959a706488e3c..5c2f906a91edb 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -237,6 +237,20 @@ 0.23.0 test + + + org.testcontainers + testcontainers + ${testcontainers.version} + test + + + + org.slf4j + slf4j-simple + 2.0.12 + test + diff --git a/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java new file mode 100644 index 0000000000000..4b2802208cde2 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteClusterContainer.java @@ -0,0 +1,92 @@ +/* + * 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.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; + + /** Network. */ + private final Network net = Network.newNetwork(); + + /** @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++) { + String hostname = "node" + (1 + i); + + IgniteContainer ignite = new IgniteContainer(ver, 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(); + + 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 new file mode 100644 index 0000000000000..b22f3bdb162f9 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteContainer.java @@ -0,0 +1,228 @@ +/* + * 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.IOException; +import java.time.Duration; +import java.time.ZoneId; +import java.util.Arrays; +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.internal.IgniteInterruptedCheckedException; +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 static org.apache.ignite.testframework.GridTestUtils.waitForCondition; +import static org.testcontainers.utility.MountableFile.forClasspathResource; + +/** Ignite container. */ +public class IgniteContainer extends GenericContainer { + /** Property for local work directory. */ + 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, + System.getProperty("user.home") + "/test-ignite-work"); + + /** 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 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)"); + + /** */ + 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; + + /** Hostname. */ + private final String hostname; + + /** Node ID. */ + private final String nodeId; + + /** Constructor. */ + 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()); + + withFileSystemBind(LOCAL_WORK_DIR_PATH, WORK_DIR_PATH, BindMode.READ_WRITE); + + withCopyFileToContainer(forClasspathResource("docker/test-config.xml"), CFG_PATH); + + withNetwork(net); + withNetworkAliases(hostname); + withExposedPorts(THIN_CLIENT_PORT); + + waitingFor(Wait.forLogMessage(".*Node started.*", 1) + .withStartupTimeout(Duration.ofSeconds(60))); + } + + /** @return Hostname. */ + public String hostname() { + return hostname; + } + + /** @return Node ID. */ + public String nodeId() { + return nodeId; + } + + /** Activate cluster. */ + 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); + } + } + + /** @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); + } + + /** */ + private String execControl(String... cmd) { + String[] fullCmd = new String[cmd.length + 2]; + + fullCmd[0] = ROOT_DIR_PATH + "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(); + } + + /** Rolling upgrade status. */ + 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..4617b8533f934 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/testcontainers/IgniteRebalanceOnUpgradeTest.java @@ -0,0 +1,142 @@ +/* + * 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.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; + +/** 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/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/test-config.xml b/modules/core/src/test/resources/docker/test-config.xml new file mode 100644 index 0000000000000..ce6f034dbdd5d --- /dev/null +++ b/modules/core/src/test/resources/docker/test-config.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/parent/pom.xml b/parent/pom.xml index ac68955a0d172..c28aea123f4c5 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -110,6 +110,7 @@ 5.3.39 3.5.4 10.0.27 + 2.0.5 0.8.3 3.9.5 1.5.7-8