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