// Copyright 2016 Twitter. All rights reserved. // // 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. package com.twitter.heron.spi.common; import java.time.Duration; import java.time.temporal.TemporalUnit; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.logging.Logger; import com.twitter.heron.common.basics.ByteAmount; import com.twitter.heron.common.basics.DryRunFormatType; import com.twitter.heron.common.basics.PackageType; import com.twitter.heron.common.basics.TypeUtils; /** * Config is an Immutable Map of <String, Object> The get/set API that uses Key objects * should be favored over Strings. Usage of the String API should be refactored out. * * A newly created Config object holds configs that might include wildcard tokens, like * ${HERON_HOME}/bin, ${HERON_LIB}/packing/*. Token substitution can be done by converting that * config to a local or cluster config by using the {@code Config.toLocalMode} or * {@code Config.toClusterMode} methods. * * Local mode is for a config to be used to run Heron locally, where HERON_HOME might be an install * dir on the local host (e.g. HERON_HOME=/usr/bin/heron). Cluster mode is to be used when building * configs for a remote process run on a service, where all directories might be relative to the * current dir by default (e.g. HERON_HOME=~/heron-core). */ public class Config { private static final Logger LOG = Logger.getLogger(Config.class.getName()); private final Map<String, Object> cfgMap; private enum Mode { RAW, // the initially provided configs without pattern substitution LOCAL, // the provided configs with pattern substitution for the local (i.e., client) env CLUSTER // the provided configs with pattern substitution for the cluster (i.e., remote) env } // Used to initialize a raw config. Should be used by consumers of Config via the builder protected Config(Builder build) { this.mode = Mode.RAW; this.rawConfig = this; this.cfgMap = new HashMap<>(build.keyValues); } // Used internally to create a Config that is actually a facade over a raw, local and // cluster config private Config(Mode mode, Config rawConfig, Config localConfig, Config clusterConfig) { this.mode = mode; this.rawConfig = rawConfig; this.localConfig = localConfig; this.clusterConfig = clusterConfig; switch (mode) { case RAW: this.cfgMap = rawConfig.cfgMap; break; case LOCAL: this.cfgMap = localConfig.cfgMap; break; case CLUSTER: this.cfgMap = clusterConfig.cfgMap; break; default: throw new IllegalArgumentException("Unrecognized mode passed to constructor: " + mode); } } public static Builder newBuilder() { return newBuilder(false); } public static Builder newBuilder(boolean loadDefaults) { return Builder.create(loadDefaults); } public static Config toLocalMode(Config config) { return config.lazyCreateConfig(Mode.LOCAL); } public static Config toClusterMode(Config config) { return config.lazyCreateConfig(Mode.CLUSTER); } private static Config expand(Config config) { return expand(config, 0); } /** * Recursively expand each config value until token substitution is exhausted. We must recurse * to handle the case where field expansion requires multiple iterations, due to new tokens being * introduced as we replace. For example: * * ${HERON_BIN}/heron-executor gets expanded to * ${HERON_HOME}/bin/heron-executor gets expanded to * /usr/local/heron/bin/heron-executor * * If break logic is when another round does not reduce the number of tokens, since it means we * couldn't find a valid replacement. */ private static Config expand(Config config, int previousTokensCount) { Config.Builder cb = Config.newBuilder().putAll(config); int tokensCount = 0; for (String key : config.getKeySet()) { Object value = config.get(key); if (value instanceof String) { String expandedValue = TokenSub.substitute(config, (String) value); if (expandedValue.contains("${")) { tokensCount++; } cb.put(key, expandedValue); } else { cb.put(key, value); } } if (previousTokensCount != tokensCount) { return expand(cb.build(), tokensCount); } else { return cb.build(); } } private final Mode mode; private final Config rawConfig; // what the user first creates private Config localConfig = null; // what gets generated during toLocalMode private Config clusterConfig = null; // what gets generated during toClusterMode private Config lazyCreateConfig(Mode newMode) { if (newMode == this.mode) { return this; } // this is here so that we don't keep cascading deeper into object creation so: // localConfig == toLocalMode(toClusterMode(localConfig)) Config newRawConfig = this.rawConfig; Config newLocalConfig = this.localConfig; Config newClusterConfig = this.clusterConfig; switch (this.mode) { case RAW: newRawConfig = this; break; case LOCAL: newLocalConfig = this; break; case CLUSTER: newClusterConfig = this; break; default: throw new IllegalArgumentException( "Unrecognized mode found in config: " + this.mode); } switch (newMode) { case LOCAL: if (this.localConfig == null) { Config tempConfig = Config.expand(Config.newBuilder().putAll(rawConfig.cfgMap).build()); this.localConfig = new Config(Mode.LOCAL, newRawConfig, tempConfig, newClusterConfig); } return this.localConfig; case CLUSTER: if (this.clusterConfig == null) { Config.Builder bc = Config.newBuilder() .putAll(rawConfig.cfgMap) .put(Key.HERON_HOME, get(Key.HERON_CLUSTER_HOME)) .put(Key.HERON_CONF, get(Key.HERON_CLUSTER_CONF)); Config tempConfig = Config.expand(bc.build()); this.clusterConfig = new Config(Mode.CLUSTER, newRawConfig, newLocalConfig, tempConfig); } return this.clusterConfig; case RAW: default: throw new IllegalArgumentException( "Unrecognized mode passed to lazyCreateConfig: " + newMode); } } public int size() { return cfgMap.size(); } public Object get(Key key) { return get(key.value()); } private Object get(String key) { switch (mode) { case LOCAL: return localConfig.cfgMap.get(key); case CLUSTER: return clusterConfig.cfgMap.get(key); case RAW: return rawConfig.cfgMap.get(key); default: throw new IllegalArgumentException(String.format( "Unrecognized mode passed to get for key=%s: %s", key, mode)); } } public String getStringValue(String key) { return (String) get(key); } public String getStringValue(Key key) { return (String) get(key); } public String getStringValue(String key, String defaultValue) { String value = getStringValue(key); return value != null ? value : defaultValue; } public Boolean getBooleanValue(Key key) { return (Boolean) get(key); } private Boolean getBooleanValue(String key) { return (Boolean) get(key); } public Boolean getBooleanValue(String key, boolean defaultValue) { Boolean value = getBooleanValue(key); return value != null ? value : defaultValue; } public ByteAmount getByteAmountValue(Key key) { Object value = get(key); return TypeUtils.getByteAmount(value); } DryRunFormatType getDryRunFormatType(Key key) { return (DryRunFormatType) get(key); } public PackageType getPackageType(Key key) { return (PackageType) get(key); } public Long getLongValue(Key key) { Object value = get(key); return TypeUtils.getLong(value); } public Long getLongValue(String key, long defaultValue) { Object value = get(key); if (value != null) { return TypeUtils.getLong(value); } return defaultValue; } public Integer getIntegerValue(Key key) { Object value = get(key); return TypeUtils.getInteger(value); } public Integer getIntegerValue(String key, int defaultValue) { Object value = get(key); if (value != null) { return TypeUtils.getInteger(value); } return defaultValue; } public Double getDoubleValue(Key key) { Object value = get(key); return TypeUtils.getDouble(value); } public Duration getDurationValue(String key, TemporalUnit unit, Duration defaultValue) { Object value = get(key); if (value != null) { return TypeUtils.getDuration(value, unit); } return defaultValue; } public boolean containsKey(Key key) { return cfgMap.containsKey(key); } public Set<String> getKeySet() { return cfgMap.keySet(); } @Override public String toString() { Map<String, Object> treeMap = new TreeMap<>(cfgMap); StringBuilder sb = new StringBuilder(); for (Map.Entry<String, Object> entry : treeMap.entrySet()) { sb.append("(\"").append(entry.getKey()).append("\""); sb.append(", ").append(entry.getValue()).append(")\n"); } return sb.toString(); } public static class Builder { private final Map<String, Object> keyValues = new HashMap<>(); private static Config.Builder create(boolean loadDefaults) { Config.Builder cb = new Builder(); if (loadDefaults) { loadDefaults(cb, Key.values()); } return cb; } private static void loadDefaults(Config.Builder cb, Key... keys) { for (Key key : keys) { if (key.getDefault() != null) { cb.put(key, key.getDefault()); } } } public Builder put(String key, Object value) { this.keyValues.put(key, value); return this; } public Builder put(Key key, Object value) { put(key.value(), value); return this; } public Builder putAll(Config ctx) { keyValues.putAll(ctx.cfgMap); return this; } public Builder putAll(Map<String, Object> map) { keyValues.putAll(map); return this; } public Config build() { return new Config(this); } } }