| /* |
| * 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.testframework; |
| |
| import static java.nio.file.StandardOpenOption.CREATE; |
| import static java.nio.file.StandardOpenOption.SYNC; |
| import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; |
| import static org.apache.ignite.internal.util.Constants.MiB; |
| |
| import com.typesafe.config.ConfigException; |
| import com.typesafe.config.parser.ConfigDocument; |
| import com.typesafe.config.parser.ConfigDocumentFactory; |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Objects; |
| import java.util.concurrent.CompletableFuture; |
| import org.apache.ignite.Ignite; |
| import org.apache.ignite.IgnitionManager; |
| import org.apache.ignite.InitParameters; |
| import org.apache.ignite.InitParametersBuilder; |
| import org.apache.ignite.internal.hlc.TestClockService; |
| import org.apache.ignite.lang.IgniteException; |
| import org.jetbrains.annotations.Nullable; |
| |
| /** Helper class for starting a node with a string-based configuration. */ |
| public class TestIgnitionManager { |
| |
| /** Default name of configuration file. */ |
| public static final String DEFAULT_CONFIG_NAME = "ignite-config.conf"; |
| |
| private static final int DEFAULT_SCALECUBE_METADATA_TIMEOUT = 10_000; |
| |
| /** Default DelayDuration in ms used for tests that is set on node init. */ |
| public static final int DEFAULT_DELAY_DURATION_MS = 100; |
| |
| private static final int DEFAULT_METASTORAGE_IDLE_SYNC_TIME_INTERVAL_MS = 10; |
| |
| /** Default partition idle SafeTime interval in ms used for tests that is set on node init. */ |
| public static final int DEFAULT_PARTITION_IDLE_SYNC_TIME_INTERVAL_MS = 100; |
| |
| public static final long DEFAULT_MAX_CLOCK_SKEW_MS = TestClockService.TEST_MAX_CLOCK_SKEW_MILLIS; |
| |
| /** Map with default node configuration values. */ |
| private static final Map<String, String> DEFAULT_NODE_CONFIG = Map.of( |
| "network.membership.scaleCube.metadataTimeout", Integer.toString(DEFAULT_SCALECUBE_METADATA_TIMEOUT), |
| "storage.profiles.default_aipersist.engine", "aipersist", |
| "storage.profiles.default_aipersist.size", Integer.toString(256 * MiB), |
| "storage.profiles.default_aimem.engine", "aimem", |
| "storage.profiles.default_aimem.initSize", Integer.toString(256 * MiB), |
| "storage.profiles.default_aimem.maxSize", Integer.toString(256 * MiB), |
| "storage.profiles.default.engine", "aipersist" |
| ); |
| |
| /** Map with default cluster configuration values. */ |
| private static final Map<String, String> DEFAULT_CLUSTER_CONFIG = Map.of( |
| "schemaSync.delayDuration", Integer.toString(DEFAULT_DELAY_DURATION_MS), |
| "schemaSync.maxClockSkew", Long.toString(DEFAULT_MAX_CLOCK_SKEW_MS), |
| "metaStorage.idleSyncTimeInterval", Integer.toString(DEFAULT_METASTORAGE_IDLE_SYNC_TIME_INTERVAL_MS), |
| "replication.idleSafeTimePropagationDuration", Integer.toString(DEFAULT_PARTITION_IDLE_SYNC_TIME_INTERVAL_MS) |
| ); |
| |
| /** |
| * Marker that explicitly requests production defaults when put to {@link InitParametersBuilder#clusterConfiguration(String)}. |
| */ |
| public static final String PRODUCTION_CLUSTER_CONFIG_STRING = "<production-cluster-config>"; |
| |
| /** |
| * Starts an Ignite node with an optional bootstrap configuration from an input stream with HOCON configs. |
| * |
| * <p>Test defaults are mixed to the configuration (only if the corresponding config keys are not explicitly defined). |
| * |
| * <p>When this method returns, the node is partially started and ready to accept the init command (that is, its |
| * REST endpoint is functional). |
| * |
| * @param nodeName Name of the node. Must not be {@code null}. |
| * @param configStr Optional node configuration. |
| * Following rules are used for applying the configuration properties: |
| * <ol> |
| * <li>Specified property overrides existing one or just applies itself if it wasn't |
| * previously specified.</li> |
| * <li>All non-specified properties either use previous value or use default one from |
| * corresponding configuration schema.</li> |
| * </ol> |
| * So that, in case of initial node start (first start ever) specified configuration, supplemented |
| * with defaults, is used. If no configuration was provided defaults are used for all |
| * configuration properties. In case of node restart, specified properties override existing |
| * ones, non specified properties that also weren't specified previously use default values. |
| * Please pay attention that previously specified properties are searched in the |
| * {@code workDir} specified by the user. |
| * |
| * @param workDir Work directory for the started node. Must not be {@code null}. |
| * @return Completable future that resolves into an Ignite node after all components are started and the cluster initialization is |
| * complete. |
| * @throws IgniteException If error occurs while reading node configuration. |
| */ |
| public static CompletableFuture<Ignite> start(String nodeName, @Nullable String configStr, Path workDir) { |
| try { |
| Files.createDirectories(workDir); |
| Path configPath = workDir.resolve(DEFAULT_CONFIG_NAME); |
| |
| addDefaultsToConfigurationFile(configStr, configPath); |
| |
| return IgnitionManager.start(nodeName, configPath, workDir); |
| } catch (IOException e) { |
| throw new IgniteException("Couldn't write node config.", e); |
| } |
| } |
| |
| /** |
| * Writes default values into the configuration file, according to the same rules that are used in {@link #start(String, String, Path)}. |
| */ |
| public static void addDefaultsToConfigurationFile(Path configPath) { |
| try { |
| addDefaultsToConfigurationFile(null, configPath); |
| } catch (IOException e) { |
| throw new IgniteException("Couldn't update node configuration file", e); |
| } |
| } |
| |
| private static void addDefaultsToConfigurationFile(@Nullable String configStr, Path configPath) throws IOException { |
| if (configStr == null && Files.exists(configPath)) { |
| // Nothing to do. |
| return; |
| } |
| |
| Files.writeString(configPath, applyTestDefaultsToConfig(configStr, DEFAULT_NODE_CONFIG), SYNC, CREATE, TRUNCATE_EXISTING); |
| } |
| |
| /** |
| * Initializes a cluster using test defaults for cluster configuration values that are not |
| * specified explicitly. |
| * |
| * @param parameters Init parameters. |
| * @see IgnitionManager#init(InitParameters) |
| */ |
| public static void init(InitParameters parameters) { |
| IgnitionManager.init(applyTestDefaultsToClusterConfig(parameters)); |
| } |
| |
| private static InitParameters applyTestDefaultsToClusterConfig(InitParameters params) { |
| InitParametersBuilder builder = new InitParametersBuilder() |
| .clusterName(params.clusterName()) |
| .destinationNodeName(params.nodeName()) |
| .metaStorageNodeNames(params.metaStorageNodeNames()) |
| .cmgNodeNames(params.cmgNodeNames()); |
| |
| if (!PRODUCTION_CLUSTER_CONFIG_STRING.equals(params.clusterConfiguration())) { |
| builder.clusterConfiguration(applyTestDefaultsToConfig(params.clusterConfiguration(), DEFAULT_CLUSTER_CONFIG)); |
| } |
| |
| return builder.build(); |
| } |
| |
| private static String applyTestDefaultsToConfig(@Nullable String configStr, Map<String, String> defaults) { |
| if (configStr == null) { |
| configStr = "{}"; |
| } |
| |
| ConfigDocument configDocument; |
| |
| try { |
| configDocument = ConfigDocumentFactory.parseString(configStr); |
| } catch (ConfigException e) { |
| // Preserve original broken content, it might be broken on purpose. |
| return configStr; |
| } |
| |
| for (Entry<String, String> entry : defaults.entrySet()) { |
| configDocument = applyTestDefault( |
| configDocument, |
| entry.getKey(), |
| entry.getValue() |
| ); |
| } |
| |
| return configDocument.render(); |
| } |
| |
| private static ConfigDocument parseNullableConfigString(@Nullable String configString) { |
| String configToParse = Objects.requireNonNullElse(configString, "{}"); |
| |
| return ConfigDocumentFactory.parseString(configToParse); |
| } |
| |
| private static ConfigDocument applyTestDefault(ConfigDocument document, String path, String value) { |
| if (document.hasPath(path)) { |
| return document; |
| } else { |
| return document.withValueText(path, value); |
| } |
| } |
| } |