blob: 5a82937e48090fe906e6a2db0d6b528d2fd8519e [file] [log] [blame]
// 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();
}
public Set<Map.Entry<String, Object>> getEntrySet() {
return cfgMap.entrySet();
}
@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);
}
}
}