diff --git a/.gitignore b/.gitignore
index ef98e66b5ca52..0029600d2ee39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -86,3 +86,11 @@ packages
**/venv
**/.pytest_cache
**/pyignite.egg-info
+
+#Ducktape
+/results
+.ducktape
+*.pyc
+/tests/venv
+modules/ducktests/tests/docker/build/**
+modules/ducktests/tests/.tox
diff --git a/.travis.yml b/.travis.yml
index 73117e8f68d49..c2dbd305a0ff1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,6 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+_ducktape-tox: &ducktape-tox
+ install: pip install tox
+ before_script: cd modules/ducktests/tests
+
matrix:
include:
- language: java
@@ -50,3 +54,21 @@ matrix:
dotnet: 3.1.101
script:
- dotnet build modules/platforms/dotnet/Apache.Ignite.DotNetCore.sln
+
+ - language: python
+ python: 3.6.12
+ <<: *ducktape-tox
+ script:
+ - tox -e py36
+
+ - language: python
+ python: 3.7.9
+ <<: *ducktape-tox
+ script:
+ - tox -e py37
+
+ - language: python
+ python: 3.8.5
+ <<: *ducktape-tox
+ script:
+ - tox -e linter,codestyle,py38
diff --git a/bin/control.sh b/bin/control.sh
index d2be94e9d8388..659399b75d203 100755
--- a/bin/control.sh
+++ b/bin/control.sh
@@ -1,12 +1,9 @@
#!/usr/bin/env bash
-if [ ! -z "${IGNITE_SCRIPT_STRICT_MODE:-}" ]
-then
- set -o nounset
- set -o errexit
- set -o pipefail
- set -o errtrace
- set -o functrace
-fi
+set -o nounset
+set -o errexit
+set -o pipefail
+set -o errtrace
+set -o functrace
#
# Licensed to the Apache Software Foundation (ASF) under one or more
@@ -166,7 +163,7 @@ elif [ $version -ge 11 ] ; then
${CONTROL_JVM_OPTS}"
fi
-if [ -n "${JVM_OPTS}" ] ; then
+if [ -n "${JVM_OPTS:-}" ] ; then
echo "JVM_OPTS environment variable is set, but will not be used. To pass JVM options use CONTROL_JVM_OPTS"
echo "JVM_OPTS=${JVM_OPTS}"
fi
diff --git a/bin/include/build-classpath.sh b/bin/include/build-classpath.sh
index dbcd81e24e861..0625f41448624 100644
--- a/bin/include/build-classpath.sh
+++ b/bin/include/build-classpath.sh
@@ -47,21 +47,27 @@ includeToClassPath() {
for file in $1/*
do
- if [ -d ${file} ] && [ -d "${file}/target" ]; then
- if [ -d "${file}/target/classes" ]; then
- IGNITE_LIBS=${IGNITE_LIBS}${SEP}${file}/target/classes
- fi
+ if [[ -z "${EXCLUDE_MODULES:-}" ]] || [[ ${EXCLUDE_MODULES:-} != *"`basename $file`"* ]]; then
+ if [ -d ${file} ] && [ -d "${file}/target" ]; then
+ if [ -d "${file}/target/classes" ]; then
+ IGNITE_LIBS=${IGNITE_LIBS}${SEP}${file}/target/classes
+ fi
- if [ -d "${file}/target/test-classes" ]; then
- IGNITE_LIBS=${IGNITE_LIBS}${SEP}${file}/target/test-classes
- fi
+ if [[ -z "${EXCLUDE_TEST_CLASSES:-}" ]]; then
+ if [ -d "${file}/target/test-classes" ]; then
+ IGNITE_LIBS=${IGNITE_LIBS}${SEP}${file}/target/test-classes
+ fi
+ fi
- if [ -d "${file}/target/libs" ]; then
- IGNITE_LIBS=${IGNITE_LIBS}${SEP}${file}/target/libs/*
+ if [ -d "${file}/target/libs" ]; then
+ IGNITE_LIBS=${IGNITE_LIBS}${SEP}${file}/target/libs/*
+ fi
fi
+ else
+ echo "$file excluded by EXCLUDE_MODULES settings"
fi
done
-
+
IFS=$SAVEIFS
}
diff --git a/modules/ducktests/licenses/apache-2.0.txt b/modules/ducktests/licenses/apache-2.0.txt
new file mode 100644
index 0000000000000..d645695673349
--- /dev/null
+++ b/modules/ducktests/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/modules/ducktests/pom.xml b/modules/ducktests/pom.xml
new file mode 100644
index 0000000000000..26226c62e788f
--- /dev/null
+++ b/modules/ducktests/pom.xml
@@ -0,0 +1,214 @@
+
+
+
+
+
+
+ 4.0.0
+
+
+ org.apache.ignite
+ ignite-parent
+ 1
+ ../../parent
+
+
+ ignite-ducktests
+ 2.10.0-SNAPSHOT
+ http://ignite.apache.org
+
+
+
+ org.apache.ignite
+ ignite-core
+ ${project.version}
+
+
+
+ org.apache.ignite
+ ignite-indexing
+ ${project.version}
+
+
+
+ org.apache.ignite
+ ignite-spark
+ ${project.version}
+
+
+ org.apache.curator
+ curator-recipes
+
+
+ org.apache.curator
+ curator-framework
+
+
+ org.apache.curator
+ curator-client
+
+
+ org.apache.zookeeper
+ zookeeper
+
+
+
+
+
+ org.apache.spark
+ spark-core_2.11
+ ${spark.version}
+
+
+ org.apache.curator
+ curator-recipes
+
+
+ org.apache.curator
+ curator-framework
+
+
+ org.apache.curator
+ curator-client
+
+
+ org.apache.zookeeper
+ zookeeper
+
+
+
+
+
+ org.apache.spark
+ spark-sql_2.11
+ ${spark.version}
+
+
+
+ org.apache.spark
+ spark-tags_2.11
+ ${spark.version}
+
+
+
+ org.apache.spark
+ spark-catalyst_2.11
+ ${spark.version}
+
+
+
+ org.apache.spark
+ spark-network-shuffle_2.11
+ ${spark.version}
+
+
+
+ org.apache.spark
+ spark-network-common_2.11
+ ${spark.version}
+
+
+
+ com.fasterxml.woodstox
+ woodstox-core
+ 5.0.3
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+
+ org.codehaus.woodstox
+ stax2-api
+ 3.1.4
+
+
+
+ org.apache.htrace
+ htrace-core4
+ 4.1.0-incubating
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+
+ maven-dependency-plugin
+
+
+ copy-libs
+ test-compile
+
+ copy-dependencies
+
+
+ org.apache.ignite
+ target/libs
+ compile
+ false
+
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-sources
+ generate-sources
+
+ add-source
+
+
+
+ tests
+
+
+
+
+
+
+
+
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/ContinuousDataLoadApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/ContinuousDataLoadApplication.java
new file mode 100644
index 0000000000000..634bdde4aa997
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/ContinuousDataLoadApplication.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.Affinity;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import org.apache.ignite.transactions.Transaction;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+/**
+ * Keeps data load until stopped.
+ */
+public class ContinuousDataLoadApplication extends IgniteAwareApplication {
+ /** Logger. */
+ private static final Logger log = LogManager.getLogger(ContinuousDataLoadApplication.class.getName());
+
+ /** */
+ private IgniteCache cache;
+
+ /** Node set to exclusively put data on if required. */
+ private List nodesToLoad = Collections.emptyList();
+
+ /** */
+ private Affinity aff;
+
+ /** Data number to put before notifying of the initialized state. */
+ private int warmUpCnt;
+
+ /** {@inheritDoc} */
+ @Override protected void run(JsonNode jsonNode) {
+ Config cfg = parseConfig(jsonNode);
+
+ init(cfg);
+
+ log.info("Generating data in background...");
+
+ long notifyTime = System.nanoTime();
+
+ int loaded = 0;
+
+ while (active()) {
+ try (Transaction tx = cfg.transactional ? ignite.transactions().txStart() : null) {
+ for (int i = 0; i < cfg.range && active(); ++i) {
+ if (skipDataKey(i))
+ continue;
+
+ cache.put(i, i);
+
+ ++loaded;
+
+ if (notifyTime + TimeUnit.MILLISECONDS.toNanos(1500) < System.nanoTime())
+ notifyTime = System.nanoTime();
+
+ // Delayed notify of the initialization to make sure the data load has completelly began and
+ // has produced some valuable amount of data.
+ if (!inited() && warmUpCnt == loaded)
+ markInitialized();
+ }
+
+ if (tx != null && active())
+ tx.commit();
+ }
+ }
+
+ log.info("Background data generation finished.");
+
+ markFinished();
+ }
+
+ /**
+ * @return {@code True} if data should not be put for {@code dataKey}. {@code False} otherwise.
+ */
+ private boolean skipDataKey(int dataKey) {
+ if (!nodesToLoad.isEmpty()) {
+ for (ClusterNode n : nodesToLoad) {
+ if (aff.isPrimary(n, dataKey))
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Prepares run settings based on {@code cfg}.
+ */
+ private void init(Config cfg) {
+ cache = ignite.getOrCreateCache(cfg.cacheName);
+
+ if (cfg.targetNodes != null && !cfg.targetNodes.isEmpty()) {
+ nodesToLoad = ignite.cluster().nodes().stream().filter(n -> cfg.targetNodes.contains(n.id().toString()))
+ .collect(Collectors.toList());
+
+ aff = ignite.affinity(cfg.cacheName);
+ }
+
+ warmUpCnt = cfg.warmUpRange < 1 ? (int)Math.max(1, 0.1f * cfg.range) : cfg.warmUpRange;
+ }
+
+ /**
+ * Converts Json-represented config into {@code Config}.
+ */
+ private static Config parseConfig(JsonNode node) {
+ ObjectMapper objMapper = new ObjectMapper();
+ objMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+
+ Config cfg;
+
+ try {
+ cfg = objMapper.treeToValue(node, Config.class);
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Unable to parse config.", e);
+ }
+
+ return cfg;
+ }
+
+ /**
+ * The configuration holder.
+ */
+ private static class Config {
+ /** Name of the cache. */
+ private String cacheName;
+
+ /** Data/keys number to load. */
+ private int range;
+
+ /** Node id set. If not empty, data will be load only on this nodes. */
+ private Set targetNodes;
+
+ /** If {@code true}, data will be put within transaction. */
+ private boolean transactional;
+
+ /**
+ * Data number to warn-up and to delay the init-notification. If < 1, ignored and considered default 10% of
+ * {@code range}.
+ */
+ private int warmUpRange;
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/DataGenerationApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/DataGenerationApplication.java
new file mode 100644
index 0000000000000..a65644aec24ec
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/DataGenerationApplication.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteDataStreamer;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+
+/**
+ *
+ */
+public class DataGenerationApplication extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override protected void run(JsonNode jsonNode) {
+ log.info("Creating cache...");
+
+ IgniteCache cache = ignite.createCache(jsonNode.get("cacheName").asText());
+
+ try (IgniteDataStreamer stmr = ignite.dataStreamer(cache.getName())) {
+ for (int i = 0; i < jsonNode.get("range").asInt(); i++) {
+ stmr.addData(i, i);
+
+ if (i % 10_000 == 0)
+ log.info("Streamed " + i + " entries");
+ }
+ }
+
+ markSyncExecutionComplete();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularAffinityBackupFilter.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularAffinityBackupFilter.java
new file mode 100644
index 0000000000000..6ecdd3fca6792
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularAffinityBackupFilter.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.cellular_affinity_test;
+
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.lang.IgniteBiPredicate;
+
+/**
+ *
+ */
+public class CellularAffinityBackupFilter implements IgniteBiPredicate> {
+ /** */
+ private static final long serialVersionUID = 1L;
+
+ /** Attribute name. */
+ private final String attrName;
+
+ /**
+ * @param attrName The attribute name for the attribute to compare.
+ */
+ public CellularAffinityBackupFilter(String attrName) {
+ this.attrName = attrName;
+ }
+
+ /**
+ * Defines a predicate which returns {@code true} if a node is acceptable for a backup
+ * or {@code false} otherwise. An acceptable node is one where its attribute value
+ * is exact match with previously selected nodes. If an attribute does not
+ * exist on exactly one node of a pair, then the attribute does not match. If the attribute
+ * does not exist both nodes of a pair, then the attribute matches.
+ *
+ * @param candidate A node that is a candidate for becoming a backup node for a partition.
+ * @param previouslySelected A list of primary/backup nodes already chosen for a partition.
+ * The primary is first.
+ */
+ @Override public boolean apply(ClusterNode candidate, List previouslySelected) {
+ for (ClusterNode node : previouslySelected)
+ return Objects.equals(candidate.attribute(attrName), node.attribute(attrName));
+
+ return true;
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularPreparedTxStreamer.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularPreparedTxStreamer.java
new file mode 100644
index 0000000000000..9c7a97eb484f4
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularPreparedTxStreamer.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.internal.ducktest.tests.cellular_affinity_test;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.Affinity;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import org.apache.ignite.internal.processors.cache.transactions.TransactionProxyImpl;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.transactions.Transaction;
+
+/**
+ * Prepares transactions at specified cell.
+ */
+public class CellularPreparedTxStreamer extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override protected void run(JsonNode jsonNode) throws Exception {
+ final String cacheName = jsonNode.get("cacheName").asText();
+ final String attr = jsonNode.get("attr").asText();
+ final String cell = jsonNode.get("cell").asText();
+ final int txCnt = jsonNode.get("txCnt").asInt();
+
+ markInitialized();
+
+ waitForActivation();
+
+ IgniteCache cache = ignite.getOrCreateCache(cacheName);
+
+ log.info("Starting Prepared Txs...");
+
+ Affinity aff = ignite.affinity(cacheName);
+
+ int cnt = 0;
+ int i = -1; // Negative keys to have no intersection with load.
+
+ while (cnt != txCnt && !terminated()) {
+ Collection nodes = aff.mapKeyToPrimaryAndBackups(i);
+
+ Map stat = nodes.stream().collect(
+ Collectors.groupingBy(n -> n.attributes().get(attr), Collectors.counting()));
+
+ assert 1 == stat.keySet().size() :
+ "Partition should be located on nodes from only one cell " +
+ "[key=" + i + ", nodes=" + nodes.size() + ", stat=" + stat + "]";
+
+ if (stat.containsKey(cell)) {
+ cnt++;
+
+ Transaction tx = ignite.transactions().txStart();
+
+ cache.put(i, i);
+
+ ((TransactionProxyImpl, ?>)tx).tx().prepare(true);
+
+ if (cnt % 100 == 0)
+ log.info("Long Tx prepared [key=" + i + ",cnt=" + cnt + ", cell=" + stat.keySet() + "]");
+ }
+
+ i--;
+ }
+
+ log.info("ALL_TRANSACTIONS_PREPARED (" + cnt + ")");
+
+ while (!terminated()) {
+ log.info("Waiting for SIGTERM.");
+
+ U.sleep(1000);
+ }
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularTxStreamer.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularTxStreamer.java
new file mode 100644
index 0000000000000..e0c5eb019fd7e
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/CellularTxStreamer.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.cellular_affinity_test;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.Affinity;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+
+/**
+ * Streams transactions to specified cell.
+ */
+public class CellularTxStreamer extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override public void run(JsonNode jsonNode) throws Exception {
+ String cacheName = jsonNode.get("cacheName").asText();
+ int warmup = jsonNode.get("warmup").asInt();
+ String cell = jsonNode.get("cell").asText();
+ String attr = jsonNode.get("attr").asText();
+
+ markInitialized();
+
+ waitForActivation();
+
+ IgniteCache cache = ignite.getOrCreateCache(cacheName);
+
+ int precision = 5;
+
+ long[] latencies = new long[precision];
+ LocalDateTime[] opStartTimes = new LocalDateTime[precision];
+
+ Arrays.fill(latencies, -1);
+
+ int cnt = 0;
+
+ long initTime = 0;
+
+ boolean record = false;
+
+ Affinity aff = ignite.affinity(cacheName);
+
+ List cellKeys = new ArrayList<>();
+
+ int candidate = 0;
+
+ while (cellKeys.size() < 100) {
+ Collection nodes = aff.mapKeyToPrimaryAndBackups(++candidate);
+
+ Set stat = nodes.stream()
+ .filter(n -> n.attributes().get(attr).equals(cell))
+ .collect(Collectors.toSet());
+
+ if (stat.isEmpty())
+ continue;
+
+ assert nodes.size() == stat.size();
+
+ cellKeys.add(candidate);
+ }
+
+ while (!terminated()) {
+ cnt++;
+
+ LocalDateTime start = LocalDateTime.now();
+
+ long from = System.nanoTime();
+
+ cache.put(cellKeys.get(cnt % cellKeys.size()), cnt); // Cycled update.
+
+ long latency = System.nanoTime() - from;
+
+ if (!record && cnt > warmup) {
+ record = true;
+
+ initTime = System.currentTimeMillis();
+
+ log.info("WARMUP_FINISHED");
+ }
+
+ if (record) {
+ for (int i = 0; i < latencies.length; i++) {
+ if (latencies[i] <= latency) {
+ System.arraycopy(latencies, i, latencies, i + 1, latencies.length - i - 1);
+ System.arraycopy(opStartTimes, i, opStartTimes, i + 1, opStartTimes.length - i - 1);
+
+ latencies[i] = latency;
+ opStartTimes[i] = start;
+
+ break;
+ }
+ }
+ }
+
+ if (cnt % 1000 == 0)
+ log.info("APPLICATION_STREAMED " + cnt + " transactions [worst_latency=" + Arrays.toString(latencies) + "]");
+ }
+
+ List result = new ArrayList<>();
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
+
+ for (int i = 0; i < precision; i++)
+ result.add(Duration.ofNanos(latencies[i]).toMillis() + " ms at " + formatter.format(opStartTimes[i]));
+
+ recordResult("WORST_LATENCY", result.toString());
+ recordResult("STREAMED", cnt - warmup);
+ recordResult("MEASURE_DURATION", System.currentTimeMillis() - initTime);
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/DistributionChecker.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/DistributionChecker.java
new file mode 100644
index 0000000000000..22b2c94ffc625
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/cellular_affinity_test/DistributionChecker.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.cellular_affinity_test;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.cluster.ClusterNode;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+
+/**
+ *
+ */
+public class DistributionChecker extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override protected void run(JsonNode jsonNode) {
+ String cacheName = jsonNode.get("cacheName").asText();
+ String attr = jsonNode.get("attr").asText();
+ int nodesPerCell = jsonNode.get("nodesPerCell").intValue();
+
+ assert ignite.cluster().forServers().nodes().size() > nodesPerCell : "Cluster should contain more than one cell";
+
+ for (int i = 0; i < 10_000; i++) {
+ Collection nodes = ignite.affinity(cacheName).mapKeyToPrimaryAndBackups(i);
+
+ Map stat = nodes.stream().collect(
+ Collectors.groupingBy(n -> n.attributes().get(attr), Collectors.counting()));
+
+ log.info("Checking [key=" + i + ", stat=" + stat + "]");
+
+ assert 1 == stat.keySet().size() : "Partition should be located on nodes from only one cell [stat=" + stat + "]";
+
+ assert nodesPerCell == stat.values().iterator().next() :
+ "Partition should be located on all nodes of the cell [stat=" + stat + "]";
+ }
+
+ markSyncExecutionComplete();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/client_test/IgniteCachePutClient.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/client_test/IgniteCachePutClient.java
new file mode 100644
index 0000000000000..0a337ce2a28e0
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/client_test/IgniteCachePutClient.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.client_test;
+
+import java.util.Optional;
+import java.util.UUID;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+
+/**
+ * Java client. Tx put operation
+ */
+public class IgniteCachePutClient extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override protected void run(JsonNode jsonNode) throws Exception {
+ String cacheName = jsonNode.get("cacheName").asText();
+
+ long pacing = Optional.ofNullable(jsonNode.get("pacing"))
+ .map(JsonNode::asLong)
+ .orElse(0L);
+
+ log.info("Test props:" +
+ " cacheName=" + cacheName +
+ " pacing=" + pacing);
+
+ IgniteCache cache = ignite.getOrCreateCache(cacheName);
+ log.info("Node name: " + ignite.name() + " starting cache operations.");
+
+ markInitialized();
+
+ while (!terminated()) {
+ UUID uuid = UUID.randomUUID();
+
+ long startTime = System.nanoTime();
+
+ cache.put(uuid, uuid);
+
+ long resultTime = System.nanoTime() - startTime;
+
+ log.info("Success put, latency: " + resultTime + "ns.");
+
+ Thread.sleep(pacing);
+ }
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/control_utility/LongRunningTransactionsGenerator.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/control_utility/LongRunningTransactionsGenerator.java
new file mode 100644
index 0000000000000..3bbf732c7e540
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/control_utility/LongRunningTransactionsGenerator.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.control_utility;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.locks.Lock;
+import javax.cache.CacheException;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteTransactions;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import org.apache.ignite.lang.IgniteUuid;
+import org.apache.ignite.transactions.Transaction;
+import org.apache.ignite.transactions.TransactionRollbackException;
+
+/**
+ * Run long running transactions on node with specified param.
+ */
+public class LongRunningTransactionsGenerator extends IgniteAwareApplication {
+ /** */
+ private static final Duration TOPOLOGY_WAIT_TIMEOUT = Duration.ofSeconds(60);
+
+ /** */
+ private static final String KEYS_LOCKED_MESSAGE = "APPLICATION_KEYS_LOCKED";
+
+ /** */
+ private static final String LOCKED_KEY_PREFIX = "KEY_";
+
+ /** */
+ private volatile Executor pool;
+
+ /** {@inheritDoc} */
+ @Override protected void run(JsonNode jsonNode) throws Exception {
+ IgniteCache cache = ignite.cache(jsonNode.get("cache_name").asText());
+
+ int txCount = jsonNode.get("tx_count") != null ? jsonNode.get("tx_count").asInt() : 1;
+
+ int txSize = jsonNode.get("tx_size") != null ? jsonNode.get("tx_size").asInt() : 1;
+
+ String keyPrefix = jsonNode.get("key_prefix") != null ? jsonNode.get("key_prefix").asText() : LOCKED_KEY_PREFIX;
+
+ String label = jsonNode.get("label") != null ? jsonNode.get("label").asText() : null;
+
+ long expectedTopologyVersion = jsonNode.get("wait_for_topology_version") != null ?
+ jsonNode.get("wait_for_topology_version").asLong() : -1L;
+
+ CountDownLatch lockLatch = new CountDownLatch(txCount);
+
+ pool = Executors.newFixedThreadPool(2 * txCount);
+
+ markInitialized();
+
+ if (expectedTopologyVersion > 0) {
+ log.info("Start waiting for topology version: " + expectedTopologyVersion + ", " +
+ "current version is: " + ignite.cluster().topologyVersion());
+
+ long start = System.nanoTime();
+
+ while (ignite.cluster().topologyVersion() < expectedTopologyVersion
+ && Duration.ofNanos(start - System.nanoTime()).compareTo(TOPOLOGY_WAIT_TIMEOUT) < 0)
+ Thread.sleep(100L);
+
+ log.info("Finished waiting for topology version: " + expectedTopologyVersion + ", " +
+ "current version is: " + ignite.cluster().topologyVersion());
+ }
+
+ for (int i = 0; i < txCount; i++) {
+ String key = keyPrefix + i;
+
+ pool.execute(() -> {
+ Lock lock = cache.lock(key);
+
+ lock.lock();
+
+ try {
+ lockLatch.countDown();
+
+ while (!terminated())
+ Thread.sleep(100L);
+ }
+ catch (InterruptedException e) {
+ markBroken(new RuntimeException("Unexpected thread interruption", e));
+
+ Thread.currentThread().interrupt();
+ }
+ finally {
+ lock.unlock();
+ }
+ });
+ }
+
+ lockLatch.await();
+
+ log.info(KEYS_LOCKED_MESSAGE);
+
+ CountDownLatch txLatch = new CountDownLatch(txCount);
+
+ for (int i = 0; i < txCount; i++) {
+ Map data = new TreeMap<>();
+
+ for (int j = 0; j < txSize; j++) {
+ String key = keyPrefix + (j == 0 ? String.valueOf(i) : i + "_" + j);
+
+ data.put(key, key);
+ }
+
+ IgniteTransactions igniteTransactions = label != null ? ignite.transactions().withLabel(label) :
+ ignite.transactions();
+
+ pool.execute(() -> {
+ IgniteUuid xid = null;
+
+ try (Transaction tx = igniteTransactions.txStart()) {
+ xid = tx.xid();
+
+ cache.putAll(data);
+
+ tx.commit();
+ }
+ catch (Exception e) {
+ if (e instanceof CacheException && e.getCause() != null &&
+ e.getCause() instanceof TransactionRollbackException)
+ recordResult("TX_ID", xid != null ? xid.toString() : "");
+ else
+ markBroken(new RuntimeException("Transaction is rolled back with unexpected error", e));
+ }
+ finally {
+ txLatch.countDown();
+ }
+ });
+ }
+
+ txLatch.await();
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/Account.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/Account.java
new file mode 100644
index 0000000000000..b4a5ac216cafd
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/Account.java
@@ -0,0 +1,60 @@
+package org.apache.ignite.internal.ducktest.tests.pds_compatibility_test;
+
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+
+import java.io.Serializable;
+
+public class Account implements Serializable {
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String firstName;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String lastName;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String catName;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String dogName;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String city;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String country;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String eMail;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String phoneNumber;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private String socialNumber;
+ @QuerySqlField(index = true, inlineSize = 48)
+ private Long postIndex;
+ public long balance;
+
+ public Account(String firstName, String lastName, String catName, String dogName, String city, String country, String eMail, String phoneNumber, String socialNumber, Long postIndex, long balance) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.catName = catName;
+ this.dogName = dogName;
+ this.city = city;
+ this.country = country;
+ this.eMail = eMail;
+ this.phoneNumber = phoneNumber;
+ this.socialNumber = socialNumber;
+ this.postIndex = postIndex;
+ this.balance = balance;
+ }
+
+ @Override
+ public String toString() {
+ return "Account{" +
+ "firstName='" + firstName + '\'' +
+ ", lastName='" + lastName + '\'' +
+ ", catName='" + catName + '\'' +
+ ", dogName='" + dogName + '\'' +
+ ", city='" + city + '\'' +
+ ", country='" + country + '\'' +
+ ", eMail='" + eMail + '\'' +
+ ", phoneNumber='" + phoneNumber + '\'' +
+ ", socialNumber='" + socialNumber + '\'' +
+ ", postIndex='" + postIndex + '\'' +
+ ", balance=" + balance +
+ '}';
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/DictionaryCacheApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/DictionaryCacheApplication.java
new file mode 100644
index 0000000000000..648089cdd11f8
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/DictionaryCacheApplication.java
@@ -0,0 +1,35 @@
+package org.apache.ignite.internal.ducktest.tests.pds_compatibility_test;
+
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.UUID;
+
+public class DictionaryCacheApplication extends IgniteAwareApplication {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void run(JsonNode jsonNode) {
+ log.info("Creating cache...");
+
+ CacheConfiguration cacheCfg = new CacheConfiguration<>();
+ cacheCfg.setName(jsonNode.get("cacheName").asText())
+ .setCacheMode(CacheMode.REPLICATED)
+ .setAtomicityMode(CacheAtomicityMode.ATOMIC)
+ .setIndexedTypes(Long.class, String.class);
+
+ IgniteCache cache = ignite.getOrCreateCache(cacheCfg);
+
+ for (long i = 0; i < jsonNode.get("range").asLong(); i++) {
+ String uuid = UUID.randomUUID().toString();
+ cache.put(i, uuid);
+ }
+ log.info("Cache created");
+ markSyncExecutionComplete();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/DictionaryCacheApplicationCheck.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/DictionaryCacheApplicationCheck.java
new file mode 100644
index 0000000000000..3830e7a8a63d6
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/DictionaryCacheApplicationCheck.java
@@ -0,0 +1,26 @@
+package org.apache.ignite.internal.ducktest.tests.pds_compatibility_test;
+
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import com.fasterxml.jackson.databind.JsonNode;
+
+
+import java.util.UUID;
+
+public class DictionaryCacheApplicationCheck extends IgniteAwareApplication {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void run(JsonNode jsonNode) {
+ log.info("Opening cache...");
+
+ IgniteCache cache = ignite.cache(jsonNode.get("cacheName").asText());
+
+ for (long i = 0; i < jsonNode.get("range").asLong(); i++) {
+ cache.get(i);
+ }
+ log.info("Cache checked");
+ markSyncExecutionComplete();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/SqlCacheApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/SqlCacheApplication.java
new file mode 100644
index 0000000000000..0439bc0ee7108
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/SqlCacheApplication.java
@@ -0,0 +1,40 @@
+package org.apache.ignite.internal.ducktest.tests.pds_compatibility_test;
+
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.CacheMode;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import com.fasterxml.jackson.databind.JsonNode;
+
+
+import java.util.UUID;
+
+public class SqlCacheApplication extends IgniteAwareApplication {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void run(JsonNode jsonNode) {
+ log.info("Creating cache...");
+
+ CacheConfiguration cacheCfg = new CacheConfiguration<>();
+ cacheCfg.setName(jsonNode.get("cacheName").asText())
+ .setCacheMode(CacheMode.PARTITIONED)
+ .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL)
+ .setBackups(3)
+ .setIndexedTypes(Long.class, Account.class);
+
+ IgniteCache cache = ignite.getOrCreateCache(cacheCfg);
+
+ for (long i = 0; i < jsonNode.get("range").asLong(); i++) {
+ String uuid = UUID.randomUUID().toString();
+ cache.put(i, new Account(
+ uuid, uuid, uuid, uuid, uuid, uuid,
+ uuid, uuid, uuid, i, i));
+ }
+
+ log.info("Cache created");
+ markSyncExecutionComplete();
+ }
+}
\ No newline at end of file
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/SqlCacheApplicationCheck.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/SqlCacheApplicationCheck.java
new file mode 100644
index 0000000000000..4f44b253788f0
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pds_compatibility_test/SqlCacheApplicationCheck.java
@@ -0,0 +1,58 @@
+package org.apache.ignite.internal.ducktest.tests.pds_compatibility_test;
+
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.util.List;
+
+public class SqlCacheApplicationCheck extends IgniteAwareApplication {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void run(JsonNode jsonNode) {
+ String count = null;
+
+ log.info("Open cache...");
+
+ IgniteCache cache = ignite.cache(jsonNode.get("cacheName").asText());
+
+ SqlFieldsQuery sql = new SqlFieldsQuery(
+ "select count(*) from Account");
+
+ log.info("Check cache size");
+
+ // Iterate over the result set.
+ try (QueryCursor> cursor = cache.query(sql)) {
+ count = cursor.getAll().get(0).get(0).toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ log.info("SELECT COUNT(*) FROM ACCOUNT return: {}", count);
+
+ assert count.equals(jsonNode.get("range").asText());
+
+ sql = new SqlFieldsQuery(
+ "explain SELECT * FROM Account WHERE postindex = 100");
+ String explain = null;
+
+ log.info("Check SQL Index");
+ try (QueryCursor> cursor = cache.query(sql)) {
+ explain = cursor.getAll().get(0).get(0).toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ log.info("SQL Execution Plan: {}", explain);
+
+ assert explain.contains("_IDX");
+
+ log.info("Cache checked");
+
+ markSyncExecutionComplete();
+ }
+}
\ No newline at end of file
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pme_free_switch_test/LongTxStreamerApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pme_free_switch_test/LongTxStreamerApplication.java
new file mode 100644
index 0000000000000..a9f10d7961264
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pme_free_switch_test/LongTxStreamerApplication.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.pme_free_switch_test;
+
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import org.apache.ignite.internal.processors.cache.transactions.IgniteInternalTx;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.transactions.Transaction;
+import org.apache.ignite.transactions.TransactionState;
+
+/**
+ *
+ */
+public class LongTxStreamerApplication extends IgniteAwareApplication {
+ /** Tx count. */
+ private static final int TX_CNT = 100;
+
+ /** Started. */
+ private static final CountDownLatch started = new CountDownLatch(TX_CNT);
+
+ /** {@inheritDoc} */
+ @Override public void run(JsonNode jsonNode) throws InterruptedException {
+ IgniteCache cache = ignite.getOrCreateCache(jsonNode.get("cacheName").asText());
+
+ log.info("Starting Long Tx...");
+
+ for (int i = -1; i >= -TX_CNT; i--) { // Negative keys to have no intersection with load.
+ int finalI = i;
+
+ new Thread(() -> {
+ Transaction tx = ignite.transactions().txStart();
+
+ cache.put(finalI, finalI);
+
+ log.info("Long Tx started [key=" + finalI + "]");
+
+ started.countDown();
+
+ while (!terminated()) {
+ if (tx.state() != TransactionState.ACTIVE) {
+ log.info("Transaction broken. [key=" + finalI + "]");
+
+ markBroken(new IllegalStateException(
+ "Illegal Tx state [key=" + finalI + " state=" + tx.state() + "]"));
+ }
+
+ try {
+ U.sleep(10);
+ }
+ catch (IgniteInterruptedCheckedException ignored) {
+ // No-op.
+ }
+ }
+
+ log.info("Stopping tx thread [key=" + finalI + " state=" + tx.state() + "]");
+
+ tx.rollback();
+
+ log.info("Finishing tx thread [key=" + finalI + " state=" + tx.state() + "]");
+
+ }).start();
+ }
+
+ started.await();
+
+ markInitialized();
+
+ while (!terminated()) {
+ Collection active =
+ ((IgniteEx)ignite).context().cache().context().tm().activeTransactions();
+
+ log.info("Long Txs are in progress [txs=" + active.size() + "]");
+
+ try {
+ U.sleep(100); // Keeping node/txs alive.
+ }
+ catch (IgniteInterruptedCheckedException ignored) {
+ log.info("Waiting for interrupted.");
+ }
+ }
+
+ while (!((IgniteEx)ignite).context().cache().context().tm().activeTransactions().isEmpty())
+ try {
+ U.sleep(100); // Keeping node alive.
+ }
+ catch (IgniteInterruptedCheckedException ignored) {
+ log.info("Waiting for tx rollback.");
+ }
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pme_free_switch_test/SingleKeyTxStreamerApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pme_free_switch_test/SingleKeyTxStreamerApplication.java
new file mode 100644
index 0000000000000..c22fd3743c16c
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/pme_free_switch_test/SingleKeyTxStreamerApplication.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.internal.ducktest.tests.pme_free_switch_test;
+
+import java.time.Duration;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+
+/**
+ *
+ */
+public class SingleKeyTxStreamerApplication extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override public void run(JsonNode jsonNode) {
+ IgniteCache cache = ignite.getOrCreateCache(jsonNode.get("cacheName").asText());
+
+ int warmup = jsonNode.get("warmup").asInt();
+
+ int key = 0;
+ int cnt = 0;
+ long initTime = 0;
+ long maxLatency = -1;
+
+ boolean record = false;
+
+ while (!terminated()) {
+ cnt++;
+
+ long from = System.nanoTime();
+
+ cache.put(key++ % 100, key); // Cycled update.
+
+ long latency = System.nanoTime() - from;
+
+ if (!record && cnt > warmup) {
+ record = true;
+
+ initTime = System.currentTimeMillis();
+
+ markInitialized();
+ }
+
+ if (record) {
+ if (maxLatency < latency)
+ maxLatency = latency;
+ }
+
+ if (cnt % 100 == 0)
+ log.info("APPLICATION_STREAMED " + cnt + " transactions [max=" + maxLatency + "]");
+ }
+
+ recordResult("WORST_LATENCY", Duration.ofNanos(maxLatency).toMillis());
+ recordResult("STREAMED", cnt - warmup);
+ recordResult("MEASURE_DURATION", System.currentTimeMillis() - initTime);
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/self_test/TestKillableApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/self_test/TestKillableApplication.java
new file mode 100644
index 0000000000000..c9f4e6789df66
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/self_test/TestKillableApplication.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.self_test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ *
+ */
+public class TestKillableApplication extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override public void run(JsonNode jsonNode) throws Exception {
+ markInitialized();
+
+ while (!terminated())
+ U.sleep(100);
+
+ U.sleep(5000);
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/self_test/TestSelfKillableApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/self_test/TestSelfKillableApplication.java
new file mode 100644
index 0000000000000..f0e1f4d0a5b45
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/self_test/TestSelfKillableApplication.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.self_test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ *
+ */
+public class TestSelfKillableApplication extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override public void run(JsonNode jsonNode) throws Exception {
+ markInitialized();
+
+ U.sleep(5000);
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/smoke_test/AssertionApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/smoke_test/AssertionApplication.java
new file mode 100644
index 0000000000000..0cb0d207e7724
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/smoke_test/AssertionApplication.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.smoke_test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+
+/**
+ * Application to check java assertions to python exception conversion
+ */
+public class AssertionApplication extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override public void run(JsonNode jsonNode) {
+ assert false;
+
+ markInitialized();
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/smoke_test/SimpleApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/smoke_test/SimpleApplication.java
new file mode 100644
index 0000000000000..5c243b4ebbdec
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/tests/smoke_test/SimpleApplication.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.tests.smoke_test;
+
+import java.util.UUID;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.internal.ducktest.utils.IgniteAwareApplication;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ * Simple application that used in smoke tests
+ */
+public class SimpleApplication extends IgniteAwareApplication {
+ /** {@inheritDoc} */
+ @Override public void run(JsonNode jsonNode) {
+ IgniteCache cache = ignite.getOrCreateCache(UUID.randomUUID().toString());
+
+ cache.put(1, 2);
+
+ markInitialized();
+
+ while (!terminated()) {
+ try {
+ U.sleep(100); // Keeping node/txs alive.
+ }
+ catch (IgniteInterruptedCheckedException ignored) {
+ log.info("Waiting interrupted.");
+ }
+ }
+
+ markFinished();
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/utils/IgniteAwareApplication.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/utils/IgniteAwareApplication.java
new file mode 100644
index 0000000000000..3d65ab54a98c5
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/utils/IgniteAwareApplication.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.utils;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.cluster.ClusterState;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.IgniteInterruptedCheckedException;
+import org.apache.ignite.internal.processors.cache.GridCachePartitionExchangeManager;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import sun.misc.Signal;
+
+/**
+ *
+ */
+public abstract class IgniteAwareApplication {
+ /** Logger. */
+ protected static final Logger log = LogManager.getLogger(IgniteAwareApplication.class.getName());
+
+ /** App inited. */
+ private static final String APP_INITED = "IGNITE_APPLICATION_INITIALIZED";
+
+ /** App finished. */
+ private static final String APP_FINISHED = "IGNITE_APPLICATION_FINISHED";
+
+ /** App broken. */
+ private static final String APP_BROKEN = "IGNITE_APPLICATION_BROKEN";
+
+ /** App terminated. */
+ private static final String APP_TERMINATED = "IGNITE_APPLICATION_TERMINATED";
+
+ /** Inited. */
+ private static volatile boolean inited;
+
+ /** Finished. */
+ private static volatile boolean finished;
+
+ /** Broken. */
+ private static volatile boolean broken;
+
+ /** Terminated. */
+ private static volatile boolean terminated;
+
+ /** State mutex. */
+ private static final Object stateMux = new Object();
+
+ /** Ignite. */
+ protected Ignite ignite;
+
+ /** Cfg path. */
+ protected String cfgPath;
+
+ /**
+ * Default constructor.
+ */
+ protected IgniteAwareApplication() {
+ Signal.handle(new Signal("TERM"), signal -> {
+ log.info("SIGTERM recorded.");
+
+ if (!finished && !broken)
+ terminate();
+ else
+ log.info("Application already done [finished=" + finished + ", broken=" + broken + "]");
+
+ if (log.isDebugEnabled())
+ log.debug("Waiting for graceful termination...");
+
+ int iter = 0;
+
+ while (!finished && !broken) {
+ log.info("Waiting for graceful termination cycle... [iter=" + ++iter + "]");
+
+ if (iter == 100)
+ dumpThreads();
+
+ try {
+ U.sleep(100);
+ }
+ catch (IgniteInterruptedCheckedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ log.info("Application finished. Waiting for graceful termination.");
+ });
+
+ log.info("SIGTERM handler registered.");
+ }
+
+ /**
+ * Used to marks as started to perform actions. Suitable for async runs.
+ */
+ protected void markInitialized() {
+ log.info("Marking as initialized.");
+
+ synchronized (stateMux) {
+ assert !inited;
+ assert !finished;
+ assert !broken;
+
+ log.info(APP_INITED);
+
+ inited = true;
+ }
+ }
+
+ /**
+ *
+ */
+ protected void markFinished() {
+ log.info("Marking as finished.");
+
+ synchronized (stateMux) {
+ assert inited;
+ assert !finished;
+ assert !broken;
+
+ log.info(APP_FINISHED);
+
+ finished = true;
+ }
+ }
+
+ /**
+ *
+ */
+ public void markBroken(Throwable th) {
+ log.info("Marking as broken.");
+
+ synchronized (stateMux) {
+ recordResult("ERROR", th.toString());
+
+ if (broken) {
+ log.info("Already marked as broken.");
+
+ return;
+ }
+
+ assert !finished;
+
+ log.error(APP_BROKEN);
+
+ broken = true;
+ }
+ }
+
+ /**
+ *
+ */
+ private void terminate() {
+ log.info("Marking as terminated.");
+
+ synchronized (stateMux) {
+ assert !terminated;
+
+ log.info(APP_TERMINATED);
+
+ terminated = true;
+ }
+ }
+
+ /**
+ *
+ */
+ protected void markSyncExecutionComplete() {
+ markInitialized();
+ markFinished();
+ }
+
+ /**
+ *
+ */
+ protected boolean terminated() {
+ return terminated;
+ }
+
+ /**
+ *
+ */
+ protected boolean inited() {
+ return inited;
+ }
+
+ /**
+ *
+ */
+ protected boolean active() {
+ return !(terminated || broken || finished);
+ }
+
+ /**
+ * @param name Name.
+ * @param val Value.
+ */
+ protected void recordResult(String name, String val) {
+ assert !finished;
+
+ log.info(name + "->" + val + "<-");
+ }
+
+ /**
+ * @param name Name.
+ * @param val Value.
+ */
+ protected void recordResult(String name, long val) {
+ recordResult(name, String.valueOf(val));
+ }
+
+ /**
+ * @param jsonNode JSON node.
+ */
+ protected abstract void run(JsonNode jsonNode) throws Exception;
+
+ /**
+ * @param jsonNode JSON node.
+ */
+ public void start(JsonNode jsonNode) {
+ try {
+ log.info("Application params: " + jsonNode);
+
+ assert cfgPath != null;
+
+ run(jsonNode);
+
+ assert inited : "Was not properly initialized.";
+ assert finished : "Was not properly finished.";
+ }
+ catch (Throwable th) {
+ log.error("Unexpected Application failure... ", th);
+
+ if (!broken)
+ markBroken(th);
+ }
+ finally {
+ log.info("Application finished.");
+ }
+ }
+
+ /**
+ *
+ */
+ private static void dumpThreads() {
+ ThreadInfo[] infos = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true);
+
+ for (ThreadInfo info : infos) {
+ log.info(info.toString());
+
+ if ("main".equals(info.getThreadName())) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("main\n");
+
+ for (StackTraceElement element : info.getStackTrace()) {
+ sb.append("\tat ").append(element.toString());
+ sb.append('\n');
+ }
+
+ log.info(sb.toString());
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ protected void waitForActivation() throws IgniteInterruptedCheckedException {
+ boolean newApi = ignite.cluster().localNode().version().greaterThanEqual(2, 9, 0);
+
+ while (newApi ? ignite.cluster().state() != ClusterState.ACTIVE : !ignite.cluster().active()) {
+ U.sleep(100);
+
+ log.info("Waiting for cluster activation");
+ }
+
+ log.info("Cluster Activated");
+ }
+
+ /**
+ *
+ */
+ protected void waitForRebalanced() throws IgniteInterruptedCheckedException {
+ boolean possible = ignite.cluster().localNode().version().greaterThanEqual(2, 8, 0);
+
+ if (possible) {
+ GridCachePartitionExchangeManager, ?> mgr = ((IgniteEx)ignite).context().cache().context().exchange();
+
+ while (!mgr.lastFinishedFuture().rebalanced()) {
+ U.sleep(1000);
+
+ log.info("Waiting for cluster rebalance finish");
+ }
+
+ log.info("Cluster Rebalanced");
+ }
+ else
+ throw new UnsupportedOperationException("Operation supported since 2.8.0");
+ }
+}
diff --git a/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/utils/IgniteAwareApplicationService.java b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/utils/IgniteAwareApplicationService.java
new file mode 100644
index 0000000000000..7e8518035470a
--- /dev/null
+++ b/modules/ducktests/src/main/java/org/apache/ignite/internal/ducktest/utils/IgniteAwareApplicationService.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.ducktest.utils;
+
+import java.util.Base64;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgnitionEx;
+import org.apache.ignite.internal.processors.resource.GridSpringResourceContext;
+import org.apache.ignite.lang.IgniteBiTuple;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+/**
+ *
+ */
+public class IgniteAwareApplicationService {
+ /** Logger. */
+ private static final Logger log = LogManager.getLogger(IgniteAwareApplicationService.class.getName());
+
+ /**
+ * @param args Args.
+ */
+ public static void main(String[] args) throws Exception {
+ log.info("Starting Application... [params=" + args[0] + "]");
+
+ String[] params = args[0].split(",");
+
+ boolean startIgnite = Boolean.parseBoolean(params[0]);
+
+ Class> clazz = Class.forName(params[1]);
+
+ String cfgPath = params[2];
+
+ ObjectMapper mapper = new ObjectMapper();
+
+ JsonNode jsonNode = params.length > 3 ?
+ mapper.readTree(Base64.getDecoder().decode(params[3])) : mapper.createObjectNode();
+
+ IgniteAwareApplication app =
+ (IgniteAwareApplication)clazz.getConstructor().newInstance();
+
+ app.cfgPath = cfgPath;
+
+ if (startIgnite) {
+ log.info("Starting Ignite node...");
+
+ IgniteBiTuple cfgs = IgnitionEx.loadConfiguration(cfgPath);
+
+ IgniteConfiguration cfg = cfgs.get1();
+
+ try (Ignite ignite = Ignition.start(cfg)) {
+ app.ignite = ignite;
+
+ app.start(jsonNode);
+ }
+ finally {
+ log.info("Ignite instance closed. [interrupted=" + Thread.currentThread().isInterrupted() + "]");
+ }
+ }
+ else
+ app.start(jsonNode);
+ }
+}
diff --git a/modules/ducktests/src/main/resources/log4j.properties b/modules/ducktests/src/main/resources/log4j.properties
new file mode 100644
index 0000000000000..ecfe84af1dd3c
--- /dev/null
+++ b/modules/ducktests/src/main/resources/log4j.properties
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+
+# Root logger option
+log4j.rootLogger=INFO, stdout
+
+# Direct log messages to stdout
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.Target=System.out
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601}][%-5p][%t][%c{1}] %m%n
diff --git a/modules/ducktests/tests/MANIFEST.in b/modules/ducktests/tests/MANIFEST.in
new file mode 100644
index 0000000000000..6fcceb37144dd
--- /dev/null
+++ b/modules/ducktests/tests/MANIFEST.in
@@ -0,0 +1,16 @@
+# 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.
+
+recursive-include ignitetest **.j2
diff --git a/modules/ducktests/tests/README.md b/modules/ducktests/tests/README.md
new file mode 100644
index 0000000000000..fe9f64f88aac0
--- /dev/null
+++ b/modules/ducktests/tests/README.md
@@ -0,0 +1,61 @@
+## Overview
+The `ignitetest` framework provides basic functionality and services
+to write integration tests for Apache Ignite. This framework bases on
+the `ducktape` test framework, for information about it check the links:
+- https://github.com/confluentinc/ducktape - source code of the `ducktape`.
+- http://ducktape-docs.readthedocs.io - documentation to the `ducktape`.
+
+Structure of the `tests` directory is:
+- `./ignitetest/services` contains basic services functionality.
+- `./ignitetest/utils` contains utils for testing.
+- `./ignitetest/tests` contains tests.
+- `./checks` contains unit tests of utils, tests' decorators etc.
+
+Docker is used to emulate distributed environment. Single container represents
+a running node.
+
+## Requirements
+To just start tests locally the only requirement is preinstalled `docker`.
+For development process requirements are `python` >= 3.6.
+
+## Run tests locally
+- Change a current directory to`${IGNITE_HOME}`
+- Build Apache IGNITE invoking `${IGNITE_HOME}/scripts/build.sh`
+- Change a current directory to `${IGNITE_HOME}/modules/ducktests/tests`
+- Run tests in docker containers using a following command:
+```
+./docker/run_tests.sh
+```
+- For detailed help and instructions, use a following command:
+```
+./docker/run_tests.sh --help
+```
+- Test reports, including service logs, are located in the `${IGNITE_HOME}/results` directory.
+
+## Preparing development environment
+- Create a virtual environment and activate it using following commands:
+```
+python3 -m venv ~/.virtualenvs/ignite-ducktests-dev
+source ~/.virtualenvs/ignite-ducktests-dev/bin/activate
+```
+- Change a current directory to `${IGNITE_HOME}/modules/ducktests/tests`. We refer to it as `${DUCKTESTS_DIR}`.
+- Install requirements and `ignitetests` as editable using following commands:
+```
+pip install -r docker/requirements-dev.txt
+pip install -e .
+```
+---
+
+- For running unit tests invoke `pytest` in `${DUCKTESTS_DIR}`.
+- For checking codestyle invoke `flake8` in `${DUCKTESTS_DIR}`.
+- For running linter invoke `pylint --rcfile=tox.ini ignitetests checks` in `${DUCKTESTS_DIR}`.
+
+#### Run checks over multiple python's versions using tox (optional)
+All commits and PR's are checked against multiple python's version, namely 3.6, 3.7 and 3.8.
+If you want to check your PR as it will be checked on Travis CI, you should do following steps:
+
+- Install `pyenv`, see installation instruction [here](https://github.com/pyenv/pyenv#installation).
+- Install different versions of python (recommended versions are `3.6.12`, `3.7.9`, `3.8.5`)
+- Activate them with a command `pyenv shell 3.6.12 3.7.9 3.8.5`
+- Install `tox` by invoking a command `pip install tox`
+- Change a current directory to `${DUCKTESTS_DIR}` and invoke `tox`
diff --git a/modules/ducktests/tests/checks/utils/check_cluster.py b/modules/ducktests/tests/checks/utils/check_cluster.py
new file mode 100644
index 0000000000000..9b8dac32c1383
--- /dev/null
+++ b/modules/ducktests/tests/checks/utils/check_cluster.py
@@ -0,0 +1,106 @@
+# 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.
+
+"""
+Checks custom cluster metadata decorator.
+"""
+
+from unittest.mock import Mock
+
+import pytest
+from ducktape.cluster.cluster_spec import ClusterSpec, LINUX
+from ducktape.mark.mark_expander import MarkedFunctionExpander
+
+from ignitetest.utils._mark import cluster, ParametrizableClusterMetadata, CLUSTER_SIZE_KEYWORD, CLUSTER_SPEC_KEYWORD
+
+
+def expand_function(*, func, sess_ctx):
+ """
+ Inject parameters into function and generate context list.
+ """
+ assert hasattr(func, "marks")
+ assert next(filter(lambda x: isinstance(x, ParametrizableClusterMetadata), func.marks), None)
+
+ return MarkedFunctionExpander(session_context=sess_ctx, function=func).expand()
+
+
+def mock_session_ctx(*, cluster_size=None):
+ """
+ Create mock of session context.
+ """
+ sess_ctx = Mock()
+ sess_ctx.globals = {"cluster_size": cluster_size} if cluster_size is not None else {}
+
+ return sess_ctx
+
+
+# pylint: disable=no-self-use
+class CheckClusterParametrization:
+ """
+ Checks custom @cluster parametrization.
+ """
+ def check_num_nodes(self):
+ """"
+ Check num_nodes.
+ """
+ @cluster(num_nodes=10)
+ def function():
+ return 0
+
+ test_context_list = expand_function(func=function, sess_ctx=mock_session_ctx())
+ assert len(test_context_list) == 1
+ assert test_context_list[0].cluster_use_metadata[CLUSTER_SIZE_KEYWORD] == 10
+
+ test_context_list = expand_function(func=function,
+ sess_ctx=mock_session_ctx(cluster_size="100"))
+ assert len(test_context_list) == 1
+ assert test_context_list[0].cluster_use_metadata[CLUSTER_SIZE_KEYWORD] == 100
+
+ def check_cluster_spec(self):
+ """"
+ Check cluster_spec.
+ """
+ @cluster(cluster_spec=ClusterSpec.simple_linux(10))
+ def function():
+ return 0
+
+ test_context_list = expand_function(func=function, sess_ctx=mock_session_ctx())
+ assert len(test_context_list) == 1
+ inserted_spec = test_context_list[0].cluster_use_metadata[CLUSTER_SPEC_KEYWORD]
+
+ assert inserted_spec.size() == 10
+ for node in inserted_spec.nodes:
+ assert node.operating_system == LINUX
+
+ test_context_list = expand_function(func=function,
+ sess_ctx=mock_session_ctx(cluster_size="100"))
+ assert len(test_context_list) == 1
+ inserted_spec = test_context_list[0].cluster_use_metadata[CLUSTER_SPEC_KEYWORD]
+
+ assert inserted_spec.size() == 100
+ for node in inserted_spec.nodes:
+ assert node.operating_system == LINUX
+
+ def check_invalid_global_param(self):
+ """Check handle of invalid params."""
+ @cluster(num_nodes=10)
+ def function():
+ return 0
+
+ invalid_vals = ["abc", "-10", "1.5", "0", 1.6, -7, 0]
+
+ for val in invalid_vals:
+ with pytest.raises(Exception):
+ expand_function(func=function, sess_ctx=mock_session_ctx(cluster_size=val))
diff --git a/modules/ducktests/tests/checks/utils/check_enum_constructible.py b/modules/ducktests/tests/checks/utils/check_enum_constructible.py
new file mode 100644
index 0000000000000..3c6781ec0319b
--- /dev/null
+++ b/modules/ducktests/tests/checks/utils/check_enum_constructible.py
@@ -0,0 +1,72 @@
+# 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.
+
+"""
+Checks IntEnum enhancement.
+"""
+
+from enum import IntEnum
+
+import pytest
+from ignitetest.utils.enum import constructible
+
+
+@constructible
+class ConnectType(IntEnum):
+ """
+ Example of IntEnum.
+ """
+ UDP = 0
+ TCP = 1
+ HTTP = 2
+
+
+check_params = []
+for name, value in ConnectType.__members__.items():
+ check_params.append([name, value])
+ check_params.append([int(value), value])
+ check_params.append([value, value])
+
+
+# pylint: disable=no-self-use, no-member
+class CheckEnumConstructible:
+ """
+ Basic test of IntEnum decorated with @constructible.
+ """
+ @pytest.mark.parametrize(
+ ['input_value', 'expected_value'],
+ check_params
+ )
+ def check_construct_from(self, input_value, expected_value):
+ """Basic checks."""
+ with ConnectType.construct_from(input_value) as conn_type:
+ assert conn_type is expected_value
+
+ @pytest.mark.parametrize(
+ ['input_value'],
+ [[val] for val in [-1, .6, 'test']]
+ )
+ def check_invalid_input(self, input_value):
+ """Check invalid input."""
+ with pytest.raises(Exception):
+ ConnectType.construct_from(input_value)
+
+ def check_invalid_usage(self):
+ """Check invalid type decoration."""
+ with pytest.raises(AssertionError):
+ class SimpleClass:
+ """Cannot be decorated"""
+
+ constructible(SimpleClass)
diff --git a/modules/ducktests/tests/checks/utils/check_parametrized.py b/modules/ducktests/tests/checks/utils/check_parametrized.py
new file mode 100644
index 0000000000000..5eddb3f07cb93
--- /dev/null
+++ b/modules/ducktests/tests/checks/utils/check_parametrized.py
@@ -0,0 +1,238 @@
+# 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.
+
+"""
+Checks custom parametrizers.
+"""
+
+import itertools
+from unittest.mock import Mock
+
+import pytest
+from ducktape.mark import parametrized, parametrize, matrix, ignore
+from ducktape.mark.mark_expander import MarkedFunctionExpander
+
+from ignitetest.utils._mark import IgniteVersionParametrize, ignite_versions, version_if
+from ignitetest.utils.version import IgniteVersion, V_2_8_0, V_2_8_1, V_2_7_6, DEV_BRANCH
+
+
+def expand_function(*, func, sess_ctx):
+ """
+ Inject parameters into function and generate context list.
+ """
+ assert parametrized(func)
+ assert next(filter(lambda x: isinstance(x, IgniteVersionParametrize), func.marks), None)
+
+ return MarkedFunctionExpander(session_context=sess_ctx, function=func).expand()
+
+
+def mock_session_ctx(*, global_args=None):
+ """
+ Create mock of session context.
+ """
+ sess_ctx = Mock()
+ sess_ctx.globals = global_args if global_args else {}
+
+ return sess_ctx
+
+
+class CheckIgniteVersions:
+ """
+ Checks @ignite_version parametrization.
+ """
+ single_params = itertools.product(
+ [[str(V_2_8_1)], [str(V_2_8_1), str(DEV_BRANCH)]],
+ [{}, {'ignite_versions': 'dev'}, {'ignite_versions': ['2.8.1', 'dev']}]
+ )
+
+ @pytest.mark.parametrize(
+ ['versions', 'global_args'],
+ map(lambda x: pytest.param(x[0], x[1]), single_params)
+ )
+ def check_injection(self, versions, global_args):
+ """
+ Checks parametrization with single version.
+ """
+ @ignite_versions(*versions, version_prefix='ver')
+ def function(ver):
+ return IgniteVersion(ver)
+
+ context_list = expand_function(func=function, sess_ctx=mock_session_ctx(global_args=global_args))
+
+ self._check_injection(context_list, versions=versions, global_args=global_args)
+
+ pair_params = itertools.product(
+ [[(str(DEV_BRANCH), str(V_2_8_1))], [(str(DEV_BRANCH), str(V_2_8_0)), (str(DEV_BRANCH), str(V_2_8_1))]],
+ [{}, {'ignite_versions': [['2.8.1', '2.7.6'], ['2.8.1', '2.8.0']]}, {'ignite_versions': [['dev', '2.8.1']]}]
+ )
+
+ @pytest.mark.parametrize(
+ ['versions', 'global_args'],
+ map(lambda x: pytest.param(x[0], x[1]), pair_params)
+ )
+ def check_injection_pairs(self, versions, global_args):
+ """
+ Checks parametrization with pair of versions.
+ """
+ @ignite_versions(*versions, version_prefix='pair')
+ def function(pair_1, pair_2):
+ return IgniteVersion(pair_1), IgniteVersion(pair_2)
+
+ context_list = expand_function(func=function, sess_ctx=mock_session_ctx(global_args=global_args))
+
+ self._check_injection(context_list, versions=versions, global_args=global_args, pairs=True)
+
+ @pytest.mark.parametrize(
+ ['versions', 'version_prefix', 'global_args'],
+ [
+ pytest.param([(DEV_BRANCH, V_2_8_1)], 'ver', {}),
+ pytest.param([DEV_BRANCH], 'ver', {'ignite_versions': [['dev', '2.8.1']]}),
+ pytest.param([DEV_BRANCH], 'invalid_prefix', {})
+ ]
+ )
+ def check_injection_fail(self, versions, version_prefix, global_args):
+ """
+ Check incorrect injecting variables with single parameter.
+ """
+ @ignite_versions(*versions, version_prefix=version_prefix)
+ def function(ver):
+ return IgniteVersion(ver)
+
+ with pytest.raises(Exception):
+ context_list = expand_function(func=function, sess_ctx=mock_session_ctx(global_args=global_args))
+
+ self._check_injection(context_list, versions=versions, global_args=global_args)
+
+ @pytest.mark.parametrize(
+ ['versions', 'version_prefix', 'global_args'],
+ [
+ pytest.param([DEV_BRANCH, V_2_8_1], 'pair', {}),
+ pytest.param([(DEV_BRANCH, V_2_8_1)], 'pair', {'ignite_versions': 'dev'}),
+ pytest.param([(DEV_BRANCH, V_2_8_1)], 'pair', {'ignite_versions': ['dev', '2.8.1']}),
+ pytest.param([(DEV_BRANCH, V_2_8_1)], 'invalid_prefix', {})
+ ]
+ )
+ def check_injection_pairs_fail(self, versions, version_prefix, global_args):
+ """
+ Check incorrect injecting with pairs of versions.
+ """
+ @ignite_versions(*versions, version_prefix=version_prefix)
+ def function(pair_1, pair_2):
+ return IgniteVersion(pair_1), IgniteVersion(pair_2)
+
+ with pytest.raises(Exception):
+ context_list = expand_function(func=function, sess_ctx=mock_session_ctx(global_args=global_args))
+
+ self._check_injection(context_list, versions=versions, global_args=global_args, pairs=True)
+
+ def check_with_others_marks(self): # pylint: disable=R0201
+ """
+ Checks that ignite version parametrization works with others correctly.
+ """
+ @ignite_versions(str(DEV_BRANCH), str(V_2_8_1), version_prefix='ver')
+ @parametrize(x=10, y=20)
+ @parametrize(x=30, y=40)
+ def function_parametrize(ver, x, y):
+ return ver, x, y
+
+ @ignite_versions((str(DEV_BRANCH), str(V_2_8_1)), (str(V_2_8_1), str(V_2_7_6)), version_prefix='pair')
+ @matrix(i=[10, 20], j=[30, 40])
+ def function_matrix(pair_1, pair_2, i, j):
+ return pair_1, pair_2, i, j
+
+ @ignore(ver=str(DEV_BRANCH))
+ @ignite_versions(str(DEV_BRANCH), str(V_2_8_1), version_prefix='ver')
+ def function_ignore(ver):
+ return ver
+
+ context_list = expand_function(func=function_parametrize, sess_ctx=mock_session_ctx())
+ context_list += expand_function(func=function_matrix, sess_ctx=mock_session_ctx())
+ context_list += expand_function(func=function_ignore, sess_ctx=mock_session_ctx())
+
+ assert len(context_list) == 14
+
+ parametrized_context = list(filter(lambda x: x.function_name == function_parametrize.__name__, context_list))
+ assert len(parametrized_context) == 4
+ for ctx in parametrized_context:
+ args = ctx.injected_args
+ assert len(args) == 3
+ assert ctx.function() == (args['ver'], args['x'], args['y'])
+
+ matrix_context = list(filter(lambda x: x.function_name == function_matrix.__name__, context_list))
+ assert len(matrix_context) == 8
+ for ctx in matrix_context:
+ args = ctx.injected_args
+ assert len(args) == 4
+ assert ctx.function() == (args['pair_1'], args['pair_2'], args['i'], args['j'])
+
+ assert len(list(filter(lambda x: x.function_name == function_ignore.__name__, context_list))) == 2
+ assert len(list(filter(lambda x: x.ignore, context_list))) == 1
+
+ @staticmethod
+ def _check_injection(context_list, *, versions, global_args=None, pairs=False):
+ if global_args:
+ global_versions = global_args['ignite_versions']
+
+ if isinstance(global_versions, str):
+ check_versions = [IgniteVersion(global_versions)]
+ elif isinstance(global_args['ignite_versions'], tuple):
+ check_versions = [tuple(map(IgniteVersion, global_versions))]
+ elif pairs:
+ check_versions = list(map(lambda x: (IgniteVersion(x[0]), IgniteVersion(x[1])), global_versions))
+ else:
+ check_versions = list(map(IgniteVersion, global_versions))
+ else:
+ if not pairs:
+ check_versions = list(map(IgniteVersion, versions))
+ else:
+ check_versions = list(map(lambda x: (IgniteVersion(x[0]), IgniteVersion(x[1])), versions))
+
+ assert len(context_list) == len(check_versions)
+
+ for i, ctx in enumerate(sorted(context_list, key=lambda x: x.function())):
+ assert ctx.function() == check_versions[i]
+
+
+class CheckVersionIf:
+ """
+ Checks @version_if parametrization.
+ """
+ def check_common(self): # pylint: disable=R0201
+ """
+ Check common scenarios with @ignite_versions parametrization.
+ """
+ @version_if(lambda ver: ver != V_2_8_0, variable_name='ver')
+ @ignite_versions(str(DEV_BRANCH), str(V_2_8_0), version_prefix='ver')
+ def function_1(ver):
+ return IgniteVersion(ver)
+
+ @version_if(lambda ver: ver > V_2_7_6, variable_name='ver_1')
+ @version_if(lambda ver: ver < V_2_8_0, variable_name='ver_2')
+ @ignite_versions((str(V_2_8_1), str(V_2_8_0)), (str(V_2_8_0), str(V_2_7_6)), version_prefix='ver')
+ def function_2(ver_1, ver_2):
+ return IgniteVersion(ver_1), IgniteVersion(ver_2)
+
+ @ignite_versions(str(DEV_BRANCH), str(V_2_8_0))
+ def function_3(ignite_version):
+ return IgniteVersion(ignite_version)
+
+ context_list = expand_function(func=function_1, sess_ctx=mock_session_ctx())
+ context_list += expand_function(func=function_2, sess_ctx=mock_session_ctx())
+ context_list += expand_function(func=function_3, sess_ctx=mock_session_ctx())
+
+ assert len(context_list) == 6
+
+ assert next(filter(lambda x: x.injected_args['ver'] == str(V_2_8_0), context_list)).ignore
+ assert not next(filter(lambda x: x.injected_args['ver'] == str(DEV_BRANCH), context_list)).ignore
diff --git a/modules/ducktests/tests/docker/Dockerfile b/modules/ducktests/tests/docker/Dockerfile
new file mode 100644
index 0000000000000..89cc3b11a93f8
--- /dev/null
+++ b/modules/ducktests/tests/docker/Dockerfile
@@ -0,0 +1,100 @@
+# 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.
+
+ARG jdk_version=openjdk:8
+FROM $jdk_version
+
+MAINTAINER Apache Ignite dev@ignite.apache.org
+VOLUME ["/opt/ignite-dev"]
+
+# Set the timezone.
+ENV TZ=Europe/Moscow
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+# Do not ask for confirmations when running apt-get, etc.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Set the ducker.creator label so that we know that this is a ducker image. This will make it
+# visible to 'ducker purge'. The ducker.creator label also lets us know what UNIX user built this
+# image.
+ARG ducker_creator=default
+LABEL ducker.creator=$ducker_creator
+
+# Update Linux and install necessary utilities.
+RUN cat /etc/apt/sources.list | sed 's/http:\/\/deb.debian.org/https:\/\/deb.debian.org/g' > /etc/apt/sources.list.2 && mv /etc/apt/sources.list.2 /etc/apt/sources.list
+RUN apt update && apt install -y sudo netcat iptables rsync unzip wget curl jq coreutils openssh-server net-tools vim python3-pip python3-dev libffi-dev libssl-dev cmake pkg-config libfuse-dev iperf traceroute mc && apt-get -y clean
+RUN python3 -m pip install -U pip==20.2.2;
+COPY ./requirements.txt /root/requirements.txt
+RUN pip3 install -r /root/requirements.txt
+
+# Set up ssh
+COPY ./ssh-config /root/.ssh/config
+# NOTE: The paramiko library supports the PEM-format private key, but does not support the RFC4716 format.
+RUN ssh-keygen -m PEM -q -t rsa -N '' -f /root/.ssh/id_rsa && cp -f /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys
+RUN echo 'PermitUserEnvironment yes' >> /etc/ssh/sshd_config
+
+ARG APACHE_MIRROR="https://apache-mirror.rbc.ru/pub/apache/"
+ARG APACHE_ARCHIVE="https://archive.apache.org/dist/"
+
+# Install binary test dependencies.
+RUN for v in "2.7.6" "2.8.0" "2.8.1" "2.9.0"; \
+ do cd /opt; \
+ curl -O $APACHE_ARCHIVE/ignite/$v/apache-ignite-$v-bin.zip;\
+ unzip apache-ignite-$v-bin.zip && mv /opt/apache-ignite-$v-bin /opt/ignite-$v;\
+ done
+
+RUN rm /opt/apache-ignite-*-bin.zip
+
+#Install zookeeper.
+ARG ZOOKEEPER_VERSION="3.5.8"
+ARG ZOOKEEPER_NAME="zookeeper-$ZOOKEEPER_VERSION"
+ARG ZOOKEEPER_RELEASE_NAME="apache-$ZOOKEEPER_NAME-bin"
+ARG ZOOKEEPER_RELEASE_ARTIFACT="$ZOOKEEPER_RELEASE_NAME.tar.gz"
+RUN echo $APACHE_ARCHIVE/zookeeper/$ZOOKEEPER_NAME/$ZOOKEEPER_RELEASE_ARTIFACT
+RUN cd /opt && curl -O $APACHE_ARCHIVE/zookeeper/$ZOOKEEPER_NAME/$ZOOKEEPER_RELEASE_ARTIFACT \
+ && tar xvf $ZOOKEEPER_RELEASE_ARTIFACT && rm $ZOOKEEPER_RELEASE_ARTIFACT
+RUN mv /opt/$ZOOKEEPER_RELEASE_NAME /opt/$ZOOKEEPER_NAME
+
+# Install spark
+ARG SPARK_VERSION="2.3.4"
+ARG SPARK_NAME="spark-$SPARK_VERSION"
+ARG SPARK_RELEASE_NAME="spark-$SPARK_VERSION-bin-hadoop2.7"
+
+RUN cd /opt && curl -O $APACHE_ARCHIVE/spark/$SPARK_NAME/$SPARK_RELEASE_NAME.tgz && tar xvf $SPARK_RELEASE_NAME.tgz && rm $SPARK_RELEASE_NAME.tgz
+RUN mv /opt/$SPARK_RELEASE_NAME /opt/$SPARK_NAME
+
+# The version of Kibosh to use for testing.
+# If you update this, also update vagrant/base.sh
+ARG KIBOSH_VERSION="8841dd392e6fbf02986e2fb1f1ebf04df344b65a"
+
+# Install Kibosh
+RUN apt-get install fuse
+RUN cd /opt && git clone -q https://github.com/confluentinc/kibosh.git && cd "/opt/kibosh" && git reset --hard $KIBOSH_VERSION && mkdir "/opt/kibosh/build" && cd "/opt/kibosh/build" && ../configure && make -j 2
+
+#Install jmxterm
+ARG JMXTERM_NAME="jmxterm"
+ARG JMXTERM_VERSION="1.0.1"
+ARG JMXTERM_ARTIFACT="$JMXTERM_NAME-$JMXTERM_VERSION-uber.jar"
+RUN cd /opt && curl -OL https://github.com/jiaqi/jmxterm/releases/download/v$JMXTERM_VERSION/$JMXTERM_ARTIFACT \
+ && mv $JMXTERM_ARTIFACT $JMXTERM_NAME.jar
+
+# Set up the ducker user.
+RUN useradd -ms /bin/bash ducker && mkdir -p /home/ducker/ && rsync -aiq /root/.ssh/ /home/ducker/.ssh && chown -R ducker /home/ducker/ /mnt/ /var/log/ && echo "PATH=$(runuser -l ducker -c 'echo $PATH'):$JAVA_HOME/bin" >> /home/ducker/.ssh/environment && echo 'PATH=$PATH:'"$JAVA_HOME/bin" >> /home/ducker/.profile && echo 'ducker ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
+USER ducker
+
+CMD sudo service ssh start && tail -f /dev/null
+
+# Container port exposure
+EXPOSE 11211 47100 47500 49112 10800 8080 2888 3888 2181
diff --git a/modules/ducktests/tests/docker/clean_up.sh b/modules/ducktests/tests/docker/clean_up.sh
new file mode 100755
index 0000000000000..a5efe2d12e38b
--- /dev/null
+++ b/modules/ducktests/tests/docker/clean_up.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env 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.
+
+bash ./ducker-ignite down
+
diff --git a/modules/ducktests/tests/docker/ducker-ignite b/modules/ducktests/tests/docker/ducker-ignite
new file mode 100755
index 0000000000000..9a92247a58e13
--- /dev/null
+++ b/modules/ducktests/tests/docker/ducker-ignite
@@ -0,0 +1,648 @@
+#!/usr/bin/env 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.
+
+#
+# Ducker-Ignite: a tool for running Apache Ignite system tests inside Docker images.
+#
+# Note: this should be compatible with the version of bash that ships on most
+# Macs, bash 3.2.57.
+#
+
+script_path="${0}"
+
+# The absolute path to the directory which this script is in. This will also be the directory
+# which we run docker build from.
+ducker_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# The absolute path to the root Ignite directory
+ignite_dir="$( cd "${ducker_dir}/../../../.." && pwd )"
+
+# The memory consumption to allow during the docker build.
+# This does not include swap.
+docker_build_memory_limit="8000m"
+
+# The maximum mmemory consumption to allow in containers.
+docker_run_memory_limit="8000m"
+
+# The default number of cluster nodes to bring up if a number is not specified.
+default_num_nodes=4
+
+# The default OpenJDK base image.
+default_jdk="openjdk:8"
+
+# The default ducker-ignite image name.
+default_image_name="ducker-ignite"
+
+# Display a usage message on the terminal and exit.
+#
+# $1: The exit status to use
+usage() {
+ local exit_status="${1}"
+ cat < /dev/null || die "You must install ${cmd} to run this script."
+ done
+}
+
+# Set a global variable to a value.
+#
+# $1: The variable name to set. This function will die if the variable already has a value. The
+# variable will be made readonly to prevent any future modifications.
+# $2: The value to set the variable to. This function will die if the value is empty or starts
+# with a dash.
+# $3: A human-readable description of the variable.
+set_once() {
+ local key="${1}"
+ local value="${2}"
+ local what="${3}"
+ [[ -n "${!key}" ]] && die "Error: more than one value specified for ${what}."
+ verify_command_line_argument "${value}" "${what}"
+ # It would be better to use declare -g, but older bash versions don't support it.
+ export ${key}="${value}"
+}
+
+# Verify that a command-line argument is present and does not start with a slash.
+#
+# $1: The command-line argument to verify.
+# $2: A human-readable description of the variable.
+verify_command_line_argument() {
+ local value="${1}"
+ local what="${2}"
+ [[ -n "${value}" ]] || die "Error: no value specified for ${what}"
+ [[ ${value} == -* ]] && die "Error: invalid value ${value} specified for ${what}"
+}
+
+# Echo a message if a flag is set.
+#
+# $1: If this is 1, the message will be echoed.
+# $@: The message
+maybe_echo() {
+ local verbose="${1}"
+ shift
+ [[ "${verbose}" -eq 1 ]] && echo "${@}"
+}
+
+# Counts the number of elements passed to this subroutine.
+count() {
+ echo $#
+}
+
+# Push a new directory on to the bash directory stack, or exit with a failure message.
+#
+# $1: The directory push on to the directory stack.
+must_pushd() {
+ local target_dir="${1}"
+ pushd -- "${target_dir}" &> /dev/null || die "failed to change directory to ${target_dir}"
+}
+
+# Pop a directory from the bash directory stack, or exit with a failure message.
+must_popd() {
+ popd &> /dev/null || die "failed to popd"
+}
+
+# Run a command and die if it fails.
+#
+# Optional flags:
+# -v: print the command before running it.
+# -o: display the command output.
+# $@: The command to run.
+must_do() {
+ local verbose=0
+ local output="/dev/null"
+ while true; do
+ case ${1} in
+ -v) verbose=1; shift;;
+ -o) output="/dev/stdout"; shift;;
+ *) break;;
+ esac
+ done
+
+ [[ "${verbose}" -eq 1 ]] && echo "${@}"
+ eval "${@}" >${output} || die "${1} failed"
+}
+
+# Ask the user a yes/no question.
+#
+# $1: The prompt to use
+# $_return: 0 if the user answered no; 1 if the user answered yes.
+ask_yes_no() {
+ local prompt="${1}"
+ while true; do
+ read -r -p "${prompt} " response
+ case "${response}" in
+ [yY]|[yY][eE][sS]) _return=1; return;;
+ [nN]|[nN][oO]) _return=0; return;;
+ *);;
+ esac
+ echo "Please respond 'yes' or 'no'."
+ echo
+ done
+}
+
+# Build a docker image.
+#
+# $1: The docker build context
+# $2: The name of the image to build.
+ducker_build_image() {
+ local docker_context="${1}"
+ local image_name="${2}"
+
+ # Use SECONDS, a builtin bash variable that gets incremented each second, to measure the docker
+ # build duration.
+ SECONDS=0
+
+ must_pushd "${ducker_dir}"
+ # Tip: if you are scratching your head for some dependency problems that are referring to an old code version
+ # (for example java.lang.NoClassDefFoundError), add --no-cache flag to the build shall give you a clean start.
+ must_do -v -o docker build --memory="${docker_build_memory_limit}" \
+ --build-arg "ducker_creator=${user_name}" --build-arg "jdk_version=${jdk_version}" -t "${image_name}" \
+ -f "${docker_context}/Dockerfile" -- "${docker_context}"
+ docker_status=$?
+ must_popd
+ duration="${SECONDS}"
+ if [[ ${docker_status} -ne 0 ]]; then
+ die "** ERROR: Failed to build ${what} image after $((duration / 60))m $((duration % 60))s."
+ fi
+
+ # Save docker image id to the file. Then could use this file to find version of docker image built last time.
+ # It could be useful if we don't confident about necessity of stoping the cluster.
+ get_image_id "${image_name}" > "${ducker_dir}/build/image_${image_name}.build"
+
+ echo "** Successfully built ${what} image in $((duration / 60))m $((duration % 60))s."
+}
+
+ducker_build() {
+ require_commands docker
+
+ local docker_context=
+ while [[ $# -ge 1 ]]; do
+ case "${1}" in
+ -j|--jdk) set_once jdk_version "${2}" "the OpenJDK base image"; shift 2;;
+ -c|--context) docker_context="${2}"; shift 2;;
+ *) set_once image_name "${1}" "docker image name"; shift;;
+ esac
+ done
+
+ [[ -n "${jdk_version}" ]] || jdk_version="${default_jdk}"
+ [[ -n "${image_name}" ]] || image_name="${default_image_name}-${jdk_version/:/-}"
+ [[ -n "${docker_context}" ]] || docker_context="${ducker_dir}"
+
+ ducker_build_image "${docker_context}" "${image_name}"
+}
+
+docker_run() {
+ local node=${1}
+ local image_name=${2}
+ local ports_option=${3}
+
+ local expose_ports=""
+ if [[ -n ${ports_option} ]]; then
+ expose_ports="-P"
+ for expose_port in ${ports_option//,/ }; do
+ expose_ports="${expose_ports} --expose ${expose_port}"
+ done
+ fi
+
+ # Invoke docker-run. We need privileged mode to be able to run iptables
+ # and mount FUSE filesystems inside the container. We also need it to
+ # run iptables inside the container.
+ must_do -v docker run --privileged \
+ -d -t -h "${node}" --network ducknet ${expose_ports} \
+ --memory=${docker_run_memory_limit} --memory-swappiness=1 \
+ --mount type=bind,source="${ignite_dir}",target=/opt/ignite-dev,consistency=delegated --name "${node}" -- "${image_name}"
+}
+
+setup_custom_ducktape() {
+ local custom_ducktape="${1}"
+ local image_name="${2}"
+
+ [[ -f "${custom_ducktape}/ducktape/__init__.py" ]] || \
+ die "You must supply a valid ducktape directory to --custom-ducktape"
+ docker_run ducker01 "${image_name}"
+ local running_container
+ running_container=$(docker ps -f=network=ducknet -q)
+ must_do -v -o docker cp "${custom_ducktape}" "${running_container}:/opt/ducktape"
+ docker exec --user=root ducker01 bash -c 'set -x && cd /opt/ignite-dev/modules/ducktests/tests && sudo python ./setup.py develop install && cd /opt/ducktape && sudo python ./setup.py develop install'
+ [[ $? -ne 0 ]] && die "failed to install the new ducktape."
+ must_do -v -o docker commit ducker01 "${image_name}"
+ must_do -v docker kill "${running_container}"
+ must_do -v docker rm ducker01
+}
+
+ducker_up() {
+ require_commands docker
+ while [[ $# -ge 1 ]]; do
+ case "${1}" in
+ -C|--custom-ducktape) set_once custom_ducktape "${2}" "the custom ducktape directory"; shift 2;;
+ -f|--force) force=1; shift;;
+ -n|--num-nodes) set_once num_nodes "${2}" "number of nodes"; shift 2;;
+ -e|--expose-ports) set_once expose_ports "${2}" "the ports to expose"; shift 2;;
+ *) set_once image_name "${1}" "docker image name"; shift;;
+ esac
+ done
+ [[ -n "${num_nodes}" ]] || num_nodes="${default_num_nodes}"
+ [[ -n "${image_name}" ]] || image_name="${default_image_name}-${default_jdk/:/-}"
+ [[ "${num_nodes}" =~ ^-?[0-9]+$ ]] || \
+ die "ducker_up: the number of nodes must be an integer."
+ [[ "${num_nodes}" -gt 0 ]] || die "ducker_up: the number of nodes must be greater than 0."
+ if [[ "${num_nodes}" -lt 2 ]]; then
+ if [[ "${force}" -ne 1 ]]; then
+ echo "ducker_up: It is recommended to run at least 2 nodes, since ducker01 is only \
+used to run ducktape itself. If you want to do it anyway, you can use --force to attempt to \
+use only ${num_nodes}."
+ exit 1
+ fi
+ fi
+
+ docker ps >/dev/null || die "ducker_up: failed to run docker. Please check that the daemon is started."
+
+ local running_containers="$(docker ps -f=network=ducknet -q)"
+ local num_running_containers=$(count ${running_containers})
+ if [[ ${num_running_containers} -gt 0 ]]; then
+ die "ducker_up: there are ${num_running_containers} ducker containers \
+running already. Use ducker down to bring down these containers before \
+attempting to start new ones."
+ fi
+
+ echo "ducker_up: Bringing up ${image_name} with ${num_nodes} nodes..."
+ docker image inspect "${image_name}" &>/dev/null || \
+ must_do -v -o docker pull "${image_name}"
+
+ if docker network inspect ducknet &>/dev/null; then
+ must_do -v docker network rm ducknet
+ fi
+ must_do -v docker network create ducknet
+ if [[ -n "${custom_ducktape}" ]]; then
+ setup_custom_ducktape "${custom_ducktape}" "${image_name}"
+ fi
+ for n in $(seq -f %02g 1 ${num_nodes}); do
+ local node="ducker${n}"
+ docker_run "${node}" "${image_name}" "${expose_ports}"
+ done
+ mkdir -p "${ducker_dir}/build"
+ exec 3<> "${ducker_dir}/build/node_hosts"
+ for n in $(seq -f %02g 1 ${num_nodes}); do
+ local node="ducker${n}"
+ docker exec --user=root "${node}" grep "${node}" /etc/hosts >&3
+ [[ $? -ne 0 ]] && die "failed to find the /etc/hosts entry for ${node}"
+ done
+ exec 3>&-
+ for n in $(seq -f %02g 1 ${num_nodes}); do
+ local node="ducker${n}"
+ docker exec --user=root "${node}" \
+ bash -c "grep -v ${node} /opt/ignite-dev/modules/ducktests/tests/docker/build/node_hosts >> /etc/hosts"
+ [[ $? -ne 0 ]] && die "failed to append to the /etc/hosts file on ${node}"
+ done
+
+ echo "ducker_up: added the latest entries to /etc/hosts on each node."
+ generate_cluster_json_file "${num_nodes}" "${ducker_dir}/build/cluster.json"
+ echo "ducker_up: successfully wrote ${ducker_dir}/build/cluster.json"
+
+ # Save docker image id to the file. Then could use this file to find version of docker image that is running.
+ # It could be useful if we don't confident about necessity of rebuilding image.
+ get_image_id "${image_name}" > "${ducker_dir}/build/image_id.up"
+
+ echo "** ducker_up: successfully brought up ${num_nodes} nodes."
+}
+
+# Generate the cluster.json file used by ducktape to identify cluster nodes.
+#
+# $1: The number of cluster nodes.
+# $2: The path to write the cluster.json file to.
+generate_cluster_json_file() {
+ local num_nodes="${1}"
+ local path="${2}"
+ rm ${path}
+ touch ${path}
+ exec 3<> "${path}"
+cat<&3
+{
+ "_comment": [
+ "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."
+ ],
+ "nodes": [
+EOF
+ for n in $(seq 2 ${num_nodes}); do
+ if [[ ${n} -eq ${num_nodes} ]]; then
+ suffix=""
+ else
+ suffix=","
+ fi
+ local node=$(printf ducker%02d ${n})
+cat<&3
+ {
+ "externally_routable_ip": "${node}",
+ "ssh_config": {
+ "host": "${node}",
+ "hostname": "${node}",
+ "identityfile": "/home/ducker/.ssh/id_rsa",
+ "password": "",
+ "port": 22,
+ "user": "ducker"
+ }
+ }${suffix}
+EOF
+ done
+cat<&3
+ ]
+}
+EOF
+ exec 3>&-
+}
+
+ducker_test() {
+ require_commands docker
+ docker inspect ducker01 &>/dev/null || \
+ die "ducker_test: the ducker01 instance appears to be down. Did you run 'ducker up'?"
+ [[ $# -lt 1 ]] && \
+ die "ducker_test: you must supply at least one system test to run. Type --help for help."
+ local args=""
+ local ignite_test=0
+ for arg in "${@}"; do
+ local regex=".*\/ignitetest\/(.*)"
+ if [[ $arg =~ $regex ]]; then
+ local ignpath=${BASH_REMATCH[1]}
+ args="${args} ./modules/ducktests/tests/ignitetest/${ignpath}"
+ else
+ args="${args} ${arg}"
+ fi
+ done
+ must_pushd "${ignite_dir}"
+ #(test mvn) && mvn package -DskipTests -Dmaven.javadoc.skip=true -Plgpl,-examples,-clean-libs,-release,-scala,-clientDocs
+ must_popd
+ cmd="cd /opt/ignite-dev && ducktape --cluster-file /opt/ignite-dev/modules/ducktests/tests/docker/build/cluster.json $args"
+ echo "docker exec ducker01 bash -c \"${cmd}\""
+ exec docker exec --user=ducker ducker01 bash -c "${cmd}"
+}
+
+ducker_ssh() {
+ require_commands docker
+ [[ $# -eq 0 ]] && die "ducker_ssh: Please specify a container name to log into. \
+Currently active containers: $(echo_running_container_names)"
+ local node_info="${1}"
+ shift
+ local guest_command="$*"
+ local user_name="ducker"
+ if [[ "${node_info}" =~ @ ]]; then
+ user_name="${node_info%%@*}"
+ local node_name="${node_info##*@}"
+ else
+ local node_name="${node_info}"
+ fi
+ local docker_flags=""
+ if [[ -z "${guest_command}" ]]; then
+ local docker_flags="${docker_flags} -t"
+ local guest_command_prefix=""
+ guest_command=bash
+ else
+ local guest_command_prefix="bash -c"
+ fi
+ if [[ "${node_name}" == "all" ]]; then
+ local nodes=$(echo_running_container_names)
+ [[ "${nodes}" == "(none)" ]] && die "ducker_ssh: can't locate any running ducker nodes."
+ for node in ${nodes}; do
+ docker exec --user=${user_name} -i ${docker_flags} "${node}" \
+ ${guest_command_prefix} "${guest_command}" || die "docker exec ${node} failed"
+ done
+ else
+ docker inspect --type=container -- "${node_name}" &>/dev/null || \
+ die "ducker_ssh: can't locate node ${node_name}. Currently running nodes: \
+$(echo_running_container_names)"
+ exec docker exec --user=${user_name} -i ${docker_flags} "${node_name}" \
+ ${guest_command_prefix} "${guest_command}"
+ fi
+}
+
+# Echo all the running Ducker container names, or (none) if there are no running Ducker containers.
+echo_running_container_names() {
+ node_names="$(docker ps -f=network=ducknet -q --format '{{.Names}}' | sort)"
+ if [[ -z "${node_names}" ]]; then
+ echo "(none)"
+ else
+ echo ${node_names//$'\n'/ }
+ fi
+}
+
+ducker_down() {
+ require_commands docker
+ local verbose=1
+ local force_str=""
+ while [[ $# -ge 1 ]]; do
+ case "${1}" in
+ -q|--quiet) verbose=0; shift;;
+ -f|--force) force_str="-f"; shift;;
+ *) die "ducker_down: unexpected command-line argument ${1}";;
+ esac
+ done
+ local running_containers
+ running_containers="$(docker ps -f=network=ducknet -q)"
+ [[ $? -eq 0 ]] || die "ducker_down: docker command failed. Is the docker daemon running?"
+ running_containers=${running_containers//$'\n'/ }
+ local all_containers
+ all_containers=$(docker ps -a -f=network=ducknet -q)
+ all_containers=${all_containers//$'\n'/ }
+ if [[ -z "${all_containers}" ]]; then
+ maybe_echo "${verbose}" "No ducker containers found."
+ return
+ fi
+ verbose_flag=""
+ if [[ ${verbose} == 1 ]]; then
+ verbose_flag="-v"
+ fi
+ if [[ -n "${running_containers}" ]]; then
+ must_do ${verbose_flag} docker kill "${running_containers[@]}"
+ fi
+ must_do ${verbose_flag} docker rm ${force_str} "${all_containers}"
+ must_do ${verbose_flag} -o rm -f -- "${ducker_dir}/build/node_hosts" "${ducker_dir}/build/cluster.json"
+ if docker network inspect ducknet &>/dev/null; then
+ must_do -v docker network rm ducknet
+ fi
+ rm "${ducker_dir}/build/image_id.up"
+ maybe_echo "${verbose}" "ducker_down: removed $(count ${all_containers}) containers."
+}
+
+ducker_purge() {
+ require_commands docker
+ local force_str=""
+ while [[ $# -ge 1 ]]; do
+ case "${1}" in
+ -f|--force) force_str="-f"; shift;;
+ *) die "ducker_purge: unknown argument ${1}";;
+ esac
+ done
+ echo "** ducker_purge: attempting to locate ducker images to purge"
+ local images
+ images=$(docker images -q -a -f label=ducker.creator)
+ [[ $? -ne 0 ]] && die "docker images command failed"
+ images=${images//$'\n'/ }
+ declare -a purge_images=()
+ if [[ -z "${images}" ]]; then
+ echo "** ducker_purge: no images found to purge."
+ exit 0
+ fi
+ echo "** ducker_purge: images to delete:"
+ for image in ${images}; do
+ echo -n "${image} "
+ docker inspect --format='{{.Config.Labels}} {{.Created}}' --type=image "${image}"
+ [[ $? -ne 0 ]] && die "docker inspect ${image} failed"
+ done
+ ask_yes_no "Delete these docker images? [y/n]"
+ [[ "${_return}" -eq 0 ]] && exit 0
+ must_do -v -o docker rmi ${force_str} ${images}
+}
+
+get_image_id() {
+ require_commands docker
+ local image_name="${1}"
+
+ must_do -o docker image inspect --format "{{.Id}}" "${image_name}"
+}
+
+ducker_compare() {
+ local cmd=""
+
+ local verbose=1
+ local force_str=""
+
+ while [[ $# -ge 1 ]]; do
+ case "${1}" in
+ -q|--quiet) verbose=0; cmd="${cmd} ${1}"; shift;;
+ -f|--force) force_str="-f"; cmd="${cmd} ${1}"; shift;;
+ *) set_once image_name "${1}" "docker image name"; shift;;
+ esac
+ done
+
+ [ -n "${image_name}" ] || image_name="${default_image_name}-${default_jdk/:/-}"
+
+ cmp -s "${ducker_dir}/build/image_${image_name}.build" "${ducker_dir}/build/image_id.up"
+ local ret="$?"
+
+ if [[ $ret != "0" ]]; then
+ echo "Docker image ${image_name} is outdated. Stop the cluster"
+ ducker_down ${cmd}
+ fi
+}
+
+# Parse command-line arguments
+[[ $# -lt 1 ]] && usage 0
+# Display the help text if -h or --help appears in the command line
+for arg in ${@}; do
+ case "${arg}" in
+ -h|--help) usage 0;;
+ --) break;;
+ *);;
+ esac
+done
+action="${1}"
+shift
+case "${action}" in
+ help) usage 0;;
+
+ build|up|test|ssh|down|purge|compare)
+ ducker_${action} "${@}"; exit 0;;
+
+ *) echo "Unknown command '${action}'. Type '${script_path} --help' for usage information."
+ exit 1;;
+esac
diff --git a/modules/ducktests/tests/docker/requirements-dev.txt b/modules/ducktests/tests/docker/requirements-dev.txt
new file mode 100644
index 0000000000000..c92e672a53e1c
--- /dev/null
+++ b/modules/ducktests/tests/docker/requirements-dev.txt
@@ -0,0 +1,19 @@
+# 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.
+
+-r requirements.txt
+pytest==6.0.1
+pylint==2.6.0
+flake8==3.8.3
diff --git a/modules/ducktests/tests/docker/requirements.txt b/modules/ducktests/tests/docker/requirements.txt
new file mode 100644
index 0000000000000..06f73eb5b322b
--- /dev/null
+++ b/modules/ducktests/tests/docker/requirements.txt
@@ -0,0 +1,16 @@
+# 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.
+
+git+https://github.com/Sberbank-Technology/ducktape
diff --git a/modules/ducktests/tests/docker/run_tests.sh b/modules/ducktests/tests/docker/run_tests.sh
new file mode 100755
index 0000000000000..bf20488d2230a
--- /dev/null
+++ b/modules/ducktests/tests/docker/run_tests.sh
@@ -0,0 +1,162 @@
+#!/usr/bin/env 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.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+###
+# DuckerUp parameters are specified with env variables
+
+# Num of cotainers that ducktape will prepare for tests
+IGNITE_NUM_CONTAINERS=${IGNITE_NUM_CONTAINERS:-13}
+
+# Image name to run nodes
+default_image_name="ducker-ignite-openjdk-8"
+IMAGE_NAME="${IMAGE_NAME:-$default_image_name}"
+
+###
+# DuckerTest parameters are specified with options to the script
+
+# Path to ducktests
+TC_PATHS="./ignitetest/"
+# Global parameters to pass to ducktape util with --global param
+GLOBALS="{}"
+# Ducktests parameters to pass to ducktape util with --parameters param
+PARAMETERS="{}"
+
+###
+# RunTests parameters
+# Force flag:
+# - skips ducker-ignite compare step;
+# - sends to duck-ignite scripts.
+FORCE=
+
+usage() {
+ cat <", self.STDOUT_STDERR_CAPTURE), allow_fail=False)
+ for line in output:
+ res.append(re.search("%s(.*)%s" % (name + "->", "<-"), line).group(1))
+
+ return res
diff --git a/modules/ducktests/tests/ignitetest/services/ignite_execution_exception.py b/modules/ducktests/tests/ignitetest/services/ignite_execution_exception.py
new file mode 100644
index 0000000000000..ce6cb562cfdc4
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/ignite_execution_exception.py
@@ -0,0 +1,24 @@
+# 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.
+
+"""
+Ignite execution exception
+"""
+
+
+class IgniteExecutionException(Exception):
+ """
+ Ignite execution exception implementation
+ """
diff --git a/modules/ducktests/tests/ignitetest/services/spark.py b/modules/ducktests/tests/ignitetest/services/spark.py
new file mode 100644
index 0000000000000..09e99ac7f623f
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/spark.py
@@ -0,0 +1,166 @@
+# 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.
+
+"""
+This module contains spark service class.
+"""
+
+import os.path
+
+from ducktape.cluster.remoteaccount import RemoteCommandError
+from ducktape.services.background_thread import BackgroundThreadService
+
+from ignitetest.services.utils.ignite_persistence import PersistenceAware
+from ignitetest.services.utils.log_utils import monitor_log
+
+
+class SparkService(BackgroundThreadService, PersistenceAware):
+ """
+ Start a spark node.
+ """
+ INSTALL_DIR = "/opt/spark-{version}".format(version="2.3.4")
+ SPARK_PERSISTENT_ROOT = "/mnt/spark"
+
+ logs = {}
+
+ # pylint: disable=R0913
+ def __init__(self, context, num_nodes=3):
+ """
+ :param context: test context
+ :param num_nodes: number of Ignite nodes.
+ """
+ super().__init__(context, num_nodes)
+
+ self.log_level = "DEBUG"
+
+ for node in self.nodes:
+ self.logs["master_logs" + node.account.hostname] = {
+ "path": self.master_log_path(node),
+ "collect_default": True
+ }
+ self.logs["worker_logs" + node.account.hostname] = {
+ "path": self.slave_log_path(node),
+ "collect_default": True
+ }
+
+ def start(self, clean=True):
+ BackgroundThreadService.start(self, clean=clean)
+
+ self.logger.info("Waiting for Spark to start...")
+
+ def start_cmd(self, node):
+ """
+ Prepare command to start Spark nodes
+ """
+ if node == self.nodes[0]:
+ script = "start-master.sh"
+ else:
+ script = "start-slave.sh spark://{spark_master}:7077".format(spark_master=self.nodes[0].account.hostname)
+
+ start_script = os.path.join(SparkService.INSTALL_DIR, "sbin", script)
+
+ cmd = "export SPARK_LOG_DIR={spark_dir}; ".format(spark_dir=SparkService.SPARK_PERSISTENT_ROOT)
+ cmd += "export SPARK_WORKER_DIR={spark_dir}; ".format(spark_dir=SparkService.SPARK_PERSISTENT_ROOT)
+ cmd += "{start_script} &".format(start_script=start_script)
+
+ return cmd
+
+ def start_node(self, node):
+ self.init_persistent(node)
+
+ cmd = self.start_cmd(node)
+ self.logger.debug("Attempting to start SparkService on %s with command: %s" % (str(node.account), cmd))
+
+ if node == self.nodes[0]:
+ log_file = self.master_log_path(node)
+ log_msg = "Started REST server for submitting applications"
+ else:
+ log_file = self.slave_log_path(node)
+ log_msg = "Successfully registered with master"
+
+ self.logger.debug("Monitoring - %s" % log_file)
+
+ timeout_sec = 30
+ with monitor_log(node, log_file) as monitor:
+ node.account.ssh(cmd)
+ monitor.wait_until(log_msg, timeout_sec=timeout_sec, backoff_sec=5,
+ err_msg="Spark doesn't start at %d seconds" % timeout_sec)
+
+ if len(self.pids(node)) == 0:
+ raise Exception("No process ids recorded on node %s" % node.account.hostname)
+
+ def stop_node(self, node):
+ if node == self.nodes[0]:
+ node.account.ssh(os.path.join(SparkService.INSTALL_DIR, "sbin", "stop-master.sh"))
+ else:
+ node.account.ssh(os.path.join(SparkService.INSTALL_DIR, "sbin", "stop-slave.sh"))
+
+ def clean_node(self, node):
+ """
+ Clean spark persistence files
+ """
+ node.account.kill_java_processes(self.java_class_name(node),
+ clean_shutdown=False, allow_fail=True)
+ node.account.ssh("sudo rm -rf -- %s" % SparkService.SPARK_PERSISTENT_ROOT, allow_fail=False)
+
+ def pids(self, node):
+ """
+ :return: list of service pids on specific node
+ """
+ try:
+ cmd = "jcmd | grep -e %s | awk '{print $1}'" % self.java_class_name(node)
+ return list(node.account.ssh_capture(cmd, allow_fail=True, callback=int))
+ except (RemoteCommandError, ValueError):
+ return []
+
+ def java_class_name(self, node):
+ """
+ :param node: Spark node.
+ :return: Class name depending on node type (master or slave).
+ """
+ if node == self.nodes[0]:
+ return "org.apache.spark.deploy.master.Master"
+
+ return "org.apache.spark.deploy.worker.Worker"
+
+ @staticmethod
+ def master_log_path(node):
+ """
+ :param node: Spark master node.
+ :return: Path to log file.
+ """
+ return "{SPARK_LOG_DIR}/spark-{userID}-org.apache.spark.deploy.master.Master-{instance}-{host}.out".format(
+ SPARK_LOG_DIR=SparkService.SPARK_PERSISTENT_ROOT,
+ userID=node.account.user,
+ instance=1,
+ host=node.account.hostname)
+
+ @staticmethod
+ def slave_log_path(node):
+ """
+ :param node: Spark slave node.
+ :return: Path to log file.
+ """
+ return "{SPARK_LOG_DIR}/spark-{userID}-org.apache.spark.deploy.worker.Worker-{instance}-{host}.out".format(
+ SPARK_LOG_DIR=SparkService.SPARK_PERSISTENT_ROOT,
+ userID=node.account.user,
+ instance=1,
+ host=node.account.hostname)
+
+ def kill(self):
+ """
+ Kills the service.
+ """
+ self.stop()
diff --git a/modules/ducktests/tests/ignitetest/services/utils/__init__.py b/modules/ducktests/tests/ignitetest/services/utils/__init__.py
new file mode 100644
index 0000000000000..ec2014340d78f
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/__init__.py
@@ -0,0 +1,14 @@
+# 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.
diff --git a/modules/ducktests/tests/ignitetest/services/utils/concurrent.py b/modules/ducktests/tests/ignitetest/services/utils/concurrent.py
new file mode 100644
index 0000000000000..99292fdfdaddb
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/concurrent.py
@@ -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.
+
+"""
+This module contains concurrent utils.
+"""
+
+import threading
+
+
+class CountDownLatch:
+ """
+ A count-down latch.
+ """
+ def __init__(self, count=1):
+ self.count = count
+ self.cond_var = threading.Condition()
+
+ def count_down(self):
+ """
+ Decreases the latch counter.
+ """
+ with self.cond_var:
+ if self.count > 0:
+ self.count -= 1
+ if self.count == 0:
+ self.cond_var.notifyAll()
+
+ def wait(self):
+ """
+ Blocks current thread if the latch is not free.
+ """
+ with self.cond_var:
+ while self.count > 0:
+ self.cond_var.wait()
+
+
+class AtomicValue:
+ """
+ An atomic reference holder.
+ """
+ def __init__(self, value=None):
+ self.value = value
+ self.lock = threading.Lock()
+
+ def set(self, value):
+ """
+ Sets new value to hold.
+ :param value: New value to hold.
+ """
+ with self.lock:
+ self.value = value
+
+ def get(self):
+ """
+ Gives current value.
+ """
+ with self.lock:
+ return self.value
+
+ def compare_and_set(self, expected, value):
+ """
+ Sets new value to hold if current one equals expected.
+ :param expected: The value to compare with.
+ :param value: New value to hold.
+ """
+ return self.check_and_set(lambda: self.value == expected, value)
+
+ def check_and_set(self, condition, value):
+ """
+ Sets new value to hold by condition.
+ :param condition: The condition to check.
+ :param value: New value to hold.
+ """
+ with self.lock:
+ if condition():
+ self.value = value
+ return self.value
diff --git a/modules/ducktests/tests/ignitetest/services/utils/config_template.py b/modules/ducktests/tests/ignitetest/services/utils/config_template.py
new file mode 100644
index 0000000000000..875f12b35c73c
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/config_template.py
@@ -0,0 +1,72 @@
+# 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.
+
+"""
+This module contains ignite config classes and utilities.
+"""
+import os
+
+from jinja2 import FileSystemLoader, Environment
+
+DEFAULT_CONFIG_PATH = os.path.dirname(os.path.abspath(__file__)) + "/templates"
+DEFAULT_IGNITE_CONF = DEFAULT_CONFIG_PATH + "/ignite.xml.j2"
+
+
+class ConfigTemplate:
+ """
+ Basic configuration.
+ """
+ def __init__(self, path):
+ tmpl_dir = os.path.dirname(path)
+ tmpl_file = os.path.basename(path)
+
+ tmpl_loader = FileSystemLoader(searchpath=[DEFAULT_CONFIG_PATH, tmpl_dir])
+ env = Environment(loader=tmpl_loader)
+
+ self.template = env.get_template(tmpl_file)
+ self.default_params = {}
+
+ def render(self, **kwargs):
+ """
+ Render configuration.
+ """
+ kwargs.update(self.default_params)
+ res = self.template.render(**kwargs)
+ return res
+
+
+class IgniteServerConfigTemplate(ConfigTemplate):
+ """
+ Ignite server node configuration.
+ """
+ def __init__(self, path=DEFAULT_IGNITE_CONF):
+ super().__init__(path)
+
+
+class IgniteClientConfigTemplate(ConfigTemplate):
+ """
+ Ignite client node configuration.
+ """
+ def __init__(self, path=DEFAULT_IGNITE_CONF):
+ super().__init__(path)
+ self.default_params.update(client_mode=True)
+
+
+class IgniteLoggerConfigTemplate(ConfigTemplate):
+ """
+ Ignite logger configuration.
+ """
+ def __init__(self):
+ super().__init__(DEFAULT_CONFIG_PATH + "/log4j.xml.j2")
diff --git a/modules/ducktests/tests/ignitetest/services/utils/control_utility.py b/modules/ducktests/tests/ignitetest/services/utils/control_utility.py
new file mode 100644
index 0000000000000..90b6549a6c497
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/control_utility.py
@@ -0,0 +1,365 @@
+# 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.
+
+"""
+This module contains control utility wrapper.
+"""
+
+import random
+import re
+import time
+from typing import NamedTuple
+
+from ducktape.cluster.remoteaccount import RemoteCommandError
+
+
+class ControlUtility:
+ """
+ Control utility (control.sh) wrapper.
+ """
+ BASE_COMMAND = "control.sh"
+
+ def __init__(self, cluster, text_context):
+ self._cluster = cluster
+ self.logger = text_context.logger
+
+ def baseline(self):
+ """
+ :return Baseline nodes.
+ """
+ return self.cluster_state().baseline
+
+ def cluster_state(self):
+ """
+ :return: Cluster state.
+ """
+ result = self.__run("--baseline")
+
+ return self.__parse_cluster_state(result)
+
+ def set_baseline(self, baseline):
+ """
+ :param baseline: Baseline nodes or topology version to set as baseline.
+ """
+ if isinstance(baseline, int):
+ result = self.__run(f"--baseline version {baseline} --yes")
+ else:
+ result = self.__run(
+ f"--baseline set {','.join([node.account.externally_routable_ip for node in baseline])} --yes")
+
+ return self.__parse_cluster_state(result)
+
+ def add_to_baseline(self, nodes):
+ """
+ :param nodes: Nodes that should be added to baseline.
+ """
+ result = self.__run(
+ f"--baseline add {','.join([node.account.externally_routable_ip for node in nodes])} --yes")
+
+ return self.__parse_cluster_state(result)
+
+ def remove_from_baseline(self, nodes):
+ """
+ :param nodes: Nodes that should be removed to baseline.
+ """
+ result = self.__run(
+ f"--baseline remove {','.join([node.account.externally_routable_ip for node in nodes])} --yes")
+
+ return self.__parse_cluster_state(result)
+
+ def disable_baseline_auto_adjust(self):
+ """
+ Disable baseline auto adjust.
+ """
+ return self.__run("--baseline auto_adjust disable --yes")
+
+ def enable_baseline_auto_adjust(self, timeout=None):
+ """
+ Enable baseline auto adjust.
+ :param timeout: Auto adjust timeout in millis.
+ """
+ timeout_str = f"timeout {timeout}" if timeout else ""
+ return self.__run(f"--baseline auto_adjust enable {timeout_str} --yes")
+
+ def activate(self):
+ """
+ Activate cluster.
+ """
+ return self.__run("--activate --yes")
+
+ def deactivate(self):
+ """
+ Deactivate cluster.
+ """
+ return self.__run("--deactivate --yes")
+
+ def tx(self, **kwargs):
+ """
+ Get list of transactions, various filters can be applied.
+ """
+ output = self.__run(self.__tx_command(**kwargs))
+ res = self.__parse_tx_list(output)
+ return res if res else output
+
+ def tx_info(self, xid):
+ """
+ Get verbose transaction info by xid.
+ """
+ return self.__parse_tx_info(self.__run(f"--tx --info {xid}"))
+
+ def tx_kill(self, **kwargs):
+ """
+ Kill transaction by xid or by various filter.
+ """
+ output = self.__run(self.__tx_command(kill=True, **kwargs))
+ res = self.__parse_tx_list(output)
+ return res if res else output
+
+ @staticmethod
+ def __tx_command(**kwargs):
+ tokens = ["--tx"]
+
+ if 'xid' in kwargs:
+ tokens.append(f"--xid {kwargs['xid']}")
+
+ if kwargs.get('clients'):
+ tokens.append("--clients")
+
+ if kwargs.get('servers'):
+ tokens.append("--servers")
+
+ if 'min_duration' in kwargs:
+ tokens.append(f"--min-duration {kwargs.get('min_duration')}")
+
+ if 'min_size' in kwargs:
+ tokens.append(f"--min-size {kwargs.get('min_size')}")
+
+ if 'label_pattern' in kwargs:
+ tokens.append(f"--label {kwargs['label_pattern']}")
+
+ if kwargs.get("nodes"):
+ tokens.append(f"--nodes {','.join(kwargs.get('nodes'))}")
+
+ if 'limit' in kwargs:
+ tokens.append(f"--limit {kwargs['limit']}")
+
+ if 'order' in kwargs:
+ tokens.append(f"--order {kwargs['order']}")
+
+ if kwargs.get('kill'):
+ tokens.append("--kill --yes")
+
+ return " ".join(tokens)
+
+ @staticmethod
+ def __parse_tx_info(output):
+ tx_info_pattern = re.compile(
+ "Near XID version: (?PGridCacheVersion \\[topVer=\\d+, order=\\d+, nodeOrder=\\d+\\])\\n\\s+"
+ "Near XID version \\(UUID\\): (?P[^\\s]+)\\n\\s+"
+ "Isolation: (?P[^\\s]+)\\n\\s+"
+ "Concurrency: (?P[^\\s]+)\\n\\s+"
+ "Timeout: (?P\\d+)\\n\\s+"
+ "Initiator node: (?P[^\\s]+)\\n\\s+"
+ "Initiator node \\(consistent ID\\): (?P[^\\s+]+)\\n\\s+"
+ "Label: (?P[^\\s]+)\\n\\s+Topology version: AffinityTopologyVersion "
+ "\\[topVer=(?P\\d+), minorTopVer=(?P\\d+)\\]\\n\\s+"
+ "Used caches \\(ID to name\\): {(?P.*)}\\n\\s+"
+ "Used cache groups \\(ID to name\\): {(?P.*)}\\n\\s+"
+ "States across the cluster: \\[(?P.*)\\]"
+ )
+
+ match = tx_info_pattern.search(output)
+
+ str_fields = ['xid', 'xid_full', 'label', 'timeout', 'isolation', 'concurrency', 'initiator_id',
+ 'initiator_consistent_id']
+ dict_fields = ['caches', 'cache_groups']
+
+ if match:
+ kwargs = {v: match.group(v) for v in str_fields}
+ kwargs['timeout'] = int(match.group('timeout'))
+ kwargs.update({v: parse_dict(match.group(v)) for v in dict_fields})
+ kwargs['top_ver'] = (int(match.group('top_ver')), int(match.group('minor_top_ver')))
+ kwargs['states'] = parse_list(match.group('states'))
+
+ return TxVerboseInfo(**kwargs)
+
+ return None
+
+ @staticmethod
+ def __parse_tx_list(output):
+ tx_pattern = re.compile(
+ "Tx: \\[xid=(?P[^\\s]+), "
+ "label=(?P[^\\s]+), state=(?P[^\\s]+), "
+ "startTime=(?P\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3}), duration=(?P\\d+), "
+ "isolation=(?P[^\\s]+), concurrency=(?P[^\\s]+), "
+ "topVer=AffinityTopologyVersion \\[topVer=(?P\\d+), minorTopVer=(?P\\d+)\\], "
+ "timeout=(?P\\d+), size=(?P\\d+), dhtNodes=\\[(?P.*)\\], "
+ "nearXid=(?P[^\\s]+), parentNodeIds=\\[(?P.*)\\]\\]")
+
+ str_fields = ['xid', 'label', 'state', 'isolation', 'concurrency', 'near_xid']
+ int_fields = ['timeout', 'size', 'duration']
+ list_fields = ['parent_nodes', 'dht_nodes']
+
+ tx_list = []
+ for match in tx_pattern.finditer(output):
+ kwargs = {v: match.group(v) for v in str_fields}
+ kwargs.update({v: int(match.group(v)) for v in int_fields})
+ kwargs['top_ver'] = (int(match.group('top_ver')), int(match.group('minor_top_ver')))
+ kwargs.update({v: parse_list(match.group(v)) for v in list_fields})
+ kwargs['start_time'] = time.strptime(match.group('start_time'), "%Y-%m-%d %H:%M:%S.%f")
+ tx_list.append(TxInfo(**kwargs))
+
+ return tx_list
+
+ @staticmethod
+ def __parse_cluster_state(output):
+ state_pattern = re.compile("Cluster state: (?P[^\\s]+)")
+ topology_pattern = re.compile("Current topology version: (?P\\d+)")
+ baseline_pattern = re.compile("Consistent(Id|ID)=(?P[^\\s]+)"
+ "(,\\sA(ddress|DDRESS)=(?P[^\\s]+))?"
+ ",\\sS(tate|TATE)=(?P[^\\s]+)"
+ "(,\\sOrder=(?P\\d+))?")
+
+ match = state_pattern.search(output)
+ state = match.group("cluster_state") if match else None
+
+ match = topology_pattern.search(output)
+ topology = int(match.group("topology_version")) if match else None
+
+ baseline = []
+ for match in baseline_pattern.finditer(output):
+ node = BaselineNode(consistent_id=match.group("consistent_id"),
+ state=match.group("state"),
+ address=match.group("address"),
+ order=int(match.group("order")) if match.group("order") else None)
+ baseline.append(node)
+
+ return ClusterState(state=state, topology_version=topology, baseline=baseline)
+
+ def __run(self, cmd):
+ node = random.choice(self.__alives())
+
+ self.logger.debug(f"Run command {cmd} on node {node.name}")
+
+ raw_output = node.account.ssh_capture(self.__form_cmd(node, cmd), allow_fail=True)
+ code, output = self.__parse_output(raw_output)
+
+ self.logger.debug(f"Output of command {cmd} on node {node.name}, exited with code {code}, is {output}")
+
+ if code != 0:
+ raise ControlUtilityError(node.account, cmd, code, output)
+
+ return output
+
+ def __form_cmd(self, node, cmd):
+ return self._cluster.spec.path.script(f"{self.BASE_COMMAND} --host {node.account.externally_routable_ip} {cmd}")
+
+ @staticmethod
+ def __parse_output(raw_output):
+ exit_code = raw_output.channel_file.channel.recv_exit_status()
+ output = "".join(raw_output)
+
+ pattern = re.compile("Command \\[[^\\s]*\\] finished with code: (\\d+)")
+ match = pattern.search(output)
+
+ if match:
+ return int(match.group(1)), output
+ return exit_code, output
+
+ def __alives(self):
+ return [node for node in self._cluster.nodes if self._cluster.alive(node)]
+
+
+class BaselineNode(NamedTuple):
+ """
+ Baseline node info.
+ """
+ consistent_id: str
+ state: str
+ address: str
+ order: int
+
+
+class ClusterState(NamedTuple):
+ """
+ Cluster state info.
+ """
+ state: str
+ topology_version: int
+ baseline: list
+
+
+class TxInfo(NamedTuple):
+ """
+ Transaction info.
+ """
+ xid: str
+ near_xid: str
+ label: str
+ state: str
+ start_time: time.struct_time
+ duration: int
+ isolation: str
+ concurrency: str
+ top_ver: tuple
+ timeout: int
+ size: int
+ dht_nodes: list = []
+ parent_nodes: list = []
+
+
+class TxVerboseInfo(NamedTuple):
+ """
+ Transaction info returned with --info
+ """
+ xid: str
+ xid_full: str
+ label: str
+ isolation: str
+ concurrency: str
+ timeout: int
+ top_ver: tuple
+ initiator_id: str
+ initiator_consistent_id: str
+ caches: dict
+ cache_groups: dict
+ states: list
+
+
+class ControlUtilityError(RemoteCommandError):
+ """
+ Error is raised when control utility failed.
+ """
+ def __init__(self, account, cmd, exit_status, output):
+ super().__init__(account, cmd, exit_status, "".join(output))
+
+
+def parse_dict(raw):
+ """
+ Parse java Map.toString() to python dict.
+ """
+ res = {}
+ for token in raw.split(','):
+ key, value = tuple(token.strip().split('='))
+ res[key] = value
+
+ return res
+
+
+def parse_list(raw):
+ """
+ Parse java List.toString() to python list
+ """
+ return [token.strip() for token in raw.split(',')]
diff --git a/modules/ducktests/tests/ignitetest/services/utils/decorators.py b/modules/ducktests/tests/ignitetest/services/utils/decorators.py
new file mode 100644
index 0000000000000..9c767f4efbee4
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/decorators.py
@@ -0,0 +1,40 @@
+# 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.
+
+"""
+This module contains various useful decorators.
+"""
+
+import functools
+from threading import RLock
+
+
+def memoize(func):
+ """
+ Decorate function to memoize first call to thread safe cache.
+ """
+ cache = func.cache = {}
+ lock = RLock()
+
+ @functools.wraps(func)
+ def memoized_func(*args, **kwargs):
+ key = str(args) + str(kwargs)
+ if key not in cache:
+ with lock:
+ if key not in cache:
+ cache[key] = func(*args, **kwargs)
+ return cache[key]
+
+ return memoized_func
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
new file mode 100644
index 0000000000000..43fca2fc6df47
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_aware.py
@@ -0,0 +1,353 @@
+# 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.
+
+"""
+This module contains the base class to build services aware of Ignite.
+"""
+import os
+import signal
+import socket
+import sys
+import time
+from abc import abstractmethod, ABCMeta
+from datetime import datetime
+from threading import Thread
+
+from ducktape.services.background_thread import BackgroundThreadService
+from ducktape.utils.util import wait_until
+
+from ignitetest.services.utils.concurrent import CountDownLatch, AtomicValue
+from ignitetest.services.utils.ignite_persistence import IgnitePersistenceAware
+from ignitetest.services.utils.ignite_spec import resolve_spec
+from ignitetest.services.utils.jmx_utils import ignite_jmx_mixin
+from ignitetest.services.utils.log_utils import monitor_log
+
+
+class IgniteAwareService(BackgroundThreadService, IgnitePersistenceAware, metaclass=ABCMeta):
+ """
+ The base class to build services aware of Ignite.
+ """
+
+ NETFILTER_STORE_PATH = os.path.join(IgnitePersistenceAware.TEMP_DIR, "iptables.bak")
+
+ # pylint: disable=R0913
+ def __init__(self, context, config, num_nodes, startup_timeout_sec, shutdown_timeout_sec, **kwargs):
+ """
+ **kwargs are params that passed to IgniteSpec
+ """
+ super().__init__(context, num_nodes)
+
+ # Ducktape checks a Service implementation attribute 'logs' to get config for logging.
+ # IgniteAwareService uses IgnitePersistenceAware mixin to override default Service 'log' definition.
+ self.log_level = "DEBUG"
+
+ self.config = config
+ self.startup_timeout_sec = startup_timeout_sec
+ self.shutdown_timeout_sec = shutdown_timeout_sec
+
+ self.spec = resolve_spec(self, context, config, **kwargs)
+
+ self.disconnected_nodes = []
+ self.killed = False
+
+ def start_async(self, clean=True):
+ """
+ Starts in async way.
+ """
+ super().start(clean=clean)
+
+ def start(self, clean=True):
+ self.start_async(clean=clean)
+ self.await_started()
+
+ def await_started(self):
+ """
+ Awaits start finished.
+ """
+ self.logger.info("Waiting for IgniteAware(s) to start ...")
+
+ self.await_event("Topology snapshot", self.startup_timeout_sec, from_the_beginning=True)
+
+ def start_node(self, node):
+ self.init_persistent(node)
+
+ super().start_node(node)
+
+ wait_until(lambda: self.alive(node), timeout_sec=10)
+
+ ignite_jmx_mixin(node, self.pids(node))
+
+ def stop_async(self):
+ """
+ Stop in async way.
+ """
+ super().stop()
+
+ def stop(self):
+ if not self.killed:
+ self.stop_async()
+ self.await_stopped()
+ else:
+ self.logger.debug("Skipping node stop since it already killed.")
+
+ def await_stopped(self):
+ """
+ Awaits stop finished.
+ """
+ self.logger.info("Waiting for IgniteAware(s) to stop ...")
+
+ for node in self.nodes:
+ stopped = self.wait_node(node, timeout_sec=self.shutdown_timeout_sec)
+ assert stopped, "Node %s's worker thread did not stop in %d seconds" % \
+ (str(node.account), self.shutdown_timeout_sec)
+
+ for node in self.nodes:
+ wait_until(lambda: not self.alive(node), timeout_sec=self.shutdown_timeout_sec,
+ err_msg="Node %s's remote processes failed to stop in %d seconds" %
+ (str(node.account), self.shutdown_timeout_sec))
+
+ def stop_node(self, node):
+ pids = self.pids(node)
+
+ for pid in pids:
+ node.account.signal(pid, signal.SIGTERM, allow_fail=False)
+
+ def kill(self):
+ """
+ Kills nodes.
+ """
+ self.logger.info("Killing IgniteAware(s) ...")
+
+ for node in self.nodes:
+ pids = self.pids(node)
+
+ for pid in pids:
+ node.account.signal(pid, signal.SIGKILL, allow_fail=False)
+
+ for node in self.nodes:
+ wait_until(lambda: not self.alive(node), timeout_sec=self.shutdown_timeout_sec,
+ err_msg="Node %s's remote processes failed to be killed in %d seconds" %
+ (str(node.account), self.shutdown_timeout_sec))
+
+ self.killed = True
+
+ def clean(self):
+ self.__restore_iptables()
+
+ super().clean()
+
+ def init_persistent(self, node):
+ """
+ Init persistent directory.
+ :param node: Ignite service node.
+ """
+ super().init_persistent(node)
+
+ node_config = self._prepare_config(node)
+
+ node.account.create_file(self.CONFIG_FILE, node_config)
+
+ def _prepare_config(self, node):
+ if not self.config.consistent_id:
+ config = self.config._replace(consistent_id=node.account.externally_routable_ip)
+ else:
+ config = self.config
+
+ config = config._replace(local_host=socket.gethostbyname(node.account.hostname))
+
+ config.discovery_spi.prepare_on_start(cluster=self)
+
+ node_config = self.spec.config_template.render(config_dir=self.PERSISTENT_ROOT, work_dir=self.WORK_DIR,
+ config=config)
+
+ setattr(node, "consistent_id", node.account.externally_routable_ip)
+
+ self.logger.debug("Config for node %s: %s" % (node.account.hostname, node_config))
+
+ return node_config
+
+ @abstractmethod
+ def pids(self, node):
+ """
+ :param node: Ignite service node.
+ :return: List of service's pids.
+ """
+ raise NotImplementedError
+
+ # pylint: disable=W0613
+ def _worker(self, idx, node):
+ cmd = self.spec.command
+
+ self.logger.debug("Attempting to start Application Service on %s with command: %s" % (str(node.account), cmd))
+
+ node.account.ssh(cmd)
+
+ def alive(self, node):
+ """
+ :param node: Ignite service node.
+ :return: True if node is alive.
+ """
+ return len(self.pids(node)) > 0
+
+ def await_event_on_node(self, evt_message, node, timeout_sec, from_the_beginning=False, backoff_sec=5):
+ """
+ Await for specific event message in a node's log file.
+ :param evt_message: Event message.
+ :param node: Ignite service node.
+ :param timeout_sec: Number of seconds to check the condition for before failing.
+ :param from_the_beginning: If True, search for message from the beginning of log file.
+ :param backoff_sec: Number of seconds to back off between each failure to meet the condition
+ before checking again.
+ """
+ with monitor_log(node, self.STDOUT_STDERR_CAPTURE, from_the_beginning) as monitor:
+ monitor.wait_until(evt_message, timeout_sec=timeout_sec, backoff_sec=backoff_sec,
+ err_msg="Event [%s] was not triggered on '%s' in %d seconds" % (evt_message, node.name,
+ timeout_sec))
+
+ def await_event(self, evt_message, timeout_sec, from_the_beginning=False, backoff_sec=5):
+ """
+ Await for specific event messages on all nodes.
+ :param evt_message: Event message.
+ :param timeout_sec: Number of seconds to check the condition for before failing.
+ :param from_the_beginning: If True, search for message from the beggining of log file.
+ :param backoff_sec: Number of seconds to back off between each failure to meet the condition
+ before checking again.
+ """
+ for node in self.nodes:
+ self.await_event_on_node(evt_message, node, timeout_sec, from_the_beginning=from_the_beginning,
+ backoff_sec=backoff_sec)
+
+ def exec_on_nodes_async(self, nodes, task, simultaneously=True, delay_ms=0, timeout_sec=20):
+ """
+ Executes given task on the nodes.
+ :param task: a 'lambda: node'.
+ :param simultaneously: Enables or disables simultaneous start of the task on each node.
+ :param delay_ms: delay before task run. Begins with 0, grows by delay_ms for each next node in nodes.
+ :param timeout_sec: timeout to wait the task.
+ """
+ sem = CountDownLatch(len(nodes)) if simultaneously else None
+ time_holder = AtomicValue()
+
+ delay = 0
+ threads = []
+
+ for node in nodes:
+ thread = Thread(target=self.__exec_on_node, args=(node, task, sem, delay, time_holder))
+
+ threads.append(thread)
+
+ thread.start()
+
+ delay += delay_ms
+
+ for thread in threads:
+ thread.join(timeout_sec)
+
+ return time_holder.get()
+
+ @staticmethod
+ def __exec_on_node(node, task, start_waiter=None, delay_ms=0, time_holder=None):
+ if start_waiter:
+ start_waiter.count_down()
+ start_waiter.wait()
+
+ if delay_ms > 0:
+ time.sleep(delay_ms / 1000.0)
+
+ if time_holder:
+ mono = time.monotonic()
+ timestamp = datetime.now()
+
+ time_holder.compare_and_set(None, (mono, timestamp))
+
+ task(node)
+
+ def drop_network(self, nodes=None):
+ """
+ Disconnects node from cluster.
+ """
+ if nodes is None:
+ assert self.num_nodes == 1
+ nodes = self.nodes
+
+ for node in nodes:
+ self.logger.info("Disconnecting " + node.account.hostname + ".")
+
+ self.__backup_iptables(nodes)
+
+ cm_spi = self.config.communication_spi
+ dsc_spi = self.config.discovery_spi
+
+ cm_ports = str(cm_spi.port) if cm_spi.port_range < 1 else str(cm_spi.port) + ':' + str(
+ cm_spi.port + cm_spi.port_range)
+
+ dsc_ports = str(dsc_spi.port) if not hasattr(dsc_spi, 'port_range') or dsc_spi.port_range < 1 else str(
+ dsc_spi.port) + ':' + str(dsc_spi.port + dsc_spi.port_range)
+
+ cmd = f"sudo iptables -I %s 1 -p tcp -m multiport --dport {dsc_ports},{cm_ports} -j DROP"
+
+ for node in nodes:
+ self.logger.debug("Activating netfilter on '%s': %s" % (node.name, self.__dump_netfilter_settings(node)))
+
+ return self.exec_on_nodes_async(nodes,
+ lambda n: (n.account.ssh_client.exec_command(cmd % "INPUT"),
+ n.account.ssh_client.exec_command(cmd % "OUTPUT")))
+
+ def __backup_iptables(self, nodes):
+ # Store current network filter settings.
+ for node in nodes:
+ cmd = "sudo iptables-save | tee " + IgniteAwareService.NETFILTER_STORE_PATH
+
+ exec_error = str(node.account.ssh_client.exec_command(cmd)[2].read(), sys.getdefaultencoding())
+
+ if "Warning: iptables-legacy tables present" in exec_error:
+ cmd = "sudo iptables-legacy-save | tee " + IgniteAwareService.NETFILTER_STORE_PATH
+
+ exec_error = str(node.account.ssh_client.exec_command(cmd)[2].read(), sys.getdefaultencoding())
+
+ assert len(exec_error) == 0, "Failed to store iptables rules on '%s': %s" % (node.name, exec_error)
+
+ self.logger.debug("Netfilter before launch on '%s': %s" % (node.name, self.__dump_netfilter_settings(node)))
+
+ assert self.disconnected_nodes.count(node) == 0
+
+ self.disconnected_nodes.append(node)
+
+ def __restore_iptables(self):
+ # Restore previous network filter settings.
+ cmd = "sudo iptables-restore < " + IgniteAwareService.NETFILTER_STORE_PATH
+
+ errors = []
+
+ for node in self.disconnected_nodes:
+ exec_error = str(node.account.ssh_client.exec_command(cmd)[2].read(), sys.getdefaultencoding())
+
+ if len(exec_error) > 0:
+ errors.append("Failed to restore iptables rules on '%s': %s" % (node.name, exec_error))
+ else:
+ self.logger.debug(
+ "Netfilter after launch on '%s': %s" % (node.name, self.__dump_netfilter_settings(node)))
+
+ if len(errors) > 0:
+ self.logger.error("Failed restoring actions:" + os.linesep + os.linesep.join(errors))
+
+ raise RuntimeError("Unable to restore node states. See the log above.")
+
+ @staticmethod
+ def __dump_netfilter_settings(node):
+ """
+ Reads current netfilter settings on the node for debugging purposes.
+ """
+ return str(node.account.ssh_client.exec_command("sudo iptables -L -n")[1].read(), sys.getdefaultencoding())
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/__init__.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/__init__.py
new file mode 100644
index 0000000000000..fbf3cd6d0f1a4
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/__init__.py
@@ -0,0 +1,50 @@
+# 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.
+
+"""
+This module contains IgniteConfiguration classes and utilities.
+"""
+
+from typing import NamedTuple
+
+from ignitetest.services.utils.ignite_configuration.communication import CommunicationSpi, TcpCommunicationSpi
+from ignitetest.services.utils.ignite_configuration.data_storage import DataStorageConfiguration
+from ignitetest.services.utils.ignite_configuration.discovery import DiscoverySpi, TcpDiscoverySpi
+from ignitetest.utils.version import IgniteVersion, DEV_BRANCH
+
+
+class IgniteConfiguration(NamedTuple):
+ """
+ Ignite configuration.
+ """
+ discovery_spi: DiscoverySpi = TcpDiscoverySpi()
+ communication_spi: CommunicationSpi = TcpCommunicationSpi()
+ version: IgniteVersion = DEV_BRANCH
+ cluster_state: str = 'ACTIVE'
+ client_mode: bool = False
+ consistent_id: str = None
+ failure_detection_timeout: int = 10000
+ sys_worker_blocked_timeout: int = 10000
+ properties: str = None
+ data_storage: DataStorageConfiguration = None
+ caches: list = []
+ local_host: str = None
+
+
+class IgniteClientConfiguration(IgniteConfiguration):
+ """
+ Ignite client configuration.
+ """
+ client_mode = True
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/cache.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/cache.py
new file mode 100644
index 0000000000000..cc58a69bb3670
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/cache.py
@@ -0,0 +1,29 @@
+# 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
+
+"""
+This module contains classes and utilities for Ignite Cache configuration.
+"""
+from typing import NamedTuple
+
+
+class CacheConfiguration(NamedTuple):
+ """
+ Ignite Cache configuration.
+ """
+ name: str
+ cache_mode: str = 'PARTITIONED'
+ atomicity_mode: str = 'ATOMIC'
+ backups: int = 0
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/communication.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/communication.py
new file mode 100644
index 0000000000000..edca32cfad326
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/communication.py
@@ -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
+
+"""
+Module contains classes and utility methods to create communication configuration for ignite nodes.
+"""
+
+from abc import ABCMeta, abstractmethod
+
+
+class CommunicationSpi(metaclass=ABCMeta):
+ """
+ Abstract class for CommunicationSpi.
+ """
+ @property
+ @abstractmethod
+ def type(self):
+ """
+ Type of CommunicationSpi.
+ """
+
+
+class TcpCommunicationSpi(CommunicationSpi):
+ """
+ TcpCommunicationSpi.
+ """
+ def __init__(self, port=47100, port_range=100):
+ self.port = port
+ self.port_range = port_range
+
+ @property
+ def type(self):
+ return "TCP"
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/data_storage.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/data_storage.py
new file mode 100644
index 0000000000000..7b2999d82e4b8
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/data_storage.py
@@ -0,0 +1,38 @@
+# 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
+
+"""
+This module contains classes and utilities for Ignite DataStorage configuration.
+"""
+
+from typing import NamedTuple
+
+
+class DataRegionConfiguration(NamedTuple):
+ """
+ Ignite DataRegion Configuration
+ """
+ name: str = "default"
+ persistent: bool = False
+ init_size: int = 100 * 1024 * 1024
+ max_size: int = 512 * 1024 * 1024
+
+
+class DataStorageConfiguration(NamedTuple):
+ """
+ Ignite DataStorage configuration
+ """
+ default: DataRegionConfiguration = DataRegionConfiguration()
+ regions: list = []
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/discovery.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/discovery.py
new file mode 100644
index 0000000000000..1cd310f16921a
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_configuration/discovery.py
@@ -0,0 +1,145 @@
+# 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
+
+"""
+Module contains classes and utility methods to create discovery configuration for ignite nodes.
+"""
+
+from abc import ABCMeta, abstractmethod
+
+from ignitetest.services.utils.ignite_aware import IgniteAwareService
+from ignitetest.services.zk.zookeeper import ZookeeperService
+
+
+class DiscoverySpi(metaclass=ABCMeta):
+ """
+ Abstract class for DiscoverySpi.
+ """
+ @property
+ @abstractmethod
+ def type(self):
+ """
+ Type of DiscoverySPI.
+ """
+
+ @abstractmethod
+ def prepare_on_start(self, **kwargs):
+ """
+ Call if update before start is needed.
+ """
+
+
+class ZookeeperDiscoverySpi(DiscoverySpi):
+ """
+ ZookeeperDiscoverySpi.
+ """
+ def __init__(self, zoo_service, root_path):
+ self.connection_string = zoo_service.connection_string()
+ self.port = zoo_service.settings.client_port
+ self.root_path = root_path
+
+ @property
+ def type(self):
+ return "ZOOKEEPER"
+
+ def prepare_on_start(self, **kwargs):
+ pass
+
+
+class TcpDiscoveryIpFinder(metaclass=ABCMeta):
+ """
+ Abstract class for TcpDiscoveryIpFinder.
+ """
+ @property
+ @abstractmethod
+ def type(self):
+ """
+ Type of TcpDiscoveryIpFinder.
+ """
+
+ @abstractmethod
+ def prepare_on_start(self, **kwargs):
+ """
+ Call if update before start is needed.
+ """
+
+
+class TcpDiscoveryVmIpFinder(TcpDiscoveryIpFinder):
+ """
+ IpFinder with static ips, obtained from cluster nodes.
+ """
+ def __init__(self, nodes=None):
+ self.addresses = TcpDiscoveryVmIpFinder.__get_addresses(nodes) if nodes else None
+
+ @property
+ def type(self):
+ return 'VM'
+
+ def prepare_on_start(self, **kwargs):
+ if not self.addresses:
+ cluster = kwargs.get('cluster')
+ self.addresses = TcpDiscoveryVmIpFinder.__get_addresses(cluster.nodes)
+
+ @staticmethod
+ def __get_addresses(nodes):
+ return [node.account.externally_routable_ip for node in nodes]
+
+
+class TcpDiscoverySpi(DiscoverySpi):
+ """
+ TcpDiscoverySpi.
+ """
+ def __init__(self, ip_finder=TcpDiscoveryVmIpFinder(), port=47500, port_range=100, local_address=None):
+ self.ip_finder = ip_finder
+ self.port = port
+ self.port_range = port_range
+ self.local_address = local_address
+
+ @property
+ def type(self):
+ return 'TCP'
+
+ def prepare_on_start(self, **kwargs):
+ self.ip_finder.prepare_on_start(**kwargs)
+
+
+def from_ignite_cluster(cluster, subset=None):
+ """
+ Form TcpDiscoverySpi from cluster or its subset.
+ :param cluster: IgniteService cluster
+ :param subset: slice object (optional).
+ :return: TcpDiscoverySpi with static ip addresses.
+ """
+ assert isinstance(cluster, IgniteAwareService)
+
+ if subset:
+ assert isinstance(subset, slice)
+ nodes = cluster.nodes[subset]
+ else:
+ nodes = cluster.nodes
+
+ return TcpDiscoverySpi(ip_finder=TcpDiscoveryVmIpFinder(nodes))
+
+
+def from_zookeeper_cluster(cluster, root_path="/apacheIgnite"):
+ """
+ Form ZookeeperDiscoverySpi from zookeeper service cluster.
+ :param cluster: ZookeeperService cluster.
+ :param root_path: root ZNode path.
+ :return: ZookeeperDiscoverySpi.
+ """
+ assert isinstance(cluster, ZookeeperService)
+
+ return ZookeeperDiscoverySpi(cluster, root_path=root_path)
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py
new file mode 100644
index 0000000000000..4089112533f98
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_path.py
@@ -0,0 +1,56 @@
+# 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.
+
+"""
+This module contains ignite path resolve utilities.
+"""
+
+import os
+
+
+class IgnitePath:
+ """Path resolver for Ignite system tests which assumes the following layout:
+
+ /opt/ignite-dev # Current version of Ignite under test
+ /opt/ignite-2.7.6 # Example of an older version of Ignite installed from tarball
+ /opt/ignite- # Other previous versions of Ignite
+ ...
+ """
+ SCRATCH_ROOT = "/mnt"
+ IGNITE_INSTALL_ROOT = "/opt"
+
+ def __init__(self, version, project="ignite"):
+ self.version = version
+ home_dir = "%s-%s" % (project, str(self.version))
+ self.home = os.path.join(IgnitePath.IGNITE_INSTALL_ROOT, home_dir)
+
+ def module(self, module_name):
+ """
+ :param module_name: name of Ignite optional lib
+ :return: absolute path to the specified module
+ """
+ if self.version.is_dev:
+ module_path = os.path.join("modules", module_name, "target")
+ else:
+ module_path = os.path.join("libs", "optional", "ignite-%s" % module_name)
+
+ return os.path.join(self.home, module_path)
+
+ def script(self, script_name):
+ """
+ :param script_name: name of Ignite script
+ :return: absolute path to the specified script
+ """
+ return os.path.join(self.home, "bin", script_name)
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_persistence.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_persistence.py
new file mode 100644
index 0000000000000..7cdc03c7d553f
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_persistence.py
@@ -0,0 +1,72 @@
+# 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.
+
+"""
+This module contains classes that represent persistent artifacts of tests
+"""
+
+import os
+
+from ignitetest.services.utils.config_template import IgniteLoggerConfigTemplate
+
+
+class PersistenceAware:
+ """
+ This class contains basic persistence artifacts
+ """
+ # Root directory for persistent output
+ PERSISTENT_ROOT = "/mnt/service"
+ STDOUT_STDERR_CAPTURE = os.path.join(PERSISTENT_ROOT, "console.log")
+ TEMP_DIR = os.path.join(PERSISTENT_ROOT, "tmp")
+
+ logs = {
+ "console_log": {
+ "path": STDOUT_STDERR_CAPTURE,
+ "collect_default": True
+ }
+ }
+
+ def init_persistent(self, node):
+ """
+ Init persistent directory.
+ :param node: Service node.
+ """
+ node.account.mkdirs(self.PERSISTENT_ROOT)
+ node.account.mkdirs(self.TEMP_DIR)
+
+
+class IgnitePersistenceAware(PersistenceAware):
+ """
+ This class contains Ignite persistence artifacts
+ """
+ WORK_DIR = os.path.join(PersistenceAware.PERSISTENT_ROOT, "work")
+ CONFIG_FILE = os.path.join(PersistenceAware.PERSISTENT_ROOT, "ignite-config.xml")
+ LOG4J_CONFIG_FILE = os.path.join(PersistenceAware.PERSISTENT_ROOT, "ignite-log4j.xml")
+
+ def __getattribute__(self, item):
+ if item == 'logs':
+ return PersistenceAware.logs
+
+ return super().__getattribute__(item)
+
+ def init_persistent(self, node):
+ """
+ Init persistent directory.
+ :param node: Ignite service node.
+ """
+ super().init_persistent(node)
+
+ logger_config = IgniteLoggerConfigTemplate().render(work_dir=self.WORK_DIR)
+ node.account.create_file(self.LOG4J_CONFIG_FILE, logger_config)
diff --git a/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py b/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
new file mode 100644
index 0000000000000..ac664f34e2922
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/ignite_spec.py
@@ -0,0 +1,210 @@
+# 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.
+
+"""
+This module contains Spec classes that describes config and command line to start Ignite services
+"""
+
+import base64
+import importlib
+import json
+from abc import ABCMeta, abstractmethod
+
+from ignitetest.services.utils.config_template import IgniteClientConfigTemplate, IgniteServerConfigTemplate
+from ignitetest.services.utils.ignite_path import IgnitePath
+from ignitetest.services.utils.ignite_persistence import IgnitePersistenceAware
+from ignitetest.utils.version import DEV_BRANCH
+
+
+def resolve_spec(service, context, config, **kwargs):
+ """
+ Resolve Spec classes for IgniteService and IgniteApplicationService
+ """
+ def _resolve_spec(name, default):
+ if name in context.globals:
+ fqdn = context.globals[name]
+ (module, clazz) = fqdn.rsplit('.', 1)
+ module = importlib.import_module(module)
+ return getattr(module, clazz)
+ return default
+
+ def is_impl(impl):
+ classes = map(lambda s: s.__name__, service.__class__.mro())
+ impl_filter = list(filter(lambda c: c == impl, classes))
+ return len(impl_filter) > 0
+
+ if is_impl("IgniteService"):
+ return _resolve_spec("NodeSpec", ApacheIgniteNodeSpec)(config=config, **kwargs)
+
+ if is_impl("IgniteApplicationService"):
+ return _resolve_spec("AppSpec", ApacheIgniteApplicationSpec)(context=context, config=config, **kwargs)
+
+ raise Exception("There is no specification for class %s" % type(service))
+
+
+class IgniteSpec(metaclass=ABCMeta):
+ """
+ This class is a basic Spec
+ """
+ def __init__(self, config, project, jvm_opts):
+ self.version = config.version
+ self.path = IgnitePath(self.version, project)
+ self.envs = {}
+ self.jvm_opts = jvm_opts or []
+ self.config = config
+
+ @property
+ def config_template(self):
+ """
+ :return: config that service will use to start on a node
+ """
+ if self.config.client_mode:
+ return IgniteClientConfigTemplate()
+ return IgniteServerConfigTemplate()
+
+ @property
+ @abstractmethod
+ def command(self):
+ """
+ :return: string that represents command to run service on a node
+ """
+
+ def _envs(self):
+ """
+ :return: line with exports env variables: export A=B; export C=D;
+ """
+ exports = ["export %s=%s" % (key, self.envs[key]) for key in self.envs]
+ return "; ".join(exports) + ";"
+
+ def _jvm_opts(self):
+ """
+ :return: line with extra JVM params for ignite.sh script: -J-Dparam=value -J-ea
+ """
+ opts = ["-J%s" % o for o in self.jvm_opts]
+ return " ".join(opts)
+
+
+class IgniteNodeSpec(IgniteSpec, IgnitePersistenceAware):
+ """
+ Spec to run ignite node
+ """
+ @property
+ def command(self):
+ cmd = "%s %s %s %s 2>&1 | tee -a %s &" % \
+ (self._envs(),
+ self.path.script("ignite.sh"),
+ self._jvm_opts(),
+ self.CONFIG_FILE,
+ self.STDOUT_STDERR_CAPTURE)
+
+ return cmd
+
+
+class IgniteApplicationSpec(IgniteSpec, IgnitePersistenceAware):
+ """
+ Spec to run ignite application
+ """
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.args = ""
+
+ def _app_args(self):
+ return ",".join(self.args)
+
+ @property
+ def command(self):
+ cmd = "%s %s %s %s 2>&1 | tee -a %s &" % \
+ (self._envs(),
+ self.path.script("ignite.sh"),
+ self._jvm_opts(),
+ self._app_args(),
+ self.STDOUT_STDERR_CAPTURE)
+
+ return cmd
+
+
+class ApacheIgniteNodeSpec(IgniteNodeSpec, IgnitePersistenceAware):
+ """
+ Implementation IgniteNodeSpec for Apache Ignite project
+ """
+ def __init__(self, modules, **kwargs):
+ super().__init__(project="ignite", **kwargs)
+
+ libs = (modules or [])
+ libs.append("log4j")
+ libs = list(map(lambda m: self.path.module(m) + "/*", libs))
+
+ libs.append(IgnitePath(DEV_BRANCH).module("ducktests") + "/*")
+
+ self.envs = {
+ 'EXCLUDE_TEST_CLASSES': 'true',
+ 'IGNITE_LOG_DIR': self.PERSISTENT_ROOT,
+ 'USER_LIBS': ":".join(libs)
+ }
+
+ self.jvm_opts.extend([
+ "-DIGNITE_SUCCESS_FILE=" + self.PERSISTENT_ROOT + "/success_file",
+ "-Dlog4j.configuration=file:" + self.LOG4J_CONFIG_FILE,
+ "-Dlog4j.configDebug=true"
+ ])
+
+
+class ApacheIgniteApplicationSpec(IgniteApplicationSpec, IgnitePersistenceAware):
+ """
+ Implementation IgniteApplicationSpec for Apache Ignite project
+ """
+ # pylint: disable=too-many-arguments
+ def __init__(self, context, modules, servicejava_class_name, java_class_name, params, start_ignite, **kwargs):
+ super().__init__(project="ignite", **kwargs)
+ self.context = context
+
+ libs = modules or []
+ libs.extend(["log4j"])
+
+ libs = [self.path.module(m) + "/*" for m in libs]
+ libs.append(IgnitePath(DEV_BRANCH).module("ducktests") + "/*")
+ libs.extend(self.__jackson())
+
+ self.envs = {
+ "MAIN_CLASS": servicejava_class_name,
+ "EXCLUDE_TEST_CLASSES": "true",
+ "IGNITE_LOG_DIR": self.PERSISTENT_ROOT,
+ "USER_LIBS": ":".join(libs)
+ }
+
+ self.jvm_opts.extend([
+ "-DIGNITE_SUCCESS_FILE=" + self.PERSISTENT_ROOT + "/success_file",
+ "-Dlog4j.configuration=file:" + self.LOG4J_CONFIG_FILE,
+ "-Dlog4j.configDebug=true",
+ "-DIGNITE_NO_SHUTDOWN_HOOK=true", # allows to perform operations on app termination.
+ "-Xmx1G",
+ "-ea",
+ "-DIGNITE_ALLOW_ATOMIC_OPS_IN_TX=false"
+ ])
+
+ self.args = [
+ str(start_ignite),
+ java_class_name,
+ self.CONFIG_FILE,
+ str(base64.b64encode(json.dumps(params).encode('utf-8')), 'utf-8')
+ ]
+
+ def __jackson(self):
+ if not self.version.is_dev:
+ aws = self.path.module("aws")
+ return self.context.cluster.nodes[0].account.ssh_capture(
+ "ls -d %s/* | grep jackson | tr '\n' ':' | sed 's/.$//'" % aws)
+
+ return []
diff --git a/modules/ducktests/tests/ignitetest/services/utils/jmx_utils.py b/modules/ducktests/tests/ignitetest/services/utils/jmx_utils.py
new file mode 100644
index 0000000000000..925034b90fbe4
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/jmx_utils.py
@@ -0,0 +1,195 @@
+# 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.
+
+"""
+This module contains JMX Console client and different utilities and mixins to retrieve ignite node parameters
+and attributes.
+"""
+
+import re
+
+from ignitetest.services.utils.decorators import memoize
+
+
+def ignite_jmx_mixin(node, pids):
+ """
+ Dynamically mixin JMX attributes to Ignite service node.
+ :param node: Ignite service node.
+ :param pids: Ignite service node pids.
+ """
+ setattr(node, 'pids', pids)
+ base_cls = node.__class__
+ base_cls_name = node.__class__.__name__
+ node.__class__ = type(base_cls_name, (base_cls, IgniteJmxMixin), {})
+
+
+class JmxMBean:
+ """
+ Dynamically exposes JMX MBean attributes.
+ """
+ def __init__(self, client, name):
+ self.client = client
+ self.name = name
+
+ def __getattr__(self, attr):
+ """
+ Retrieves through JMX client MBean attributes.
+ :param attr: Attribute name.
+ :return: Attribute value.
+ """
+ return self.client.mbean_attribute(self.name, attr)
+
+
+class JmxClient:
+ """JMX client, invokes jmxterm on node locally.
+ """
+ jmx_util_cmd = 'java -jar /opt/jmxterm.jar -v silent -n'
+
+ def __init__(self, node):
+ self.node = node
+ self.pid = node.pids[0]
+
+ @memoize
+ def find_mbean(self, pattern, domain='org.apache'):
+ """
+ Find mbean by specified pattern and domain on node.
+ :param pattern: MBean name pattern.
+ :param domain: Domain of MBean
+ :return: JmxMBean instance
+ """
+ cmd = "echo $'open %s\\n beans -d %s \\n close' | %s | grep -o '%s'" \
+ % (self.pid, domain, self.jmx_util_cmd, pattern)
+
+ name = next(self.__run_cmd(cmd)).strip()
+
+ return JmxMBean(self, name)
+
+ def mbean_attribute(self, mbean, attr):
+ """
+ Get MBean attribute.
+ :param mbean: MBean name
+ :param attr: Attribute name
+ :return: Attribute value
+ """
+ cmd = "echo $'open %s\\n get -b %s %s \\n close' | %s | sed 's/%s = \\(.*\\);/\\1/'" \
+ % (self.pid, mbean, attr, self.jmx_util_cmd, attr)
+
+ return iter(s.strip() for s in self.__run_cmd(cmd))
+
+ def __run_cmd(self, cmd):
+ return self.node.account.ssh_capture(cmd, allow_fail=False, callback=str)
+
+
+class DiscoveryInfo:
+ """ Ignite service node discovery info, obtained from DiscoverySpi mbean.
+ """
+ def __init__(self, coordinator, local_raw):
+ self._local_raw = local_raw
+ self._coordinator = coordinator
+
+ @property
+ def node_id(self):
+ """
+ :return: Local node id.
+ """
+ return self.__find__("id=([^\\s]+),")
+
+ @property
+ def coordinator(self):
+ """
+ :return: Coordinator node id.
+ """
+ return self._coordinator
+
+ @property
+ def consistent_id(self):
+ """
+ :return: Node consistent id, if presents (only in TcpDiscovery).
+ """
+ return self.__find__("consistentId=([^\\s]+),")
+
+ @property
+ def is_client(self):
+ """
+ :return: True if node is client.
+ """
+ return self.__find__("isClient=([^\\s]+),") == "true"
+
+ @property
+ def order(self):
+ """
+ :return: Topology order.
+ """
+ return int(self.__find__("order=(\\d+),"))
+
+ @property
+ def int_order(self):
+ """
+ :return: Internal order (TcpDiscovery).
+ """
+ val = self.__find__("intOrder=(\\d+),")
+ return int(val) if val else -1
+
+ def __find__(self, pattern):
+ res = re.search(pattern, self._local_raw)
+ return res.group(1) if res else None
+
+
+class IgniteJmxMixin:
+ """
+ Mixin to IgniteService node, exposing useful properties, obtained from JMX.
+ """
+ @memoize
+ def jmx_client(self):
+ """
+ :return: JmxClient instance.
+ """
+ # noinspection PyTypeChecker
+ return JmxClient(self)
+
+ @memoize
+ def node_id(self):
+ """
+ :return: Local node id.
+ """
+ return next(self.kernal_mbean().LocalNodeId).strip()
+
+ def discovery_info(self):
+ """
+ :return: DiscoveryInfo instance.
+ """
+ disco_mbean = self.disco_mbean()
+ crd = next(disco_mbean.Coordinator).strip()
+ local = next(disco_mbean.LocalNodeFormatted).strip()
+
+ return DiscoveryInfo(crd, local)
+
+ def kernal_mbean(self):
+ """
+ :return: IgniteKernal MBean.
+ """
+ return self.jmx_client().find_mbean('.*group=Kernal,name=IgniteKernal')
+
+ @memoize
+ def disco_mbean(self):
+ """
+ :return: DiscoverySpi MBean.
+ """
+ disco_spi = next(self.kernal_mbean().DiscoverySpiFormatted).strip()
+
+ if 'ZookeeperDiscoverySpi' in disco_spi:
+ return self.jmx_client().find_mbean('.*group=SPIs,name=ZookeeperDiscoverySpi')
+
+ return self.jmx_client().find_mbean('.*group=SPIs,name=TcpDiscoverySpi')
diff --git a/modules/ducktests/tests/ignitetest/services/utils/log_utils.py b/modules/ducktests/tests/ignitetest/services/utils/log_utils.py
new file mode 100644
index 0000000000000..58cb5e4219ba8
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/log_utils.py
@@ -0,0 +1,42 @@
+# 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.
+
+"""
+This module contains log utils.
+"""
+
+from contextlib import contextmanager
+
+from ducktape.cluster.remoteaccount import LogMonitor
+
+
+# pylint: disable=W0703
+@contextmanager
+def monitor_log(node, log, from_the_beginning=False):
+ """
+ Context manager that returns an object that helps you wait for events to
+ occur in a log. This checks the size of the log at the beginning of the
+ block and makes a helper object available with convenience methods for
+ checking or waiting for a pattern to appear in the log. This will commonly
+ be used to start a process, then wait for a log message indicating the
+ process is in a ready state.
+
+ See ``LogMonitor`` for more usage information.
+ """
+ try:
+ offset = 0 if from_the_beginning else int(node.account.ssh_output("wc -c %s" % log).split()[0])
+ except Exception:
+ offset = 0
+ yield LogMonitor(node.account, log, offset)
diff --git a/modules/ducktests/tests/ignitetest/services/utils/templates/cache_macro.j2 b/modules/ducktests/tests/ignitetest/services/utils/templates/cache_macro.j2
new file mode 100644
index 0000000000000..d227369abe3d7
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/templates/cache_macro.j2
@@ -0,0 +1,34 @@
+{#
+ 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.
+#}
+
+{% macro cache_configs(caches) %}
+ {% if caches %}
+
+
+ {% for cache in caches %}
+
+
+ {% if cache.cache_mode == 'PARTITIONED' %}
+
+ {% endif %}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+{% endmacro %}
diff --git a/modules/ducktests/tests/ignitetest/services/utils/templates/communication_macro.j2 b/modules/ducktests/tests/ignitetest/services/utils/templates/communication_macro.j2
new file mode 100644
index 0000000000000..1f5c17cb15a85
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/templates/communication_macro.j2
@@ -0,0 +1,25 @@
+{#
+ 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.
+#}
+
+{% macro communication_spi(spi) %}
+
+
+
+
+
+
+{% endmacro %}
diff --git a/modules/ducktests/tests/ignitetest/services/utils/templates/datastorage_macro.j2 b/modules/ducktests/tests/ignitetest/services/utils/templates/datastorage_macro.j2
new file mode 100644
index 0000000000000..1c2b4627b10e4
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/templates/datastorage_macro.j2
@@ -0,0 +1,46 @@
+{#
+ 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.
+#}
+
+{% macro data_storage(config) %}
+ {% if config %}
+
+
+
+ {{ data_region(config.default) }}
+
+ {% if config.regions %}
+
+
+ {% for region in config.regions %}
+ {{ data_region(region) }}
+ {% endfor %}
+
+
+ {% endif %}
+
+
+ {% endif %}
+{% endmacro %}
+
+{% macro data_region(config) %}
+
+
+
+
+
+
+{% endmacro %}
diff --git a/modules/ducktests/tests/ignitetest/services/utils/templates/discovery_macro.j2 b/modules/ducktests/tests/ignitetest/services/utils/templates/discovery_macro.j2
new file mode 100644
index 0000000000000..dc666dfda99da
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/templates/discovery_macro.j2
@@ -0,0 +1,64 @@
+{#
+ 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.
+#}
+
+{% macro ip_finder(spi) %}
+ {% if spi.ip_finder and spi.ip_finder.type == 'VM' %}
+
+
+
+
+ {% for address in spi.ip_finder.addresses %}
+ {% if spi.port_range > 0 %}
+ {{ address }}:{{ spi.port }}..{{ spi.port + spi.port_range }}
+ {% else %}
+ {{ address }}:{{ spi.port }}
+ {% endif %}
+ {% endfor %}
+
+
+
+
+ {% endif %}
+{% endmacro %}
+
+{% macro zookeeper_discovery_spi(spi) %}
+
+
+
+
+{% endmacro %}
+
+{% macro tcp_discovery_spi(spi) %}
+
+ {% if spi.local_address %}
+
+ {% endif %}
+
+
+ {{ ip_finder(spi) }}
+
+{% endmacro %}
+
+{% macro discovery_spi(spi) %}
+
+ {% if spi.type == 'TCP' %}
+ {{ tcp_discovery_spi(spi) }}
+ {% elif spi.type == 'ZOOKEEPER' %}
+ {{ zookeeper_discovery_spi(spi) }}
+ {% endif %}
+
+{% endmacro %}
diff --git a/modules/ducktests/tests/ignitetest/services/utils/templates/ignite.xml.j2 b/modules/ducktests/tests/ignitetest/services/utils/templates/ignite.xml.j2
new file mode 100644
index 0000000000000..cfed0a777ff9a
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/templates/ignite.xml.j2
@@ -0,0 +1,61 @@
+
+
+{#
+ 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.
+#}
+
+{% import 'communication_macro.j2' as communication %}
+{% import 'discovery_macro.j2' as disco_utils %}
+{% import 'cache_macro.j2' as cache_utils %}
+{% import 'datastorage_macro.j2' as datastorage_utils %}
+{% import 'misc_macro.j2' as misc_utils %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ misc_utils.cluster_state(config.cluster_state, config.version) }}
+
+ {{ communication.communication_spi(config.communication_spi) }}
+
+ {{ disco_utils.discovery_spi(config.discovery_spi) }}
+
+ {{ datastorage_utils.data_storage(config.data_storage) }}
+
+ {{ cache_utils.cache_configs(config.caches) }}
+
+ {% if config.local_host %}
+
+ {% endif %}
+
+ {% if config.properties %}
+ {{ config.properties }}
+ {% endif %}
+
+
diff --git a/modules/ducktests/tests/ignitetest/services/utils/templates/log4j.xml.j2 b/modules/ducktests/tests/ignitetest/services/utils/templates/log4j.xml.j2
new file mode 100644
index 0000000000000..57a00c062b6b3
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/templates/log4j.xml.j2
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/ducktests/tests/ignitetest/services/utils/templates/misc_macro.j2 b/modules/ducktests/tests/ignitetest/services/utils/templates/misc_macro.j2
new file mode 100644
index 0000000000000..892453b74fb2e
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/templates/misc_macro.j2
@@ -0,0 +1,24 @@
+{#
+ 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.
+#}
+
+{% macro cluster_state(state, version) %}
+ {% if version > "2.9.0" %}
+
+ {% else %}
+
+ {% endif %}
+{% endmacro %}
diff --git a/modules/ducktests/tests/ignitetest/services/utils/time_utils.py b/modules/ducktests/tests/ignitetest/services/utils/time_utils.py
new file mode 100644
index 0000000000000..01926bfed7129
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/utils/time_utils.py
@@ -0,0 +1,28 @@
+# 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.
+
+"""
+This module contains time utils.
+"""
+
+import time
+
+
+def epoch_mills(date_time):
+ """
+ :param date_time: a datetime.
+ :return: milliseconds since epoch of passed datetime.
+ """
+ return int(round((time.mktime(date_time.timetuple()) + date_time.microsecond / 1e6) * 1000))
diff --git a/modules/ducktests/tests/ignitetest/services/zk/__init__.py b/modules/ducktests/tests/ignitetest/services/zk/__init__.py
new file mode 100644
index 0000000000000..ec2014340d78f
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/zk/__init__.py
@@ -0,0 +1,14 @@
+# 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.
diff --git a/modules/ducktests/tests/ignitetest/services/zk/templates/log4j.properties.j2 b/modules/ducktests/tests/ignitetest/services/zk/templates/log4j.properties.j2
new file mode 100644
index 0000000000000..507ec9852ef14
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/zk/templates/log4j.properties.j2
@@ -0,0 +1,33 @@
+{#
+ 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.
+#}
+
+zookeeper.root.logger=INFO, FILE
+zookeeper.console.threshold=INFO
+
+zookeeper.log.dir={{ PERSISTENT_ROOT }}
+zookeeper.log.file=zookeeper.log
+zookeeper.log.threshold=INFO
+zookeeper.log.maxfilesize=256MB
+zookeeper.log.maxbackupindex=20
+
+log4j.rootLogger=${zookeeper.root.logger}
+
+log4j.appender.FILE=org.apache.log4j.FileAppender
+log4j.appender.FILE.Threshold=${zookeeper.log.threshold}
+log4j.appender.FILE.File=${zookeeper.log.dir}/${zookeeper.log.file}
+log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
+log4j.appender.FILE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
diff --git a/modules/ducktests/tests/ignitetest/services/zk/templates/zookeeper.properties.j2 b/modules/ducktests/tests/ignitetest/services/zk/templates/zookeeper.properties.j2
new file mode 100644
index 0000000000000..2ea8c3de4c9f3
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/zk/templates/zookeeper.properties.j2
@@ -0,0 +1,29 @@
+{#
+ 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.
+#}
+
+tickTime={{ settings.tick_time }}
+minSessionTimeout={{ settings.min_session_timeout }}
+initLimit={{ settings.init_limit }}
+syncLimit={{ settings.sync_limit }}
+dataDir={{ DATA_DIR }}
+clientPort={{ settings.client_port }}
+{% for node in nodes %}
+server.{{ loop.index }}={{ node.account.hostname }}:2888:3888
+{% endfor %}
+txnLogSizeLimitInKb=5120
+preAllocSize=5120
+forceSync=no
diff --git a/modules/ducktests/tests/ignitetest/services/zk/zookeeper.py b/modules/ducktests/tests/ignitetest/services/zk/zookeeper.py
new file mode 100644
index 0000000000000..baf88489f026f
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/services/zk/zookeeper.py
@@ -0,0 +1,159 @@
+# 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.
+
+"""
+This module contains classes and utilities to start zookeeper cluster for testing ZookeeperDiscovery.
+"""
+
+import os.path
+
+from ducktape.services.service import Service
+from ducktape.utils.util import wait_until
+
+from ignitetest.services.utils.log_utils import monitor_log
+
+
+class ZookeeperSettings:
+ """
+ Settings for zookeeper quorum nodes.
+ """
+ def __init__(self, **kwargs):
+ self.min_session_timeout = kwargs.get('min_session_timeout', 2000)
+ self.tick_time = kwargs.get('tick_time', self.min_session_timeout // 3)
+ self.init_limit = kwargs.get('init_limit', 10)
+ self.sync_limit = kwargs.get('sync_limit', 5)
+ self.client_port = kwargs.get('client_port', 2181)
+
+ assert self.tick_time <= self.min_session_timeout // 2, "'tick_time' must be <= 'min_session_timeout' / 2"
+
+
+class ZookeeperService(Service):
+ """
+ Zookeeper service.
+ """
+ PERSISTENT_ROOT = "/mnt/zookeeper"
+ CONFIG_ROOT = os.path.join(PERSISTENT_ROOT, "conf")
+ LOG_FILE = os.path.join(PERSISTENT_ROOT, "zookeeper.log")
+ DATA_DIR = os.path.join(PERSISTENT_ROOT, "data")
+ CONFIG_FILE = os.path.join(CONFIG_ROOT, "zookeeper.properties")
+ LOG_CONFIG_FILE = os.path.join(CONFIG_ROOT, "log4j.properties")
+ ZK_LIB_DIR = "/opt/zookeeper-3.5.8/lib"
+
+ logs = {
+ "zk_log": {
+ "path": LOG_FILE,
+ "collect_default": True
+ }
+ }
+
+ def __init__(self, context, num_nodes, settings=ZookeeperSettings(), start_timeout_sec=60):
+ super().__init__(context, num_nodes)
+ self.settings = settings
+ self.start_timeout_sec = start_timeout_sec
+
+ def start(self, clean=True):
+ super().start(clean=clean)
+ self.logger.info("Waiting for Zookeeper quorum...")
+
+ for node in self.nodes:
+ self.await_quorum(node, self.start_timeout_sec)
+
+ self.logger.info("Zookeeper quorum is formed.")
+
+ def start_node(self, node):
+ idx = self.idx(node)
+
+ self.logger.info("Starting Zookeeper node %d on %s", idx, node.account.hostname)
+
+ node.account.ssh("mkdir -p %s" % self.DATA_DIR)
+ node.account.ssh("mkdir -p %s" % self.CONFIG_ROOT)
+ node.account.ssh("echo %d > %s/myid" % (idx, self.DATA_DIR))
+
+ config_file = self.render('zookeeper.properties.j2', settings=self.settings)
+ node.account.create_file(self.CONFIG_FILE, config_file)
+ self.logger.info("ZK config %s", config_file)
+
+ log_config_file = self.render('log4j.properties.j2')
+ node.account.create_file(self.LOG_CONFIG_FILE, log_config_file)
+
+ start_cmd = "nohup java -cp %s/*:%s org.apache.zookeeper.server.quorum.QuorumPeerMain %s >/dev/null 2>&1 &" % \
+ (self.ZK_LIB_DIR, self.CONFIG_ROOT, self.CONFIG_FILE)
+
+ node.account.ssh(start_cmd)
+
+ def wait_node(self, node, timeout_sec=20):
+ wait_until(lambda: not self.alive(node), timeout_sec=timeout_sec)
+
+ return not self.alive(node)
+
+ def await_quorum(self, node, timeout):
+ """
+ Await quorum formed on node (leader election ready).
+ :param node: Zookeeper service node.
+ :param timeout: Wait timeout.
+ """
+ with monitor_log(node, self.LOG_FILE, from_the_beginning=True) as monitor:
+ monitor.wait_until(
+ "LEADER ELECTION TOOK",
+ timeout_sec=timeout,
+ err_msg="Zookeeper quorum was not formed on %s" % node.account.hostname
+ )
+
+ @staticmethod
+ def java_class_name():
+ """ The class name of the Zookeeper quorum peers. """
+ return "org.apache.zookeeper.server.quorum.QuorumPeerMain"
+
+ def pids(self, node):
+ """
+ Get pids of zookeeper service node.
+ :param node: Zookeeper service node.
+ :return: List of pids.
+ """
+ return node.account.java_pids(self.java_class_name())
+
+ def alive(self, node):
+ """
+ Check if zookeeper service node is alive.
+ :param node: Zookeeper service node.
+ :return: True if node is alive
+ """
+ return len(self.pids(node)) > 0
+
+ def connection_string(self):
+ """
+ Form a connection string to zookeeper cluster.
+ :return: Connection string.
+ """
+ return ','.join([node.account.hostname + ":" + str(2181) for node in self.nodes])
+
+ def stop_node(self, node):
+ idx = self.idx(node)
+ self.logger.info("Stopping %s node %d on %s" % (type(self).__name__, idx, node.account.hostname))
+ node.account.kill_process("zookeeper", allow_fail=False)
+
+ def clean_node(self, node):
+ self.logger.info("Cleaning Zookeeper node %d on %s", self.idx(node), node.account.hostname)
+ if self.alive(node):
+ self.logger.warn("%s %s was still alive at cleanup time. Killing forcefully..." %
+ (self.__class__.__name__, node.account))
+ node.account.kill_process("zookeeper", clean_shutdown=False, allow_fail=True)
+ node.account.ssh("rm -rf %s %s %s" % (self.CONFIG_ROOT, self.DATA_DIR, self.LOG_FILE), allow_fail=False)
+
+ def kill(self):
+ """
+ Kills the service.
+ """
+ self.stop()
diff --git a/modules/ducktests/tests/ignitetest/tests/__init__.py b/modules/ducktests/tests/ignitetest/tests/__init__.py
new file mode 100644
index 0000000000000..ec2014340d78f
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/__init__.py
@@ -0,0 +1,14 @@
+# 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.
diff --git a/modules/ducktests/tests/ignitetest/tests/add_node_rebalance_test.py b/modules/ducktests/tests/ignitetest/tests/add_node_rebalance_test.py
new file mode 100644
index 0000000000000..551180552f2a8
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/add_node_rebalance_test.py
@@ -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.
+
+"""
+Module contains node rebalance tests.
+"""
+
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration
+from ignitetest.services.utils.ignite_configuration.discovery import from_ignite_cluster
+from ignitetest.utils import ignite_versions, cluster
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, IgniteVersion, LATEST
+
+
+# pylint: disable=W0223
+class AddNodeRebalanceTest(IgniteTest):
+ """
+ Test basic rebalance scenarios.
+ """
+ NUM_NODES = 4
+ PRELOAD_TIMEOUT = 60
+ DATA_AMOUNT = 1000000
+ REBALANCE_TIMEOUT = 60
+
+ @cluster(num_nodes=NUM_NODES + 1)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ def test_add_node(self, ignite_version):
+ """
+ Test performs add node rebalance test which consists of following steps:
+ * Start cluster.
+ * Put data to it via IgniteClientApp.
+ * Start one more node and awaits for rebalance to finish.
+ """
+ node_config = IgniteConfiguration(version=IgniteVersion(ignite_version))
+
+ ignites = IgniteService(self.test_context, config=node_config, num_nodes=self.NUM_NODES - 1)
+ ignites.start()
+
+ # This client just put some data to the cache.
+ app_config = node_config._replace(client_mode=True, discovery_spi=from_ignite_cluster(ignites))
+ IgniteApplicationService(self.test_context, config=app_config,
+ java_class_name="org.apache.ignite.internal.ducktest.tests.DataGenerationApplication",
+ params={"cacheName": "test-cache", "range": self.DATA_AMOUNT},
+ startup_timeout_sec=self.PRELOAD_TIMEOUT).run()
+
+ ignite = IgniteService(self.test_context, node_config._replace(discovery_spi=from_ignite_cluster(ignites)),
+ num_nodes=1)
+
+ ignite.start()
+
+ start = self.monotonic()
+
+ ignite.await_event("rebalanced=true, wasRebalanced=false",
+ timeout_sec=AddNodeRebalanceTest.REBALANCE_TIMEOUT,
+ from_the_beginning=True,
+ backoff_sec=1)
+
+ data = {"Rebalanced in (sec)": self.monotonic() - start}
+
+ return data
diff --git a/modules/ducktests/tests/ignitetest/tests/cellular_affinity_test.py b/modules/ducktests/tests/ignitetest/tests/cellular_affinity_test.py
new file mode 100644
index 0000000000000..e6541fad685e0
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/cellular_affinity_test.py
@@ -0,0 +1,302 @@
+# 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.
+
+"""
+This module contains Cellular Affinity tests.
+"""
+import math
+from enum import IntEnum
+
+from ducktape.mark import matrix
+from jinja2 import Template
+
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.utils.control_utility import ControlUtility
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration, IgniteClientConfiguration
+from ignitetest.services.utils.ignite_configuration.discovery import from_ignite_cluster, from_zookeeper_cluster, \
+ TcpDiscoverySpi
+from ignitetest.services.zk.zookeeper import ZookeeperSettings, ZookeeperService
+from ignitetest.utils import ignite_versions, version_if, cluster
+from ignitetest.utils.enum import constructible
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, IgniteVersion, LATEST_2_8
+
+
+@constructible
+class StopType(IntEnum):
+ """
+ Node stop method type.
+ """
+ SIGTERM = 0
+ SIGKILL = 1
+ DROP_NETWORK = 2
+
+
+@constructible
+class DiscoreryType(IntEnum):
+ """
+ Discovery type.
+ """
+ ZooKeeper = 0
+ TCP = 1
+
+
+# pylint: disable=W0223
+class CellularAffinity(IgniteTest):
+ """
+ Tests Cellular Affinity scenarios.
+ """
+ NODES_PER_CELL = 3
+ ZOOKEPER_CLUSTER_SIZE = 3
+
+ FAILURE_DETECTION_TIMEOUT = 500
+ ZOOKEPER_SESSION_TIMEOUT = FAILURE_DETECTION_TIMEOUT
+
+ ATTRIBUTE = "CELL"
+
+ CACHE_NAME = "test-cache"
+
+ PREPARED_TX_CNT = 500 # possible amount at real cluster under load (per cell).
+
+ CONFIG_TEMPLATE = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """ # noqa: E501
+
+ @staticmethod
+ def properties():
+ """
+ :return: Configuration properties.
+ """
+ return Template(CellularAffinity.CONFIG_TEMPLATE) \
+ .render(
+ backups=CellularAffinity.NODES_PER_CELL, # bigger than cell capacity (to handle single cell useless test)
+ attr=CellularAffinity.ATTRIBUTE,
+ cacheName=CellularAffinity.CACHE_NAME)
+
+ @cluster(num_nodes=NODES_PER_CELL * 3 + 1)
+ @version_if(lambda version: version >= DEV_BRANCH)
+ @ignite_versions(str(DEV_BRANCH))
+ def test_distribution(self, ignite_version):
+ """
+ Tests Cellular Affinity scenario (partition distribution).
+ """
+ cell1 = self.start_cell(ignite_version, ['-D' + CellularAffinity.ATTRIBUTE + '=1'])
+
+ discovery_spi = from_ignite_cluster(cell1)
+
+ cell2 = self.start_cell(ignite_version, ['-D' + CellularAffinity.ATTRIBUTE + '=2'], discovery_spi)
+ cell3 = self.start_cell(ignite_version, ['-D' + CellularAffinity.ATTRIBUTE + '=XXX', '-DRANDOM=42'],
+ discovery_spi)
+
+ for cell in [cell1, cell2, cell3]:
+ cell.await_started()
+
+ ControlUtility(cell1, self.test_context).activate()
+
+ checker = IgniteApplicationService(
+ self.test_context,
+ IgniteClientConfiguration(version=IgniteVersion(ignite_version), discovery_spi=from_ignite_cluster(cell1)),
+ java_class_name="org.apache.ignite.internal.ducktest.tests.cellular_affinity_test.DistributionChecker",
+ params={"cacheName": CellularAffinity.CACHE_NAME,
+ "attr": CellularAffinity.ATTRIBUTE,
+ "nodesPerCell": self.NODES_PER_CELL})
+
+ checker.run()
+
+ # pylint: disable=R0912
+ # pylint: disable=R0914
+ # pylint: disable=no-member
+ @cluster(num_nodes=2 * (NODES_PER_CELL + 1) + 3) # cell_cnt * (srv_per_cell + cell_streamer) + zookeper_cluster
+ @ignite_versions(str(DEV_BRANCH), str(LATEST_2_8))
+ @matrix(stop_type=[StopType.DROP_NETWORK, StopType.SIGKILL, StopType.SIGTERM],
+ discovery_type=[DiscoreryType.ZooKeeper, DiscoreryType.TCP])
+ def test_latency(self, ignite_version, stop_type, discovery_type):
+ """
+ Tests Cellular switch tx latency.
+ """
+ cluster_size = len(self.test_context.cluster)
+
+ cells_amount = math.floor((cluster_size - self.ZOOKEPER_CLUSTER_SIZE) / (self.NODES_PER_CELL + 1))
+
+ assert cells_amount >= 2
+
+ self.test_context.logger.info(
+ "Cells amount calculated as %d at cluster with %d nodes in total" % (cells_amount, cluster_size))
+
+ data = {}
+
+ discovery_spi = None
+
+ modules = []
+
+ d_type = DiscoreryType.construct_from(discovery_type)
+
+ if d_type is DiscoreryType.ZooKeeper:
+ zk_settings = ZookeeperSettings(min_session_timeout=self.ZOOKEPER_SESSION_TIMEOUT)
+ zk_quorum = ZookeeperService(self.test_context, self.ZOOKEPER_CLUSTER_SIZE, settings=zk_settings)
+ zk_quorum.start()
+
+ modules.append('zookeeper')
+
+ discovery_spi = from_zookeeper_cluster(zk_quorum)
+
+ cell0, prepared_tx_loader1 = self.start_cell_with_prepared_txs(ignite_version, "C0", discovery_spi, modules)
+
+ if d_type is DiscoreryType.TCP:
+ discovery_spi = from_ignite_cluster(cell0)
+
+ assert discovery_spi is not None
+
+ loaders = [prepared_tx_loader1]
+ nodes = [cell0]
+
+ for cell in range(1, cells_amount):
+ node, prepared_tx_loader = \
+ self.start_cell_with_prepared_txs(ignite_version, "C%d" % cell, discovery_spi, modules)
+
+ loaders.append(prepared_tx_loader)
+ nodes.append(node)
+
+ failed_loader = loaders[1]
+
+ for node in [*nodes, *loaders]:
+ node.await_started()
+
+ streamers = []
+
+ for cell in range(0, cells_amount):
+ streamers.append(self.start_tx_streamer(ignite_version, "C%d" % cell, discovery_spi, modules))
+
+ for streamer in streamers: # starts tx streaming with latency record (with some warmup).
+ streamer.start_async()
+
+ for streamer in streamers:
+ streamer.await_started()
+
+ ControlUtility(cell0, self.test_context).disable_baseline_auto_adjust() # baseline set.
+ ControlUtility(cell0, self.test_context).activate()
+
+ for loader in loaders:
+ loader.await_event("ALL_TRANSACTIONS_PREPARED", 180, from_the_beginning=True)
+
+ for streamer in streamers:
+ streamer.await_event("WARMUP_FINISHED", 180, from_the_beginning=True)
+
+ # node left with prepared txs.
+ with StopType.construct_from(stop_type) as s_type:
+ if s_type is StopType.SIGTERM:
+ failed_loader.stop_async()
+ elif s_type is StopType.SIGKILL:
+ failed_loader.kill()
+ elif s_type is StopType.DROP_NETWORK:
+ failed_loader.drop_network()
+
+ for streamer in streamers:
+ streamer.await_event("Node left topology\\|Node FAILED", 60, from_the_beginning=True)
+
+ for streamer in streamers: # just an assertion that we have PME-free switch.
+ streamer.await_event("exchangeFreeSwitch=true", 60, from_the_beginning=True)
+
+ for streamer in streamers: # waiting for streaming continuation.
+ streamer.await_event("APPLICATION_STREAMED", 60)
+
+ for streamer in streamers: # stops streaming and records results.
+ streamer.stop_async()
+
+ for streamer in streamers:
+ streamer.await_stopped()
+
+ cell = streamer.params["cell"]
+
+ data["[%s cell %s]" % ("alive" if cell != failed_loader.params["cell"] else "broken", cell)] = \
+ "worst_latency=%s, tx_streamed=%s, measure_duration=%s" % (
+ streamer.extract_result("WORST_LATENCY"), streamer.extract_result("STREAMED"),
+ streamer.extract_result("MEASURE_DURATION"))
+
+ return data
+
+ def start_tx_streamer(self, version, cell, discovery_spi, modules):
+ """
+ Starts transaction streamer.
+ """
+ return IgniteApplicationService(
+ self.test_context,
+ IgniteClientConfiguration(version=IgniteVersion(version), properties=self.properties(),
+ discovery_spi=discovery_spi),
+ java_class_name="org.apache.ignite.internal.ducktest.tests.cellular_affinity_test.CellularTxStreamer",
+ params={"cacheName": CellularAffinity.CACHE_NAME,
+ "attr": CellularAffinity.ATTRIBUTE,
+ "cell": cell,
+ "warmup": 10000},
+ modules=modules, startup_timeout_sec=180)
+
+ def start_cell_with_prepared_txs(self, version, cell_id, discovery_spi, modules):
+ """
+ Starts cell with prepared transactions.
+ """
+ nodes = self.start_cell(version, ['-D' + CellularAffinity.ATTRIBUTE + '=' + cell_id], discovery_spi, modules,
+ CellularAffinity.NODES_PER_CELL - 1)
+
+ prepared_tx_streamer = IgniteApplicationService( # last server node at the cell.
+ self.test_context,
+ IgniteConfiguration(version=IgniteVersion(version), properties=self.properties(),
+ failure_detection_timeout=self.FAILURE_DETECTION_TIMEOUT,
+ discovery_spi=from_ignite_cluster(nodes) if discovery_spi is None else discovery_spi),
+ java_class_name="org.apache.ignite.internal.ducktest.tests.cellular_affinity_test."
+ "CellularPreparedTxStreamer",
+ params={"cacheName": CellularAffinity.CACHE_NAME,
+ "attr": CellularAffinity.ATTRIBUTE,
+ "cell": cell_id,
+ "txCnt": CellularAffinity.PREPARED_TX_CNT},
+ jvm_opts=['-D' + CellularAffinity.ATTRIBUTE + '=' + cell_id], modules=modules, startup_timeout_sec=180)
+
+ prepared_tx_streamer.start_async() # starts last server node and creates prepared txs on it.
+
+ return nodes, prepared_tx_streamer
+
+ # pylint: disable=R0913
+ def start_cell(self, version, jvm_opts, discovery_spi=None, modules=None, nodes_cnt=NODES_PER_CELL):
+ """
+ Starts cell.
+ """
+ ignites = IgniteService(
+ self.test_context,
+ IgniteConfiguration(version=IgniteVersion(version), properties=self.properties(),
+ cluster_state="INACTIVE",
+ failure_detection_timeout=self.FAILURE_DETECTION_TIMEOUT,
+ discovery_spi=TcpDiscoverySpi() if discovery_spi is None else discovery_spi),
+ num_nodes=nodes_cnt, modules=modules, jvm_opts=jvm_opts, startup_timeout_sec=180)
+
+ ignites.start_async()
+
+ return ignites
diff --git a/modules/ducktests/tests/ignitetest/tests/client_test.py b/modules/ducktests/tests/ignitetest/tests/client_test.py
new file mode 100644
index 0000000000000..28b1213527df1
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/client_test.py
@@ -0,0 +1,159 @@
+# 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.
+
+"""
+This module contains client tests
+"""
+import time
+
+from ducktape.mark import parametrize
+
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.utils.control_utility import ControlUtility
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration
+from ignitetest.services.utils.ignite_configuration.cache import CacheConfiguration
+from ignitetest.utils import ignite_versions, cluster
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, LATEST, IgniteVersion
+
+
+# pylint: disable=W0223
+class ClientTest(IgniteTest):
+ """
+ cluster - cluster size
+ CACHE_NAME - name of the cache to create for the test.
+ PACING - the frequency of the operation on clients (ms).
+ JAVA_CLIENT_CLASS_NAME - running classname.
+ client_work_time - clients working time (s).
+ iteration_count - the number of iterations of starting and stopping client nodes (s).
+ static_clients - the number of permanently employed clients.
+ temp_client - number of clients who come log in and out.
+ """
+
+ CACHE_NAME = "simple-tx-cache"
+ PACING = 10
+ JAVA_CLIENT_CLASS_NAME = "org.apache.ignite.internal.ducktest.tests.client_test.IgniteCachePutClient"
+
+ # pylint: disable=R0913
+ @cluster(num_nodes=7)
+ @ignite_versions(str(LATEST), str(DEV_BRANCH))
+ @parametrize(num_nodes=7, static_clients=2, temp_client=3, iteration_count=3, client_work_time=30)
+ def test_ignite_start_stop_nodes(self, ignite_version, num_nodes, static_clients, temp_client, iteration_count,
+ client_work_time):
+ """
+ Start and stop clients node test without kill java process.
+ Check topology.
+ """
+ self.ignite_start_stop(ignite_version, True, num_nodes, static_clients,
+ temp_client, iteration_count, client_work_time)
+
+ # pylint: disable=R0913
+ @cluster(num_nodes=7)
+ @ignite_versions(str(LATEST), str(DEV_BRANCH))
+ @parametrize(num_nodes=7, static_clients=2, temp_client=3, iteration_count=3, client_work_time=30)
+ def test_ignite_kill_start_nodes(self, ignite_version, num_nodes, static_clients, temp_client, iteration_count,
+ client_work_time):
+ """
+ Start and kill client nodes, Check topology
+ """
+ self.ignite_start_stop(ignite_version, False, num_nodes, static_clients,
+ temp_client, iteration_count, client_work_time)
+
+ # pylint: disable=R0914
+ # pylint: disable=R0913
+ def ignite_start_stop(self, ignite_version, graceful_shutdown, nodes_num, static_clients_num, temp_client,
+ iteration_count, client_work_time):
+ """
+ Test for starting and stopping fat clients.
+ """
+
+ servers_count = nodes_num - static_clients_num - temp_client
+ current_top_v = servers_count
+
+ # Topology version after test.
+ fin_top_ver = servers_count + (2 * static_clients_num) + (2 * iteration_count * temp_client)
+
+ server_cfg = IgniteConfiguration(version=IgniteVersion(ignite_version), caches=[
+ CacheConfiguration(name=self.CACHE_NAME, backups=1, atomicity_mode='TRANSACTIONAL')])
+
+ ignite = IgniteService(self.test_context, server_cfg, num_nodes=servers_count)
+
+ control_utility = ControlUtility(ignite, self.test_context)
+
+ client_cfg = server_cfg._replace(client_mode=True)
+
+ static_clients = IgniteApplicationService(self.test_context, client_cfg,
+ java_class_name=self.JAVA_CLIENT_CLASS_NAME,
+ num_nodes=static_clients_num,
+ params={"cacheName": self.CACHE_NAME, "pacing": self.PACING})
+
+ temp_clients = IgniteApplicationService(self.test_context, client_cfg,
+ java_class_name=self.JAVA_CLIENT_CLASS_NAME, num_nodes=temp_client,
+ params={"cacheName": self.CACHE_NAME, "pacing": self.PACING})
+
+ ignite.start()
+
+ static_clients.start()
+
+ current_top_v += static_clients_num
+
+ check_topology(control_utility, current_top_v)
+
+ # Start / stop temp_clients node. Check cluster.
+ for i in range(iteration_count):
+ self.logger.info(f'Starting iteration: {i}.')
+
+ temp_clients.start()
+
+ current_top_v += temp_client
+
+ await_event(static_clients, f'ver={current_top_v}, locNode=')
+
+ check_topology(control_utility, current_top_v)
+
+ await_event(temp_clients, f'clients={static_clients_num + temp_client}')
+
+ time.sleep(client_work_time)
+
+ if graceful_shutdown:
+ temp_clients.stop()
+ else:
+ temp_clients.kill()
+
+ current_top_v += temp_client
+
+ await_event(static_clients, f'ver={current_top_v}, locNode=')
+
+ static_clients.stop()
+
+ check_topology(control_utility, fin_top_ver)
+
+
+def await_event(service: IgniteApplicationService, message):
+ """
+ :param service: target service for wait
+ :param message: message
+ """
+ service.await_event(message, timeout_sec=80, from_the_beginning=True)
+
+
+def check_topology(control_utility: ControlUtility, fin_top_ver: int):
+ """
+ Check current topology version.
+ """
+ top_ver = control_utility.cluster_state().topology_version
+ assert top_ver == fin_top_ver, f'Cluster current topology version={top_ver}, ' \
+ f'expected topology version={fin_top_ver}.'
diff --git a/modules/ducktests/tests/ignitetest/tests/control_utility/__init__.py b/modules/ducktests/tests/ignitetest/tests/control_utility/__init__.py
new file mode 100644
index 0000000000000..1540a34610ca5
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/control_utility/__init__.py
@@ -0,0 +1,18 @@
+# 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.
+
+"""
+This package contains control.sh utility tests.
+"""
diff --git a/modules/ducktests/tests/ignitetest/tests/control_utility/baseline_test.py b/modules/ducktests/tests/ignitetest/tests/control_utility/baseline_test.py
new file mode 100644
index 0000000000000..8951006e238f0
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/control_utility/baseline_test.py
@@ -0,0 +1,206 @@
+# 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.
+
+"""
+This module contains manipulating baseline test through control utility.
+"""
+
+from ducktape.utils.util import wait_until
+
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.utils.control_utility import ControlUtility, ControlUtilityError
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration, DataStorageConfiguration
+from ignitetest.services.utils.ignite_configuration.data_storage import DataRegionConfiguration
+from ignitetest.services.utils.ignite_configuration.discovery import from_ignite_cluster
+from ignitetest.utils import version_if, ignite_versions, cluster
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, LATEST, IgniteVersion, V_2_8_0
+
+
+# pylint: disable=W0223
+class BaselineTests(IgniteTest):
+ """
+ Tests baseline command
+ """
+ NUM_NODES = 3
+
+ @cluster(num_nodes=NUM_NODES)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ def test_baseline_set(self, ignite_version):
+ """
+ Test baseline set.
+ """
+ blt_size = self.NUM_NODES - 2
+ servers = self.__start_ignite_nodes(ignite_version, blt_size)
+
+ control_utility = ControlUtility(servers, self.test_context)
+ control_utility.activate()
+
+ # Check baseline of activated cluster.
+ baseline = control_utility.baseline()
+ self.__check_baseline_size(baseline, blt_size)
+ self.__check_nodes_in_baseline(servers.nodes, baseline)
+
+ # Set baseline using list of consisttent ids.
+ new_node = self.__start_ignite_nodes(ignite_version, 1, join_cluster=servers)
+ control_utility.set_baseline(servers.nodes + new_node.nodes)
+ blt_size += 1
+
+ baseline = control_utility.baseline()
+ self.__check_baseline_size(baseline, blt_size)
+ self.__check_nodes_in_baseline(new_node.nodes, baseline)
+
+ # Set baseline using topology version.
+ new_node = self.__start_ignite_nodes(ignite_version, 1, join_cluster=servers)
+ _, version, _ = control_utility.cluster_state()
+ control_utility.set_baseline(version)
+ blt_size += 1
+
+ baseline = control_utility.baseline()
+ self.__check_baseline_size(baseline, blt_size)
+ self.__check_nodes_in_baseline(new_node.nodes, baseline)
+
+ @cluster(num_nodes=NUM_NODES)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ def test_baseline_add_remove(self, ignite_version):
+ """
+ Test add and remove nodes from baseline.
+ """
+ blt_size = self.NUM_NODES - 1
+ servers = self.__start_ignite_nodes(ignite_version, blt_size)
+
+ control_utility = ControlUtility(servers, self.test_context)
+
+ control_utility.activate()
+
+ # Add node to baseline.
+ new_node = self.__start_ignite_nodes(ignite_version, 1, join_cluster=servers)
+ control_utility.add_to_baseline(new_node.nodes)
+ blt_size += 1
+
+ baseline = control_utility.baseline()
+ self.__check_baseline_size(baseline, blt_size)
+ self.__check_nodes_in_baseline(new_node.nodes, baseline)
+
+ # Expected failure (remove of online node is not allowed).
+ try:
+ control_utility.remove_from_baseline(new_node.nodes)
+
+ assert False, "Remove of online node from baseline should fail!"
+ except ControlUtilityError:
+ pass
+
+ # Remove of offline node from baseline.
+ new_node.stop()
+
+ servers.await_event("Node left topology", timeout_sec=30, from_the_beginning=True)
+
+ control_utility.remove_from_baseline(new_node.nodes)
+ blt_size -= 1
+
+ baseline = control_utility.baseline()
+ self.__check_baseline_size(baseline, blt_size)
+ self.__check_nodes_not_in_baseline(new_node.nodes, baseline)
+
+ @cluster(num_nodes=NUM_NODES)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ def test_activate_deactivate(self, ignite_version):
+ """
+ Test activate and deactivate cluster.
+ """
+ servers = self.__start_ignite_nodes(ignite_version, self.NUM_NODES)
+
+ control_utility = ControlUtility(servers, self.test_context)
+
+ control_utility.activate()
+
+ state, _, _ = control_utility.cluster_state()
+
+ assert state.lower() == 'active', 'Unexpected state %s' % state
+
+ control_utility.deactivate()
+
+ state, _, _ = control_utility.cluster_state()
+
+ assert state.lower() == 'inactive', 'Unexpected state %s' % state
+
+ @cluster(num_nodes=NUM_NODES)
+ @version_if(lambda version: version >= V_2_8_0)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ def test_baseline_autoadjust(self, ignite_version):
+ """
+ Test activate and deactivate cluster.
+ """
+ blt_size = self.NUM_NODES - 2
+ servers = self.__start_ignite_nodes(ignite_version, blt_size)
+
+ control_utility = ControlUtility(servers, self.test_context)
+ control_utility.activate()
+
+ # Add node.
+ control_utility.enable_baseline_auto_adjust(2000)
+ new_node = self.__start_ignite_nodes(ignite_version, 1, join_cluster=servers)
+ blt_size += 1
+
+ wait_until(lambda: len(control_utility.baseline()) == blt_size, timeout_sec=5)
+
+ baseline = control_utility.baseline()
+ self.__check_nodes_in_baseline(new_node.nodes, baseline)
+
+ # Add node when auto adjust disabled.
+ control_utility.disable_baseline_auto_adjust()
+ old_topology = control_utility.cluster_state().topology_version
+ new_node = self.__start_ignite_nodes(ignite_version, 1, join_cluster=servers)
+
+ wait_until(lambda: control_utility.cluster_state().topology_version != old_topology, timeout_sec=5)
+ baseline = control_utility.baseline()
+ self.__check_nodes_not_in_baseline(new_node.nodes, baseline)
+
+ @staticmethod
+ def __check_nodes_in_baseline(nodes, baseline):
+ blset = set(node.consistent_id for node in baseline)
+
+ for node in nodes:
+ assert node.consistent_id in blset
+
+ @staticmethod
+ def __check_nodes_not_in_baseline(nodes, baseline):
+ blset = set(node.consistent_id for node in baseline)
+
+ for node in nodes:
+ assert node.consistent_id not in blset
+
+ @staticmethod
+ def __check_baseline_size(baseline, size):
+ assert len(baseline) == size, 'Unexpected size of baseline %d, %d expected' % (len(baseline), size)
+
+ def __start_ignite_nodes(self, version, num_nodes, timeout_sec=60, join_cluster=None):
+ config = IgniteConfiguration(
+ cluster_state="INACTIVE",
+ version=IgniteVersion(version),
+ data_storage=DataStorageConfiguration(
+ default=DataRegionConfiguration(name='persistent', persistent=True),
+ regions=[DataRegionConfiguration(name='in-memory', persistent=False, max_size=100 * 1024 * 1024)]
+ )
+ )
+
+ if join_cluster:
+ config._replace(discovery_spi=from_ignite_cluster(join_cluster))
+
+ servers = IgniteService(self.test_context, config=config, num_nodes=num_nodes, startup_timeout_sec=timeout_sec)
+
+ servers.start()
+
+ return servers
diff --git a/modules/ducktests/tests/ignitetest/tests/control_utility/tx_test.py b/modules/ducktests/tests/ignitetest/tests/control_utility/tx_test.py
new file mode 100644
index 0000000000000..bc059e5b35202
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/control_utility/tx_test.py
@@ -0,0 +1,191 @@
+
+# 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.
+
+"""
+This module contains transactions manipulation test through control utility.
+"""
+
+import random
+
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.utils.control_utility import ControlUtility
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration
+from ignitetest.services.utils.ignite_configuration.cache import CacheConfiguration
+from ignitetest.services.utils.ignite_configuration.discovery import from_ignite_cluster
+from ignitetest.utils import ignite_versions, cluster
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, LATEST, IgniteVersion
+
+
+# pylint: disable=W0223
+class TransactionsTests(IgniteTest):
+ """
+ Tests control.sh transaction management command.
+ """
+ NUM_NODES = 4
+ CACHE_NAME = "TEST"
+
+ @cluster(num_nodes=NUM_NODES)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ def test_tx_info(self, ignite_version):
+ """
+ Tests verbose tx info for specific xid.
+ """
+ servers = self.__start_ignite_nodes(ignite_version, self.NUM_NODES - 2)
+
+ long_tx = self.__start_tx_app(ignite_version, servers, cache_name=self.CACHE_NAME, tx_count=2, tx_size=2,
+ key_prefix='TX_1_KEY_')
+
+ wait_for_key_locked(long_tx)
+
+ control_utility = ControlUtility(servers, self.test_context)
+
+ transactions = control_utility.tx()
+
+ pick_tx = random.choice(transactions)
+
+ res = control_utility.tx_info(pick_tx.xid)
+
+ assert res.xid == pick_tx.xid
+ assert res.timeout == pick_tx.timeout
+ assert res.top_ver == pick_tx.top_ver
+ assert res.label == pick_tx.label
+
+ @cluster(num_nodes=NUM_NODES)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ def test_kill_tx(self, ignite_version):
+ """
+ Test kill transactions by xid and filter.
+ """
+ servers = self.__start_ignite_nodes(ignite_version, self.NUM_NODES - 2)
+
+ tx_count = 3
+
+ long_tx_1 = self.__start_tx_app(ignite_version, servers, cache_name=self.CACHE_NAME, tx_count=tx_count,
+ tx_size=2, key_prefix='TX_1_KEY_', label='TX_1', wait_for_topology_version=4)
+
+ long_tx_2 = self.__start_tx_app(ignite_version, servers, cache_name=self.CACHE_NAME, tx_count=tx_count,
+ tx_size=2, key_prefix='TX_2_KEY_', label='TX_2', wait_for_topology_version=4)
+
+ wait_for_key_locked(long_tx_1, long_tx_2)
+
+ control_utility = ControlUtility(servers, self.test_context)
+
+ # check kill with specific xid.
+ transactions = control_utility.tx(label_pattern='TX_1')
+ res = control_utility.tx_kill(xid=random.choice(transactions).xid)
+ assert res and len(res) == 1 and res[0].xid == long_tx_1.extract_result("TX_ID")
+
+ # check kill with filter.
+ res = control_utility.tx_kill(label_pattern='TX_2')
+ assert res and len(res) == tx_count and set(map(lambda x: x.xid, res))\
+ .issubset(set(long_tx_2.extract_results("TX_ID")))
+
+ @cluster(num_nodes=NUM_NODES)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ def test_tx_filter(self, ignite_version):
+ """
+ Test filtering transactions list.
+ """
+ servers = self.__start_ignite_nodes(ignite_version, self.NUM_NODES - 2)
+
+ client_tx_count, client_tx_size = 5, 4
+ server_tx_count, server_tx_size = 3, 2
+
+ servers = self.__start_tx_app(ignite_version, servers, client_mode=False, cache_name=self.CACHE_NAME,
+ tx_count=server_tx_count, tx_size=server_tx_size, key_prefix='TX_1_KEY_',
+ label='LBL_SERVER', wait_for_topology_version=4)
+
+ clients = self.__start_tx_app(ignite_version, servers, cache_name=self.CACHE_NAME, tx_count=client_tx_count,
+ tx_size=client_tx_size, key_prefix='TX_2_KEY_', label='LBL_CLIENT',
+ wait_for_topology_version=4)
+
+ wait_for_key_locked(clients, servers)
+ control_utility = ControlUtility(servers, self.test_context)
+
+ start_check = self.monotonic()
+ assert len(control_utility.tx(clients=True, label_pattern='LBL_.*')) == client_tx_count
+ assert len(control_utility.tx(servers=True, label_pattern='LBL_.*')) == server_tx_count
+
+ # limit to 2 transactions on each node, therefore 4 total.
+ assert len(control_utility.tx(limit=2, label_pattern='LBL_.*')) == 4
+
+ assert len(control_utility.tx(label_pattern='LBL_.*')) == client_tx_count + server_tx_count
+
+ # filter transactions with keys size greater or equal to min_size.
+ assert len(control_utility.tx(min_size=client_tx_size, label_pattern='LBL_.*')) == client_tx_count
+
+ server_nodes = [node.consistent_id for node in servers.nodes]
+ assert len(control_utility.tx(label_pattern='LBL_.*', nodes=server_nodes)) == server_tx_count
+
+ # test ordering.
+ for order_type in ['DURATION', 'SIZE', 'START_TIME']:
+ transactions = control_utility.tx(label_pattern='LBL_.*', order=order_type)
+ assert is_sorted(transactions, key=lambda x, attr=order_type: getattr(x, attr.lower()), reverse=True)
+
+ # test min_duration filtering.
+ min_duration = int(self.monotonic() - start_check)
+ transactions = control_utility.tx(min_duration=min_duration, label_pattern='LBL_.*')
+ assert len(transactions) == server_tx_count + client_tx_count
+ for tx in transactions:
+ assert tx.duration >= min_duration
+
+ def __start_tx_app(self, version, servers, *, client_mode=True, **kwargs):
+ app_params = {
+ 'config': IgniteConfiguration(version=IgniteVersion(version),
+ client_mode=client_mode,
+ discovery_spi=from_ignite_cluster(servers)),
+ 'java_class_name': 'org.apache.ignite.internal.ducktest.tests.control_utility'
+ '.LongRunningTransactionsGenerator',
+ 'params': kwargs
+ }
+
+ app = IgniteApplicationService(self.test_context, **app_params)
+ app.start()
+
+ return app
+
+ def __start_ignite_nodes(self, version, num_nodes, timeout_sec=60):
+ config = IgniteConfiguration(
+ cluster_state="ACTIVE",
+ version=IgniteVersion(version),
+ caches=[CacheConfiguration(name=self.CACHE_NAME, atomicity_mode='TRANSACTIONAL')]
+ )
+
+ servers = IgniteService(self.test_context, config=config, num_nodes=num_nodes, startup_timeout_sec=timeout_sec)
+
+ servers.start()
+
+ return servers
+
+
+def wait_for_key_locked(*clusters):
+ """
+ Wait for APPLICATION_KEYS_LOCKED on tx_app nodes.
+ """
+ for cluster_ in clusters:
+ cluster_.await_event("APPLICATION_KEYS_LOCKED", timeout_sec=60, from_the_beginning=True)
+
+
+def is_sorted(lst, key=lambda x: x, reverse=False):
+ """
+ Check if list is sorted.
+ """
+ for i, elem in enumerate(lst[1:]):
+ return key(elem) <= key(lst[i]) if not reverse else key(elem) >= key(lst[i])
+
+ return True
diff --git a/modules/ducktests/tests/ignitetest/tests/discovery_test.py b/modules/ducktests/tests/ignitetest/tests/discovery_test.py
new file mode 100644
index 0000000000000..eb0d250110b25
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/discovery_test.py
@@ -0,0 +1,340 @@
+# 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.
+
+"""
+Module contains discovery tests.
+"""
+
+import os
+import random
+import sys
+from enum import IntEnum
+from time import monotonic
+from typing import NamedTuple
+
+from ducktape.mark import matrix
+
+from ignitetest.services.ignite import IgniteAwareService, IgniteService, get_event_time, node_failed_event_pattern
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration
+from ignitetest.services.utils.ignite_configuration.cache import CacheConfiguration
+from ignitetest.services.utils.ignite_configuration.discovery import from_zookeeper_cluster, from_ignite_cluster, \
+ TcpDiscoverySpi
+from ignitetest.services.utils.time_utils import epoch_mills
+from ignitetest.services.zk.zookeeper import ZookeeperService, ZookeeperSettings
+from ignitetest.utils import ignite_versions, version_if, cluster
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, LATEST, V_2_8_0, IgniteVersion
+from ignitetest.utils.enum import constructible
+
+
+@constructible
+class ClusterLoad(IntEnum):
+ """
+ Type of cluster loading.
+ """
+ NONE = 0
+ ATOMIC = 1
+ TRANSACTIONAL = 2
+
+
+# pylint: disable=R0913
+class DiscoveryTestConfig(NamedTuple):
+ """
+ Configuration for DiscoveryTest.
+ """
+ version: IgniteVersion
+ nodes_to_kill: int = 1
+ load_type: ClusterLoad = ClusterLoad.NONE
+ sequential_failure: bool = False
+ with_zk: bool = False
+ failure_detection_timeout: int = 1000
+
+
+# pylint: disable=W0223, no-member
+class DiscoveryTest(IgniteTest):
+ """
+ Test various node failure scenarios (TCP and ZooKeeper).
+ 1. Start of ignite cluster.
+ 2. Kill random node.
+ 3. Wait that survived node detects node failure.
+ """
+ MAX_CONTAINERS = 12
+
+ ZOOKEEPER_NODES = 3
+
+ DATA_AMOUNT = 5_000_000
+
+ WARMUP_DATA_AMOUNT = 10_000
+
+ FAILURE_TIMEOUT = 800
+
+ def __init__(self, test_context):
+ super().__init__(test_context=test_context)
+
+ self.netfilter_store_path = None
+
+ @cluster(num_nodes=MAX_CONTAINERS)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ @matrix(nodes_to_kill=[1, 2], failure_detection_timeout=[FAILURE_TIMEOUT],
+ load_type=[ClusterLoad.NONE, ClusterLoad.ATOMIC, ClusterLoad.TRANSACTIONAL])
+ def test_nodes_fail_not_sequential_tcp(self, ignite_version, nodes_to_kill, load_type, failure_detection_timeout):
+ """
+ Test nodes failure scenario with TcpDiscoverySpi not allowing nodes to fail in a row.
+ """
+ test_config = DiscoveryTestConfig(version=IgniteVersion(ignite_version), nodes_to_kill=nodes_to_kill,
+ load_type=ClusterLoad.construct_from(load_type), sequential_failure=False,
+ failure_detection_timeout=failure_detection_timeout)
+
+ return self._perform_node_fail_scenario(test_config)
+
+ @cluster(num_nodes=MAX_CONTAINERS)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ @matrix(load_type=[ClusterLoad.NONE, ClusterLoad.ATOMIC, ClusterLoad.TRANSACTIONAL],
+ failure_detection_timeout=[FAILURE_TIMEOUT])
+ def test_2_nodes_fail_sequential_tcp(self, ignite_version, load_type, failure_detection_timeout):
+ """
+ Test 2 nodes sequential failure scenario with TcpDiscoverySpi.
+ """
+ test_config = DiscoveryTestConfig(version=IgniteVersion(ignite_version), nodes_to_kill=2,
+ load_type=ClusterLoad.construct_from(load_type), sequential_failure=True,
+ failure_detection_timeout=failure_detection_timeout)
+
+ return self._perform_node_fail_scenario(test_config)
+
+ @cluster(num_nodes=MAX_CONTAINERS)
+ @version_if(lambda version: version != V_2_8_0) # ignite-zookeeper package is broken in 2.8.0
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ @matrix(nodes_to_kill=[1, 2], failure_detection_timeout=[FAILURE_TIMEOUT],
+ load_type=[ClusterLoad.NONE, ClusterLoad.ATOMIC, ClusterLoad.TRANSACTIONAL])
+ def test_nodes_fail_not_sequential_zk(self, ignite_version, nodes_to_kill, load_type, failure_detection_timeout):
+ """
+ Test node failure scenario with ZooKeeperSpi not allowing nodes to fail in a row.
+ """
+ test_config = DiscoveryTestConfig(version=IgniteVersion(ignite_version), nodes_to_kill=nodes_to_kill,
+ load_type=ClusterLoad.construct_from(load_type), sequential_failure=False,
+ with_zk=True, failure_detection_timeout=failure_detection_timeout)
+
+ return self._perform_node_fail_scenario(test_config)
+
+ @cluster(num_nodes=MAX_CONTAINERS)
+ @version_if(lambda version: version != V_2_8_0) # ignite-zookeeper package is broken in 2.8.0
+ @ignite_versions(str(DEV_BRANCH), str(LATEST))
+ @matrix(load_type=[ClusterLoad.NONE, ClusterLoad.ATOMIC, ClusterLoad.TRANSACTIONAL],
+ failure_detection_timeout=[FAILURE_TIMEOUT])
+ def test_2_nodes_fail_sequential_zk(self, ignite_version, load_type, failure_detection_timeout):
+ """
+ Test node failure scenario with ZooKeeperSpi not allowing to fail nodes in a row.
+ """
+ test_config = DiscoveryTestConfig(version=IgniteVersion(ignite_version), nodes_to_kill=2,
+ load_type=ClusterLoad.construct_from(load_type), sequential_failure=True,
+ with_zk=True, failure_detection_timeout=failure_detection_timeout)
+
+ return self._perform_node_fail_scenario(test_config)
+
+ def _perform_node_fail_scenario(self, test_config):
+ max_containers = len(self.test_context.cluster)
+
+ # One node is required to detect the failure.
+ assert max_containers >= 1 + test_config.nodes_to_kill + (
+ DiscoveryTest.ZOOKEEPER_NODES if test_config.with_zk else 0) + (
+ 0 if test_config.load_type == ClusterLoad.NONE else 1), "Few required containers: " + \
+ str(max_containers) + ". Check the params."
+
+ self.logger.info("Starting on " + str(max_containers) + " maximal containers.")
+
+ results = {}
+
+ modules = ['zookeeper'] if test_config.with_zk else None
+
+ if test_config.with_zk:
+ zk_quorum = start_zookeeper(self.test_context, DiscoveryTest.ZOOKEEPER_NODES, test_config)
+
+ discovery_spi = from_zookeeper_cluster(zk_quorum)
+ else:
+ discovery_spi = TcpDiscoverySpi()
+
+ ignite_config = IgniteConfiguration(
+ version=test_config.version,
+ discovery_spi=discovery_spi,
+ failure_detection_timeout=test_config.failure_detection_timeout,
+ caches=[CacheConfiguration(
+ name='test-cache',
+ backups=1,
+ atomicity_mode='TRANSACTIONAL' if test_config.load_type == ClusterLoad.TRANSACTIONAL else 'ATOMIC'
+ )]
+ )
+
+ # Start Ignite nodes in count less than max_nodes_in_use. One node is erequired for the loader. Some nodes might
+ # be needed for ZooKeeper.
+ servers, start_servers_sec = start_servers(
+ self.test_context, max_containers - DiscoveryTest.ZOOKEEPER_NODES - 1, ignite_config, modules)
+
+ results['Ignite cluster start time (s)'] = start_servers_sec
+
+ failed_nodes = choose_node_to_kill(servers, test_config.nodes_to_kill, test_config.sequential_failure)
+
+ if test_config.load_type is not ClusterLoad.NONE:
+ load_config = ignite_config._replace(client_mode=True) if test_config.with_zk else \
+ ignite_config._replace(client_mode=True, discovery_spi=from_ignite_cluster(servers))
+
+ tran_nodes = [node_id(n) for n in failed_nodes] \
+ if test_config.load_type == ClusterLoad.TRANSACTIONAL else None
+
+ params = {"cacheName": "test-cache",
+ "range": self.DATA_AMOUNT,
+ "warmUpRange": self.WARMUP_DATA_AMOUNT,
+ "targetNodes": tran_nodes,
+ "transactional": bool(tran_nodes)}
+
+ start_load_app(self.test_context, ignite_config=load_config, params=params, modules=modules)
+
+ results.update(self._simulate_and_detect_failure(servers, failed_nodes,
+ test_config.failure_detection_timeout * 4))
+
+ return results
+
+ def _simulate_and_detect_failure(self, servers, failed_nodes, timeout):
+ """
+ Perform node failure scenario
+ """
+ for node in failed_nodes:
+ self.logger.info(
+ "Simulating failure of node '%s' (order %d) on '%s'" % (node_id(node), order(node), node.name))
+
+ ids_to_wait = [node_id(n) for n in failed_nodes]
+
+ _, first_terminated = servers.drop_network(failed_nodes)
+
+ # Keeps dates of logged node failures.
+ logged_timestamps = []
+ data = {}
+
+ start = monotonic()
+
+ for survivor in [n for n in servers.nodes if n not in failed_nodes]:
+ for failed_id in ids_to_wait:
+ logged_timestamps.append(get_event_time(servers, survivor, node_failed_event_pattern(failed_id),
+ timeout=start + timeout - monotonic()))
+
+ self._check_failed_number(failed_nodes, survivor)
+
+ self._check_not_segmented(failed_nodes)
+
+ logged_timestamps.sort(reverse=True)
+
+ data['Detection of node(s) failure (ms)'] = epoch_mills(logged_timestamps[0]) - epoch_mills(first_terminated)
+ data['All detection delays (ms):'] = str(
+ [epoch_mills(ts) - epoch_mills(first_terminated) for ts in logged_timestamps])
+ data['Nodes failed'] = len(failed_nodes)
+
+ return data
+
+ def _check_failed_number(self, failed_nodes, survived_node):
+ """Ensures number of failed nodes is correct."""
+ cmd = "grep '%s' %s | wc -l" % (node_failed_event_pattern(), IgniteAwareService.STDOUT_STDERR_CAPTURE)
+
+ failed_cnt = int(str(survived_node.account.ssh_client.exec_command(cmd)[1].read(), sys.getdefaultencoding()))
+
+ if failed_cnt != len(failed_nodes):
+ failed = str(survived_node.account.ssh_client.exec_command(
+ "grep '%s' %s" % (node_failed_event_pattern(), IgniteAwareService.STDOUT_STDERR_CAPTURE))[1].read(),
+ sys.getdefaultencoding())
+
+ self.logger.warn("Node '%s' (%s) has detected the following failures:%s%s" % (
+ survived_node.name, node_id(survived_node), os.linesep, failed))
+
+ raise AssertionError(
+ "Wrong number of failed nodes: %d. Expected: %d. Check the logs." % (failed_cnt, len(failed_nodes)))
+
+ def _check_not_segmented(self, failed_nodes):
+ """Ensures only target nodes failed"""
+ for service in [srv for srv in self.test_context.services if isinstance(srv, IgniteAwareService)]:
+ for node in [srv_node for srv_node in service.nodes if srv_node not in failed_nodes]:
+ cmd = "grep -i '%s' %s | wc -l" % ("local node segmented", IgniteAwareService.STDOUT_STDERR_CAPTURE)
+
+ failed = str(node.account.ssh_client.exec_command(cmd)[1].read(), sys.getdefaultencoding())
+
+ if int(failed) > 0:
+ raise AssertionError(
+ "Wrong node failed (segmented) on '%s'. Check the logs." % node.name)
+
+
+def start_zookeeper(test_context, num_nodes, test_config):
+ """
+ Start zookeeper cluster.
+ """
+ zk_settings = ZookeeperSettings(min_session_timeout=test_config.failure_detection_timeout)
+
+ zk_quorum = ZookeeperService(test_context, num_nodes, settings=zk_settings)
+ zk_quorum.start()
+ return zk_quorum
+
+
+def start_servers(test_context, num_nodes, ignite_config, modules=None):
+ """
+ Start ignite servers.
+ """
+ servers = IgniteService(test_context, config=ignite_config, num_nodes=num_nodes, modules=modules,
+ # mute spam in log.
+ jvm_opts=["-DIGNITE_DUMP_THREADS_ON_FAILURE=false"],
+ startup_timeout_sec=100)
+
+ start = monotonic()
+ servers.start()
+ return servers, round(monotonic() - start, 1)
+
+
+def start_load_app(test_context, ignite_config, params, modules=None):
+ """
+ Start loader application.
+ """
+ IgniteApplicationService(
+ test_context,
+ config=ignite_config,
+ java_class_name="org.apache.ignite.internal.ducktest.tests.ContinuousDataLoadApplication",
+ modules=modules,
+ # mute spam in log.
+ jvm_opts=["-DIGNITE_DUMP_THREADS_ON_FAILURE=false"],
+ params=params).start()
+
+
+def choose_node_to_kill(servers, nodes_to_kill, sequential):
+ """Choose node to kill during test"""
+ assert nodes_to_kill > 0, "No nodes to kill passed. Check the parameters."
+
+ idx = random.randint(0, len(servers.nodes)-1)
+
+ to_kill = servers.nodes[idx:] + servers.nodes[:idx-1]
+
+ if not sequential:
+ to_kill = to_kill[0::2]
+
+ idx = random.randint(0, len(to_kill) - nodes_to_kill)
+ to_kill = to_kill[idx:idx + nodes_to_kill]
+
+ assert len(to_kill) == nodes_to_kill, "Unable to pick up required number of nodes to kill."
+
+ return to_kill
+
+
+def order(node):
+ """Return discovery order of the node."""
+ return node.discovery_info().order
+
+
+def node_id(node):
+ """Return node id."""
+ return node.discovery_info().node_id
diff --git a/modules/ducktests/tests/ignitetest/tests/pds_compatibility_test.py b/modules/ducktests/tests/ignitetest/tests/pds_compatibility_test.py
new file mode 100644
index 0000000000000..eb9eae6bab372
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/pds_compatibility_test.py
@@ -0,0 +1,160 @@
+# 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.
+
+"""
+This module contains tests that checks that dev version compatible with LATEST
+"""
+
+import time
+from enum import IntEnum
+
+from ducktape.mark import matrix
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.utils.control_utility import ControlUtility
+from ignitetest.services.utils.ignite_configuration.discovery import from_ignite_cluster
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration, DataStorageConfiguration
+from ignitetest.services.utils.ignite_configuration.data_storage import DataRegionConfiguration
+from ignitetest.utils import cluster, versions_pair
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, LATEST, IgniteVersion
+from ignitetest.utils.enum import constructible
+
+
+@constructible
+class LoadType(IntEnum):
+ """
+ Load type.
+ """
+ DICTIONARY_CACHE = 1
+ SQL_CACHE = 2
+
+
+# pylint: disable=W0223
+# pylint: disable=no-member
+class PdsCompatibilityTest(IgniteTest):
+ """
+ Saves data using previous version of ignite and then load this data using actual version.
+
+ DictianaryCacheApplication - create replicated cache
+ SqlCacheApplication - create pojo cache with index
+ TODO: VariablesCacheApplication - create caches for different simple Java library objects
+
+ """
+
+ DATA_AMOUNT = 100
+
+ DICTIONARY_APP_CLASS = "org.apache.ignite.internal.ducktest.tests.pds_compatibility_test.DictionaryCacheApplication"
+ DICTIONARY_CHECK_CLASS = "org.apache.ignite.internal.ducktest.tests.pds_compatibility_test." \
+ "DictionaryCacheApplicationCheck"
+ SQL_APP_CLASS = "org.apache.ignite.internal.ducktest.tests.pds_compatibility_test.SqlCacheApplication"
+ SQL_CHECK_CLASS = "org.apache.ignite.internal.ducktest.tests.pds_compatibility_test.SqlCacheApplicationCheck"
+
+ @cluster(num_nodes=5)
+ @versions_pair(str(DEV_BRANCH), str(LATEST))
+ @matrix(load_type=[LoadType.DICTIONARY_CACHE, LoadType.SQL_CACHE])
+ def test_pds_compatibility(self, ignite_version_1, ignite_version_2, load_type):
+ """
+ Saves data using previous version of ignite and then load this data using actual version.
+ """
+
+ l_type = LoadType.construct_from(load_type)
+ if l_type == LoadType.DICTIONARY_CACHE:
+ application_class = self.DICTIONARY_APP_CLASS
+ check_class = self.DICTIONARY_CHECK_CLASS
+ elif l_type == LoadType.SQL_CACHE:
+ application_class = self.SQL_APP_CLASS
+ check_class = self.SQL_CHECK_CLASS
+
+ num_nodes = len(self.test_context.cluster) - 1
+
+ # Start server with ignite_version_2
+
+ server_configuration = IgniteConfiguration(cluster_state="INACTIVE",
+ version=IgniteVersion(ignite_version_1),
+ data_storage=DataStorageConfiguration(
+ default=DataRegionConfiguration(name='persistent',
+ persistent=True)
+ ))
+ ignite = IgniteService(self.test_context, server_configuration, num_nodes=num_nodes)
+
+ # TODO: Remove after merge: Start on the same nodes fix
+ running_nodes_tmp = ignite.nodes.copy()
+
+ ignite.start(True)
+
+ control_utility = ControlUtility(ignite, self.test_context)
+ control_utility.activate()
+
+ # This client just put some data to the cache.
+ app_config = server_configuration._replace(client_mode=True, discovery_spi=from_ignite_cluster(ignite))
+ app = IgniteApplicationService(self.test_context, config=app_config,
+ java_class_name=application_class,
+ params={"cacheName": "test-cache", "range": self.DATA_AMOUNT})
+
+ app.start()
+
+ app.await_event("Cache created",
+ timeout_sec=120,
+ from_the_beginning=True,
+ backoff_sec=1)
+
+ app.stop()
+ app.free()
+
+ control_utility.deactivate()
+
+ ignite.stop()
+ ignite.free()
+
+ # Start server with ignite_version_2
+
+ server_configuration = IgniteConfiguration(cluster_state="INACTIVE",
+ version=IgniteVersion(ignite_version_2),
+ data_storage=DataStorageConfiguration(
+ default=DataRegionConfiguration(name='persistent',
+ persistent=True),
+ regions=[
+ DataRegionConfiguration(name='in-memory', persistent=False,
+ max_size=100 * 1024 * 1024)]
+ ))
+ ignite = IgniteService(self.test_context, server_configuration, num_nodes=num_nodes)
+
+ ignite.nodes = running_nodes_tmp
+
+ ignite.start(False)
+
+ # # TODO: Remove after merge: IGNITE-13829: Added log rotation to ducktape-tests
+ time.sleep(2)
+
+ control_utility = ControlUtility(ignite, self.test_context)
+ control_utility.activate()
+
+ # Start client
+
+ app_config = server_configuration._replace(client_mode=True, discovery_spi=from_ignite_cluster(ignite))
+ app = IgniteApplicationService(self.test_context, config=app_config,
+ java_class_name=check_class,
+ params={"cacheName": "test-cache", "range": self.DATA_AMOUNT})
+
+ app.start()
+
+ app.await_event("Cache checked",
+ timeout_sec=120,
+ from_the_beginning=True,
+ backoff_sec=1)
+
+ app.stop()
+ ignite.stop()
diff --git a/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py b/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py
new file mode 100644
index 0000000000000..08a43bfae3682
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/pme_free_switch_test.py
@@ -0,0 +1,134 @@
+# 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.
+
+"""
+This module contains PME free switch tests.
+"""
+
+import time
+from enum import IntEnum
+
+from ducktape.mark import matrix
+
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.utils.control_utility import ControlUtility
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration
+from ignitetest.services.utils.ignite_configuration.cache import CacheConfiguration
+from ignitetest.services.utils.ignite_configuration.discovery import from_ignite_cluster
+from ignitetest.utils import ignite_versions, cluster
+from ignitetest.utils.enum import constructible
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, LATEST_2_7, V_2_8_0, IgniteVersion
+
+
+@constructible
+class LoadType(IntEnum):
+ """
+ Load type.
+ """
+ NONE = 0
+ EXTRA_CACHES = 1
+ LONG_TXS = 2
+
+
+# pylint: disable=W0223
+# pylint: disable=no-member
+class PmeFreeSwitchTest(IgniteTest):
+ """
+ Tests PME free switch scenarios.
+ """
+ NUM_NODES = 9
+ EXTRA_CACHES_AMOUNT = 100
+
+ @cluster(num_nodes=NUM_NODES + 2)
+ @ignite_versions(str(DEV_BRANCH), str(LATEST_2_7))
+ @matrix(load_type=[LoadType.NONE, LoadType.EXTRA_CACHES, LoadType.LONG_TXS])
+ def test(self, ignite_version, load_type):
+ """
+ Tests PME-free switch scenario (node stop).
+ """
+ data = {}
+
+ caches = [CacheConfiguration(name='test-cache', backups=2, atomicity_mode='TRANSACTIONAL')]
+
+ l_type = LoadType.construct_from(load_type)
+
+ # Checking PME (before 2.8) vs PME-free (2.8+) switch duration, but
+ # focusing on switch duration (which depends on caches amount) when long_txs is false and
+ # on waiting for previously started txs before the switch (which depends on txs duration) when long_txs of true.
+ if l_type is LoadType.EXTRA_CACHES:
+ for idx in range(1, self.EXTRA_CACHES_AMOUNT):
+ caches.append(CacheConfiguration(name="cache-%d" % idx, backups=2, atomicity_mode='TRANSACTIONAL'))
+
+ config = IgniteConfiguration(version=IgniteVersion(ignite_version), caches=caches, cluster_state="INACTIVE")
+
+ num_nodes = len(self.test_context.cluster) - 2
+
+ self.test_context.logger.info("Nodes amount calculated as %d." % num_nodes)
+
+ ignites = IgniteService(self.test_context, config, num_nodes=num_nodes)
+
+ ignites.start()
+
+ if IgniteVersion(ignite_version) >= V_2_8_0:
+ ControlUtility(ignites, self.test_context).disable_baseline_auto_adjust()
+
+ ControlUtility(ignites, self.test_context).activate()
+
+ client_config = config._replace(client_mode=True,
+ discovery_spi=from_ignite_cluster(ignites, slice(0, num_nodes - 1)))
+
+ long_tx_streamer = IgniteApplicationService(
+ self.test_context,
+ client_config,
+ java_class_name="org.apache.ignite.internal.ducktest.tests.pme_free_switch_test.LongTxStreamerApplication",
+ params={"cacheName": "test-cache"},
+ startup_timeout_sec=180)
+
+ if l_type is LoadType.LONG_TXS:
+ long_tx_streamer.start()
+
+ single_key_tx_streamer = IgniteApplicationService(
+ self.test_context,
+ client_config,
+ java_class_name="org.apache.ignite.internal.ducktest.tests.pme_free_switch_test."
+ "SingleKeyTxStreamerApplication",
+ params={"cacheName": "test-cache", "warmup": 1000},
+ startup_timeout_sec=180)
+
+ single_key_tx_streamer.start()
+
+ ignites.stop_node(ignites.nodes[num_nodes - 1])
+
+ single_key_tx_streamer.await_event("Node left topology", 60, from_the_beginning=True)
+
+ if l_type is LoadType.LONG_TXS:
+ time.sleep(30) # keeping txs alive for 30 seconds.
+
+ long_tx_streamer.stop_async()
+
+ single_key_tx_streamer.await_event("Node left topology", 60, from_the_beginning=True)
+
+ single_key_tx_streamer.await_event("APPLICATION_STREAMED", 60) # waiting for streaming continuation.
+
+ single_key_tx_streamer.stop()
+
+ data["Worst latency (ms)"] = single_key_tx_streamer.extract_result("WORST_LATENCY")
+ data["Streamed txs"] = single_key_tx_streamer.extract_result("STREAMED")
+ data["Measure duration (ms)"] = single_key_tx_streamer.extract_result("MEASURE_DURATION")
+ data["Server nodes"] = num_nodes
+
+ return data
diff --git a/modules/ducktests/tests/ignitetest/tests/self_test.py b/modules/ducktests/tests/ignitetest/tests/self_test.py
new file mode 100644
index 0000000000000..e5f91e90a772e
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/self_test.py
@@ -0,0 +1,95 @@
+# 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.
+
+"""
+This module contains smoke tests that checks that ducktape works as expected
+"""
+
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.ignite_execution_exception import IgniteExecutionException
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration, IgniteClientConfiguration
+from ignitetest.services.utils.ignite_configuration.discovery import from_ignite_cluster
+from ignitetest.utils import ignite_versions, cluster
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, IgniteVersion
+
+
+# pylint: disable=W0223
+class SelfTest(IgniteTest):
+ """
+ Self test
+ """
+
+ @cluster(num_nodes=1)
+ @ignite_versions(str(DEV_BRANCH))
+ def test_assertion_convertion(self, ignite_version):
+ """
+ Test to make sure Java assertions are converted to python exceptions
+ """
+ server_configuration = IgniteConfiguration(version=IgniteVersion(ignite_version))
+
+ app = IgniteApplicationService(
+ self.test_context,
+ server_configuration,
+ java_class_name="org.apache.ignite.internal.ducktest.tests.smoke_test.AssertionApplication")
+
+ try:
+ app.start()
+ except IgniteExecutionException as ex:
+ assert str(ex) == "Java application execution failed. java.lang.AssertionError"
+ else:
+ app.stop()
+ assert False
+
+ @cluster(num_nodes=4)
+ @ignite_versions(str(DEV_BRANCH))
+ def test_simple_services_start_stop(self, ignite_version):
+ """
+ Tests plain services start and stop (termitation vs self-terination).
+ """
+ ignites = IgniteService(self.test_context, IgniteConfiguration(version=IgniteVersion(ignite_version)),
+ num_nodes=1)
+
+ ignites.start()
+
+ client = IgniteService(self.test_context, IgniteClientConfiguration(version=IgniteVersion(ignite_version)),
+ num_nodes=1)
+
+ client.start()
+
+ node1 = IgniteApplicationService(
+ self.test_context,
+ IgniteClientConfiguration(version=IgniteVersion(ignite_version),
+ discovery_spi=from_ignite_cluster(ignites)),
+ java_class_name="org.apache.ignite.internal.ducktest.tests.self_test.TestKillableApplication",
+ startup_timeout_sec=180)
+
+ node2 = IgniteApplicationService(
+ self.test_context,
+ IgniteClientConfiguration(version=IgniteVersion(ignite_version),
+ discovery_spi=from_ignite_cluster(ignites)),
+ java_class_name="org.apache.ignite.internal.ducktest.tests.self_test.TestSelfKillableApplication",
+ startup_timeout_sec=180)
+
+ node1.start()
+
+ node2.run()
+
+ node1.stop()
+
+ client.stop()
+
+ ignites.stop()
diff --git a/modules/ducktests/tests/ignitetest/tests/smoke_test.py b/modules/ducktests/tests/ignitetest/tests/smoke_test.py
new file mode 100644
index 0000000000000..da796345f5ebb
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/smoke_test.py
@@ -0,0 +1,87 @@
+# 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.
+
+"""
+This module contains smoke tests that checks that services work
+"""
+
+from ignitetest.services.ignite import IgniteService
+from ignitetest.services.ignite_app import IgniteApplicationService
+from ignitetest.services.spark import SparkService
+from ignitetest.services.utils.ignite_configuration.discovery import from_ignite_cluster
+from ignitetest.services.utils.ignite_configuration import IgniteConfiguration
+from ignitetest.services.zk.zookeeper import ZookeeperService
+from ignitetest.utils import ignite_versions, cluster
+from ignitetest.utils.ignite_test import IgniteTest
+from ignitetest.utils.version import DEV_BRANCH, IgniteVersion
+
+
+# pylint: disable=W0223
+class SmokeServicesTest(IgniteTest):
+ """
+ Tests services implementations
+ """
+
+ @cluster(num_nodes=1)
+ @ignite_versions(str(DEV_BRANCH))
+ def test_ignite_start_stop(self, ignite_version):
+ """
+ Test that IgniteService correctly start and stop
+ """
+ ignite = IgniteService(self.test_context, IgniteConfiguration(version=IgniteVersion(ignite_version)),
+ num_nodes=1)
+ print(self.test_context)
+ ignite.start()
+ ignite.stop()
+
+ @cluster(num_nodes=2)
+ @ignite_versions(str(DEV_BRANCH))
+ def test_ignite_app_start_stop(self, ignite_version):
+ """
+ Test that IgniteService and IgniteApplicationService correctly start and stop
+ """
+ server_configuration = IgniteConfiguration(version=IgniteVersion(ignite_version))
+
+ ignite = IgniteService(self.test_context, server_configuration, num_nodes=1)
+
+ client_configuration = server_configuration._replace(client_mode=True,
+ discovery_spi=from_ignite_cluster(ignite))
+ app = IgniteApplicationService(
+ self.test_context,
+ client_configuration,
+ java_class_name="org.apache.ignite.internal.ducktest.tests.smoke_test.SimpleApplication")
+
+ ignite.start()
+ app.start()
+ app.stop()
+ ignite.stop()
+
+ @cluster(num_nodes=2)
+ def test_spark_start_stop(self):
+ """
+ Test that SparkService correctly start and stop
+ """
+ spark = SparkService(self.test_context, num_nodes=2)
+ spark.start()
+ spark.stop()
+
+ @cluster(num_nodes=3)
+ def test_zk_start_stop(self):
+ """
+ Test that ZookeeperService correctly start and stop
+ """
+ zookeeper = ZookeeperService(self.test_context, num_nodes=3)
+ zookeeper.start()
+ zookeeper.stop()
diff --git a/modules/ducktests/tests/ignitetest/tests/suites/fast_suite.yml b/modules/ducktests/tests/ignitetest/tests/suites/fast_suite.yml
new file mode 100644
index 0000000000000..7725391963539
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/suites/fast_suite.yml
@@ -0,0 +1,33 @@
+# 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.
+
+smoke:
+ - ../smoke_test.py
+
+control_utility:
+ - ../control_utility
+
+pme_free_switch:
+ - ../pme_free_switch_test.py
+
+cellular_affinity:
+ - ../cellular_affinity_test.py
+
+rebalance:
+ - ../add_node_rebalance_test.py
+
+clients:
+ - ../client_test.py
+
diff --git a/modules/ducktests/tests/ignitetest/tests/suites/slow_suite.yml b/modules/ducktests/tests/ignitetest/tests/suites/slow_suite.yml
new file mode 100644
index 0000000000000..6f066d50889a6
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/tests/suites/slow_suite.yml
@@ -0,0 +1,17 @@
+# 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
+
+discovery:
+ - ../discovery_test.py
diff --git a/modules/ducktests/tests/ignitetest/utils/__init__.py b/modules/ducktests/tests/ignitetest/utils/__init__.py
new file mode 100644
index 0000000000000..3a4acfde256f5
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/utils/__init__.py
@@ -0,0 +1,22 @@
+# 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.
+
+"""
+This module contains convenient utils for test.
+"""
+
+from ._mark import version_if, ignite_versions, cluster, versions_pair
+
+__all__ = ['version_if', 'ignite_versions', 'cluster', 'versions_pair']
diff --git a/modules/ducktests/tests/ignitetest/utils/_mark.py b/modules/ducktests/tests/ignitetest/utils/_mark.py
new file mode 100644
index 0000000000000..67f917d02837f
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/utils/_mark.py
@@ -0,0 +1,242 @@
+# 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.
+
+"""
+Module contains useful test decorators.
+"""
+
+from collections.abc import Iterable
+import copy
+
+from ducktape.cluster.cluster_spec import ClusterSpec
+from ducktape.mark._mark import Ignore, Mark, _inject
+
+from ignitetest.utils.version import IgniteVersion
+
+
+class VersionIf(Ignore):
+ """
+ Ignore test if version doesn't corresponds to condition.
+ """
+ def __init__(self, condition, variable_name):
+ super().__init__()
+ self.condition = condition
+ self.variable_name = variable_name
+
+ def apply(self, seed_context, context_list):
+ assert len(context_list) > 0, "ignore_if decorator is not being applied to any test cases"
+
+ for ctx in context_list:
+ if self.variable_name in ctx.injected_args:
+ version = ctx.injected_args[self.variable_name]
+ assert isinstance(version, str), "'%s'n injected args must be a string" % (self.variable_name,)
+ ctx.ignore = ctx.ignore or not self.condition(IgniteVersion(version))
+
+ return context_list
+
+ def __eq__(self, other):
+ return super().__eq__(other) and self.condition == other.condition
+
+
+class IgniteVersionParametrize(Mark):
+ """
+ Parametrize function with ignite_version
+ """
+ def __init__(self, *args, version_prefix):
+ """
+ :param args: Can be string, tuple of strings or iterable of them.
+ :param version_prefix: prefix for variable to inject into test function.
+ """
+ self.versions = list(args)
+ self.version_prefix = version_prefix
+
+ def apply(self, seed_context, context_list):
+ if 'ignite_versions' in seed_context.globals:
+ ver = seed_context.globals['ignite_versions']
+ if isinstance(ver, str):
+ self.versions = [ver]
+ elif isinstance(ver, Iterable):
+ self.versions = list(ver)
+ else:
+ raise AssertionError("Expected string or iterable as parameter in ignite_versions, "
+ "%s of type %s passed" % (ver, type(ver)))
+
+ new_context_list = []
+ if context_list:
+ for ctx in context_list:
+ for version in self.versions:
+ if self._check_injected(ctx):
+ continue
+
+ new_context_list.insert(0, self._prepare_new_ctx(version, seed_context, ctx))
+ else:
+ for version in self.versions:
+ new_context_list.insert(0, self._prepare_new_ctx(version, seed_context))
+
+ return new_context_list
+
+ def _prepare_new_ctx(self, version, seed_context, ctx=None):
+ injected_args = dict(ctx.injected_args) if ctx and ctx.injected_args else {}
+
+ if isinstance(version, (list, tuple)) and len(version) >= 2:
+ for idx, ver in enumerate(version):
+ injected_args["%s_%s" % (self.version_prefix, idx + 1)] = ver
+ elif isinstance(version, str):
+ injected_args[self.version_prefix] = version
+ else:
+ raise AssertionError("Expected string or iterable of size greater than 2 as element in ignite_versions,"
+ "%s of type %s passed" % (version, type(version)))
+
+ injected_fun = _inject(**injected_args)(seed_context.function)
+
+ return seed_context.copy(function=injected_fun, injected_args=injected_args)
+
+ def _check_injected(self, ctx):
+ if ctx.injected_args:
+ for arg_name in ctx.injected_args.keys():
+ if arg_name.startswith(self.version_prefix):
+ return True
+
+ return False
+
+ @property
+ def name(self):
+ """
+ This function should return "PARAMETRIZE" string in order to ducktape's method parametrization works.
+ Should be fixed after apropriate patch in ducktape will be merged.
+ """
+ return "PARAMETRIZE"
+
+ def __eq__(self, other):
+ return super().__eq__(other) and self.versions == other.versions
+
+
+CLUSTER_SPEC_KEYWORD = "cluster_spec"
+CLUSTER_SIZE_KEYWORD = "num_nodes"
+
+
+class ParametrizableClusterMetadata(Mark):
+ """Provide a hint about how a given test will use the cluster."""
+
+ def __init__(self, **kwargs):
+ self.metadata = copy.copy(kwargs)
+
+ @property
+ def name(self):
+ return "PARAMETRIZABLE_RESOURCE_HINT_CLUSTER_USE"
+
+ def apply(self, seed_context, context_list):
+ assert len(context_list) > 0, "parametrizable cluster use annotation is not being applied to any test cases"
+
+ cluster_size_param = self._extract_cluster_size(seed_context)
+
+ for ctx in context_list:
+ if cluster_size_param:
+ self._modify_metadata(cluster_size_param)
+
+ if not ctx.cluster_use_metadata:
+ ctx.cluster_use_metadata = self.metadata
+
+ return context_list
+
+ @staticmethod
+ def _extract_cluster_size(seed_context):
+ cluster_size = seed_context.globals.get('cluster_size')
+
+ if cluster_size is None:
+ return None
+
+ res = int(cluster_size) if isinstance(cluster_size, str) else cluster_size
+
+ if not isinstance(res, int):
+ raise ValueError(f"Expected string or int param, {cluster_size} of {type(cluster_size)} passed instead.")
+
+ if res <= 0:
+ raise ValueError(f"Expected cluster_size greater than 0, {cluster_size} passed instead.")
+
+ return res
+
+ def _modify_metadata(self, new_size):
+ cluster_spec = self.metadata.get(CLUSTER_SPEC_KEYWORD)
+ cluster_size = self.metadata.get(CLUSTER_SIZE_KEYWORD)
+
+ if cluster_spec is not None and not cluster_spec.empty():
+ node_spec = next(iter(cluster_spec))
+ self.metadata[CLUSTER_SPEC_KEYWORD] = ClusterSpec.from_nodes([node_spec] * new_size)
+ elif cluster_size is not None:
+ self.metadata[CLUSTER_SIZE_KEYWORD] = new_size
+
+
+def ignite_versions(*args, version_prefix="ignite_version"):
+ """
+ Decorate test function to inject ignite versions. Versions will be overriden by globals "ignite_versions" param.
+ :param args: Can be string, tuple of strings or iterable of them.
+ :param version_prefix: prefix for variable to inject into test function.
+ """
+ def parametrizer(func):
+ Mark.mark(func, IgniteVersionParametrize(*args, version_prefix=version_prefix))
+
+ return func
+ return parametrizer
+
+
+def version_if(condition, *, variable_name='ignite_version'):
+ """
+ Mark decorated test method as IGNORE if version doesn't corresponds to condition.
+
+ :param condition: function(IgniteVersion) -> bool
+ :param variable_name: version variable name
+ """
+ def ignorer(func):
+ Mark.mark(func, VersionIf(condition, variable_name))
+ return func
+
+ return ignorer
+
+
+def cluster(**kwargs):
+ """Test method decorator used to provide hints about how the test will use the given cluster.
+
+ :Keywords:
+
+ - ``num_nodes`` provide hint about how many nodes the test will consume
+ - ``cluster_spec`` provide hint about how many nodes of each type the test will consume
+ """
+ def cluster_use_metadata_adder(func):
+ Mark.mark(func, ParametrizableClusterMetadata(**kwargs))
+ return func
+
+ return cluster_use_metadata_adder
+
+
+def versions_pair(*args, version_prefix="ignite_version"):
+ """
+ Decorate test function to inject ignite versions. Versions will be overriden by globals "ignite_versions" param.
+ produce pairs for all possible versions intersections
+ :param args: Can be string, tuple of strings or iterable of them.
+ :param version_prefix: prefix for variable to inject into test function.
+ """
+ res = []
+ for v_arg1 in args:
+ for v_arg2 in args:
+ if v_arg1 < v_arg2:
+ res.append((v_arg1, v_arg2))
+
+ def parametrizer(func):
+ Mark.mark(func, IgniteVersionParametrize(*res, version_prefix=version_prefix))
+
+ return func
+
+ return parametrizer
diff --git a/modules/ducktests/tests/ignitetest/utils/enum.py b/modules/ducktests/tests/ignitetest/utils/enum.py
new file mode 100644
index 0000000000000..3aa3b2c803322
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/utils/enum.py
@@ -0,0 +1,66 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+This module contains various syntaxic sugar for enums
+"""
+
+from enum import IntEnum
+
+
+def constructible(cls):
+ """
+ If decorate IntEnum subclass with this decorator, following is possible:
+
+ @constructible
+ class StopType(IntEnum):
+ SIGTERM = 0
+ SIGKILL = 1
+ DISCONNECT = 2
+
+ stop_type = 1
+
+ with StopType.construct_from(stop_type) as sp:
+ if sp is StopType.SIGKILL:
+ print('got sigkill')
+
+ stop_type = 'DISCONNECT'
+
+ with StopType.construct_from(stop_type) as sp:
+ if sp is StopType.DISCONNECT:
+ print('got disconnect')
+
+ """
+ assert issubclass(cls, IntEnum)
+
+ members = dict(cls.__members__.items())
+
+ def construct_from(val):
+ if isinstance(val, str):
+ return members[val]
+ return cls.__new__(cls, val)
+
+ def __enter__(self):
+ return self
+
+ # pylint: disable=unused-argument
+ def __exit__(self, *args, **kwargs):
+ pass
+
+ cls.construct_from = construct_from
+ cls.__enter__ = __enter__
+ cls.__exit__ = __exit__
+
+ return cls
diff --git a/modules/ducktests/tests/ignitetest/utils/ignite_test.py b/modules/ducktests/tests/ignitetest/utils/ignite_test.py
new file mode 100644
index 0000000000000..677141c72b165
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/utils/ignite_test.py
@@ -0,0 +1,57 @@
+# 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.
+
+"""
+This module contains basic ignite test.
+"""
+from time import monotonic
+
+from ducktape.cluster.remoteaccount import RemoteCommandError
+from ducktape.tests.test import Test
+
+
+# pylint: disable=W0223
+class IgniteTest(Test):
+ """
+ Basic ignite test.
+ """
+ def __init__(self, test_context):
+ super().__init__(test_context=test_context)
+
+ @staticmethod
+ def monotonic():
+ """
+ monotonic() -> float
+
+ :return:
+ The value (in fractional seconds) of a monotonic clock, i.e. a clock that cannot go backwards.
+ The clock is not affected by system clock updates. The reference point of the returned value is undefined,
+ so that only the difference between the results of consecutive calls is valid.
+ """
+ return monotonic()
+
+ # pylint: disable=W0212
+ def tearDown(self):
+ self.logger.debug("Killing all services to speed-up the tearing down.")
+
+ for service in self.test_context.services._services.values():
+ try:
+ service.kill()
+ except RemoteCommandError:
+ pass # Process may be already self-killed on segmentation.
+
+ self.logger.debug("All services killed.")
+
+ super().tearDown()
diff --git a/modules/ducktests/tests/ignitetest/utils/version.py b/modules/ducktests/tests/ignitetest/utils/version.py
new file mode 100644
index 0000000000000..dec655f3e94cf
--- /dev/null
+++ b/modules/ducktests/tests/ignitetest/utils/version.py
@@ -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.
+
+"""
+Module contains ignite version utility class.
+"""
+
+from distutils.version import LooseVersion
+
+from ignitetest import __version__
+
+
+class IgniteVersion(LooseVersion):
+ """
+ Container for Ignite versions which makes versions simple to compare.
+
+ distutils.version.LooseVersion (and StrictVersion) has robust comparison and ordering logic.
+
+ Example:
+
+ v27 = IgniteVersion("2.7.0")
+ v28 = IgniteVersion("2.8.1")
+ assert v28 > v27 # assertion passes!
+ """
+ def __init__(self, version_string):
+ self.is_dev = (version_string.lower() == "dev")
+ if self.is_dev:
+ version_string = __version__
+
+ # Drop dev suffix if present
+ dev_suffix_index = version_string.find(".dev")
+ if dev_suffix_index >= 0:
+ version_string = version_string[:dev_suffix_index]
+
+ super().__init__(version_string)
+
+ def __str__(self):
+ if self.is_dev:
+ return "dev"
+
+ return super().__str__()
+
+ def __repr__(self):
+ return "IgniteVersion ('%s')" % str(self)
+
+
+DEV_BRANCH = IgniteVersion("dev")
+
+# 2.7.x versions
+V_2_7_6 = IgniteVersion("2.7.6")
+LATEST_2_7 = V_2_7_6
+
+# 2.8.x versions
+V_2_8_0 = IgniteVersion("2.8.0")
+V_2_8_1 = IgniteVersion("2.8.1")
+LATEST_2_8 = V_2_8_1
+
+# 2.9.x versions
+V_2_9_0 = IgniteVersion("2.9.0")
+LATEST_2_9 = V_2_9_0
+
+LATEST = LATEST_2_9
diff --git a/modules/ducktests/tests/setup.py b/modules/ducktests/tests/setup.py
new file mode 100644
index 0000000000000..7c658ee7caf0a
--- /dev/null
+++ b/modules/ducktests/tests/setup.py
@@ -0,0 +1,37 @@
+# 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.
+
+import re
+from setuptools import find_packages, setup
+
+
+with open('ignitetest/__init__.py', 'r') as fd:
+ version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1)
+
+
+# Note: when changing the version of ducktape, also revise tests/docker/Dockerfile
+setup(name="ignitetest",
+ version=version,
+ description="Apache Ignite System Tests",
+ author="Apache Ignite",
+ platforms=["any"],
+ license="apache2.0",
+ packages=find_packages(exclude=["ignitetest.tests", "ignitetest.tests.*"]),
+ include_package_data=True,
+ install_requires=["ducktape==0.8.1"],
+ tests_require=["pytest==6.0.1"],
+ dependency_links=[
+ 'https://github.com/Sberbank-Technology/ducktape/tarball/master#egg=ducktape-0.8.1'
+ ])
diff --git a/modules/ducktests/tests/tox.ini b/modules/ducktests/tests/tox.ini
new file mode 100644
index 0000000000000..f74d685ca7464
--- /dev/null
+++ b/modules/ducktests/tests/tox.ini
@@ -0,0 +1,69 @@
+# 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.
+[tox]
+envlist = codestyle, linter, py36, py37, py38
+skipsdist = True
+
+[travis]
+python =
+ 3.8: codestyle, linter, py38
+ 3.7: py37
+ 3.6: py36
+
+[testenv]
+envdir = {homedir}/.virtualenvs/ignite-ducktests-{envname}
+deps =
+ -r ./docker/requirements-dev.txt
+recreate = True
+usedevelop = True
+commands =
+ pytest {env:PYTESTARGS:} {posargs}
+
+[testenv:linter]
+basepython = python3.8
+commands =
+ pylint --rcfile=tox.ini ignitetest checks
+
+[testenv:codestyle]
+basepython = python3.8
+commands =
+ flake8
+
+[testenv:py36]
+envdir = {homedir}/.virtualenvs/ignite-ducktests-py36
+
+[testenv:py37]
+envdir = {homedir}/.virtualenvs/ignite-ducktests-py37
+
+[testenv:py38]
+envdir = {homedir}/.virtualenvs/ignite-ducktests-py38
+
+[BASIC]
+min-public-methods=0
+good-names=i, j, k, x, y, ex, pk, tx
+
+[SIMILARITIES]
+ignore-imports=yes
+
+[FORMAT]
+max-line-length=120
+
+[flake8]
+max-line-length=120
+
+[pytest]
+python_files=check_*.py
+python_classes=Check
+python_functions=check_*
diff --git a/parent/pom.xml b/parent/pom.xml
index ab52dbb6bd199..64e002a152fb4 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -872,6 +872,7 @@
**/keystore/ca/*.txt.attr
**/keystore/ca/*serial
**/META-INF/services/**
+ **/id_rsa**
.travis.yml
.github/PULL_REQUEST_TEMPLATE.md
diff --git a/pom.xml b/pom.xml
index 5b410fed6e023..c40269ca3bf18 100644
--- a/pom.xml
+++ b/pom.xml
@@ -95,6 +95,7 @@
examples
modules/benchmarks
modules/compatibility
+ modules/ducktests
modules/geospatial
modules/hibernate-4.2
modules/hibernate-5.1
@@ -123,6 +124,13 @@
+
+ ducktests
+
+ modules/ducktests
+
+
+
test
diff --git a/scripts/build-module.sh b/scripts/build-module.sh
new file mode 100755
index 0000000000000..541ac8b762410
--- /dev/null
+++ b/scripts/build-module.sh
@@ -0,0 +1,25 @@
+#!/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.
+#
+
+#
+# Builds project.
+# Run in Ignite sources root directory.
+# Usage: ./scripts/build-module.sh ducktests
+#
+
+mvn package -pl :ignite-$1 -Pall-java,all-scala -DskipTests -Dmaven.javadoc.skip=true -am
\ No newline at end of file
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000000000..d6d83050c901c
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,25 @@
+#!/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.
+#
+
+#
+# Builds project.
+# Run in Ignite sources root directory.
+# Usage: ./scripts/build.sh
+#
+
+mvn clean package -Pall-java,all-scala -DskipTests -Dmaven.javadoc.skip=true
\ No newline at end of file