/*
* 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 gobblin.config.client;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import org.apache.log4j.Logger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.typesafe.config.Config;
import gobblin.config.client.api.ConfigStoreFactoryDoesNotExistsException;
import gobblin.config.client.api.VersionStabilityPolicy;
import gobblin.config.common.impl.ConfigStoreBackedTopology;
import gobblin.config.common.impl.ConfigStoreBackedValueInspector;
import gobblin.config.common.impl.ConfigStoreTopologyInspector;
import gobblin.config.common.impl.ConfigStoreValueInspector;
import gobblin.config.common.impl.InMemoryTopology;
import gobblin.config.common.impl.InMemoryValueInspector;
import gobblin.config.store.api.ConfigKeyPath;
import gobblin.config.store.api.ConfigStore;
import gobblin.config.store.api.ConfigStoreCreationException;
import gobblin.config.store.api.ConfigStoreFactory;
import gobblin.config.store.api.ConfigStoreWithStableVersioning;
import gobblin.config.store.api.VersionDoesNotExistException;
/**
* This class is used by Client to access the Configuration Management core library.
*
*
* @author mitu
*
*/
public class ConfigClient {
private static final Logger LOG = Logger.getLogger(ConfigClient.class);
private final VersionStabilityPolicy policy;
/** Normally key is the ConfigStore.getStoreURI(), value is the ConfigStoreAccessor
*
* However, there may be two entries for a specific config store, for example
* if user pass in URI like "etl-hdfs:///datasets/a1/a2" and the etl-hdfs config store factory using
* default authority/default config store root normalized the URI to
* "etl-hdfs://eat1-nertznn01.grid.linkedin.com:9000/user/mitu/HdfsBasedConfigTest/datasets/a1/a2"
* where /user/mitu/HdfsBasedConfigTest is the config store root
*
* Then there will be two entries in the Map which point to the same value
* key1: "etl-hdfs:/"
* key2: "etl-hdfs://eat1-nertznn01.grid.linkedin.com:9000/user/mitu/HdfsBasedConfigTest/"
*
*/
private final TreeMap<URI, ConfigStoreAccessor> configStoreAccessorMap = new TreeMap<>();
private final ConfigStoreFactoryRegister configStoreFactoryRegister;
private ConfigClient(VersionStabilityPolicy policy) {
this(policy, new ConfigStoreFactoryRegister());
}
@VisibleForTesting
ConfigClient(VersionStabilityPolicy policy, ConfigStoreFactoryRegister register) {
this.policy = policy;
this.configStoreFactoryRegister = register;
}
/**
* Create the {@link ConfigClient} based on the {@link VersionStabilityPolicy}.
* @param policy - {@link VersionStabilityPolicy} to specify the stability policy which control the caching layer creation
* @return - {@link ConfigClient} for client to use to access the {@link ConfigStore}
*/
public static ConfigClient createConfigClient(VersionStabilityPolicy policy) {
return new ConfigClient(policy);
}
/**
* Get the resolved {@link Config} based on the input URI.
*
* @param configKeyUri - The URI for the configuration key. There are two types of URI:
*
* 1. URI missing authority and configuration store root , for example "etl-hdfs:///datasets/a1/a2". It will get
* the configuration based on the default {@link ConfigStore} in etl-hdfs {@link ConfigStoreFactory}
* 2. Complete URI: for example "etl-hdfs://eat1-nertznn01.grid.linkedin.com:9000/user/mitu/HdfsBasedConfigTest/"
*
* @return the resolved {@link Config} based on the input URI.
*
* @throws ConfigStoreFactoryDoesNotExistsException: if missing scheme name or the scheme name is invalid
* @throws ConfigStoreCreationException: Specified {@link ConfigStoreFactory} can not create required {@link ConfigStore}
* @throws VersionDoesNotExistException: Required version does not exist anymore ( may get deleted by retention job )
*/
public Config getConfig(URI configKeyUri)
throws ConfigStoreFactoryDoesNotExistsException, ConfigStoreCreationException, VersionDoesNotExistException {
ConfigStoreAccessor accessor = this.getConfigStoreAccessor(configKeyUri);
ConfigKeyPath configKeypath = ConfigClientUtils.buildConfigKeyPath(configKeyUri, accessor.configStore);
return accessor.valueInspector.getResolvedConfig(configKeypath);
}
/**
* batch process for {@link #getConfig(URI)} method
* @param configKeyUris
* @return
* @throws ConfigStoreFactoryDoesNotExistsException
* @throws ConfigStoreCreationException
* @throws VersionDoesNotExistException
*/
public Map<URI, Config> getConfigs(Collection<URI> configKeyUris)
throws ConfigStoreFactoryDoesNotExistsException, ConfigStoreCreationException, VersionDoesNotExistException {
if (configKeyUris == null || configKeyUris.size() == 0)
return Collections.emptyMap();
Map<URI, Config> result = new HashMap<>();
Multimap<ConfigStoreAccessor, ConfigKeyPath> partitionedAccessor = ArrayListMultimap.create();
// map contains the mapping between ConfigKeyPath back to original URI , partitioned by ConfigStoreAccessor
Map<ConfigStoreAccessor, Map<ConfigKeyPath, URI>> reverseMap = new HashMap<>();
// partitioned the ConfigKeyPaths which belongs to the same store to one accessor
for (URI u : configKeyUris) {
ConfigStoreAccessor accessor = this.getConfigStoreAccessor(u);
ConfigKeyPath configKeypath = ConfigClientUtils.buildConfigKeyPath(u, accessor.configStore);
partitionedAccessor.put(accessor, configKeypath);
if (!reverseMap.containsKey(accessor)) {
reverseMap.put(accessor, new HashMap<ConfigKeyPath, URI>());
}
reverseMap.get(accessor).put(configKeypath, u);
}
for (Map.Entry<ConfigStoreAccessor, Collection<ConfigKeyPath>> entry : partitionedAccessor.asMap().entrySet()) {
Map<ConfigKeyPath, Config> batchResult = entry.getKey().valueInspector.getResolvedConfigs(entry.getValue());
for (Map.Entry<ConfigKeyPath, Config> resultEntry : batchResult.entrySet()) {
// get the original URI from reverseMap
URI orgURI = reverseMap.get(entry.getKey()).get(resultEntry.getKey());
result.put(orgURI, resultEntry.getValue());
}
}
return result;
}
/**
* Convenient method to get resolved {@link Config} based on String input.
*/
public Config getConfig(String configKeyStr) throws ConfigStoreFactoryDoesNotExistsException,
ConfigStoreCreationException, VersionDoesNotExistException, URISyntaxException {
return this.getConfig(new URI(configKeyStr));
}
/**
* batch process for {@link #getConfig(String)} method
* @param configKeyStrs
* @return
* @throws ConfigStoreFactoryDoesNotExistsException
* @throws ConfigStoreCreationException
* @throws VersionDoesNotExistException
* @throws URISyntaxException
*/
public Map<URI, Config> getConfigsFromStrings(Collection<String> configKeyStrs)
throws ConfigStoreFactoryDoesNotExistsException, ConfigStoreCreationException, VersionDoesNotExistException,
URISyntaxException {
if (configKeyStrs == null || configKeyStrs.size() == 0)
return Collections.emptyMap();
Collection<URI> configKeyUris = new ArrayList<>();
for (String s : configKeyStrs) {
configKeyUris.add(new URI(s));
}
return getConfigs(configKeyUris);
}
/**
* Get the import links of the input URI.
*
* @param configKeyUri - The URI for the configuration key.
* @param recursive - Specify whether to get direct import links or recursively import links
* @return the import links of the input URI.
*
* @throws ConfigStoreFactoryDoesNotExistsException: if missing scheme name or the scheme name is invalid
* @throws ConfigStoreCreationException: Specified {@link ConfigStoreFactory} can not create required {@link ConfigStore}
* @throws VersionDoesNotExistException: Required version does not exist anymore ( may get deleted by retention job )
*/
public Collection<URI> getImports(URI configKeyUri, boolean recursive)
throws ConfigStoreFactoryDoesNotExistsException, ConfigStoreCreationException, VersionDoesNotExistException {
ConfigStoreAccessor accessor = this.getConfigStoreAccessor(configKeyUri);
ConfigKeyPath configKeypath = ConfigClientUtils.buildConfigKeyPath(configKeyUri, accessor.configStore);
Collection<ConfigKeyPath> result;
if (!recursive) {
result = accessor.topologyInspector.getOwnImports(configKeypath);
} else {
result = accessor.topologyInspector.getImportsRecursively(configKeypath);
}
return ConfigClientUtils.buildUriInClientFormat(result, accessor.configStore, configKeyUri.getAuthority() != null);
}
/**
* Get the URIs which imports the input URI
*
* @param configKeyUri - The URI for the configuration key.
* @param recursive - Specify whether to get direct or recursively imported by links
* @return the URIs which imports the input URI
*
* @throws ConfigStoreFactoryDoesNotExistsException: if missing scheme name or the scheme name is invalid
* @throws ConfigStoreCreationException: Specified {@link ConfigStoreFactory} can not create required {@link ConfigStore}
* @throws VersionDoesNotExistException: Required version does not exist anymore ( may get deleted by retention job )
*/
public Collection<URI> getImportedBy(URI configKeyUri, boolean recursive)
throws ConfigStoreFactoryDoesNotExistsException, ConfigStoreCreationException, VersionDoesNotExistException {
ConfigStoreAccessor accessor = this.getConfigStoreAccessor(configKeyUri);
ConfigKeyPath configKeypath = ConfigClientUtils.buildConfigKeyPath(configKeyUri, accessor.configStore);
Collection<ConfigKeyPath> result;
if (!recursive) {
result = accessor.topologyInspector.getImportedBy(configKeypath);
} else {
result = accessor.topologyInspector.getImportedByRecursively(configKeypath);
}
return ConfigClientUtils.buildUriInClientFormat(result, accessor.configStore, configKeyUri.getAuthority() != null);
}
private URI getMatchedFloorKeyFromCache(URI configKeyURI) {
URI floorKey = this.configStoreAccessorMap.floorKey(configKeyURI);
if (floorKey == null) {
return null;
}
// both scheme name and authority name, if present, should match
// or both authority should be null
if (ConfigClientUtils.isAncestorOrSame(configKeyURI, floorKey)) {
return floorKey;
}
return null;
}
private ConfigStoreAccessor createNewConfigStoreAccessor(URI configKeyURI)
throws ConfigStoreFactoryDoesNotExistsException, ConfigStoreCreationException, VersionDoesNotExistException {
LOG.info("Create new config store accessor for URI " + configKeyURI);
ConfigStoreAccessor result;
ConfigStoreFactory<ConfigStore> csFactory = this.getConfigStoreFactory(configKeyURI);
ConfigStore cs = csFactory.createConfigStore(configKeyURI);
if (!isConfigStoreWithStableVersion(cs)) {
if (this.policy == VersionStabilityPolicy.CROSS_JVM_STABILITY) {
throw new RuntimeException(String.format("with policy set to %s, can not connect to unstable config store %s",
VersionStabilityPolicy.CROSS_JVM_STABILITY, cs.getStoreURI()));
}
}
String currentVersion = cs.getCurrentVersion();
// topology related
ConfigStoreBackedTopology csTopology = new ConfigStoreBackedTopology(cs, currentVersion);
InMemoryTopology inMemoryTopology = new InMemoryTopology(csTopology);
// value related
ConfigStoreBackedValueInspector rawValueInspector =
new ConfigStoreBackedValueInspector(cs, currentVersion, inMemoryTopology);
InMemoryValueInspector inMemoryValueInspector;
// ConfigStoreWithStableVersioning always create Soft reference cache
if (isConfigStoreWithStableVersion(cs) || this.policy == VersionStabilityPolicy.WEAK_LOCAL_STABILITY) {
inMemoryValueInspector = new InMemoryValueInspector(rawValueInspector, false);
result = new ConfigStoreAccessor(cs, inMemoryValueInspector, inMemoryTopology);
}
// Non ConfigStoreWithStableVersioning but require STRONG_LOCAL_STABILITY, use Strong reference cache
else if (this.policy == VersionStabilityPolicy.STRONG_LOCAL_STABILITY) {
inMemoryValueInspector = new InMemoryValueInspector(rawValueInspector, true);
result = new ConfigStoreAccessor(cs, inMemoryValueInspector, inMemoryTopology);
}
// Require No cache
else {
result = new ConfigStoreAccessor(cs, rawValueInspector, inMemoryTopology);
}
return result;
}
private static boolean isConfigStoreWithStableVersion(ConfigStore cs) {
for (Annotation annotation : cs.getClass().getDeclaredAnnotations()) {
if (annotation instanceof ConfigStoreWithStableVersioning) {
return true;
}
}
return false;
}
private ConfigStoreAccessor getConfigStoreAccessor(URI configKeyURI)
throws ConfigStoreFactoryDoesNotExistsException, ConfigStoreCreationException, VersionDoesNotExistException {
URI matchedFloorKey = getMatchedFloorKeyFromCache(configKeyURI);
ConfigStoreAccessor result;
if (matchedFloorKey != null) {
result = this.configStoreAccessorMap.get(matchedFloorKey);
return result;
}
result = createNewConfigStoreAccessor(configKeyURI);
ConfigStore cs = result.configStore;
// put default root URI in cache as well for the URI which missing authority
if (configKeyURI.getAuthority() == null) {
// configKeyURI is missing authority/configstore root "etl-hdfs:///datasets/a1/a2"
try {
this.configStoreAccessorMap.put(new URI(configKeyURI.getScheme(), null, "/", null, null), result);
} catch (URISyntaxException e) {
// should not come here
throw new RuntimeException("Can not build URI based on " + configKeyURI);
}
} else {
// need to check Config Store's root is the prefix of input configKeyURI
if (!ConfigClientUtils.isAncestorOrSame(configKeyURI, cs.getStoreURI())) {
throw new RuntimeException(
String.format("Config Store root URI %s is not the prefix of input %s", cs.getStoreURI(), configKeyURI));
}
}
// put to cache
this.configStoreAccessorMap.put(cs.getStoreURI(), result);
return result;
}
// use serviceLoader to load configStoreFactories
@SuppressWarnings("unchecked")
private ConfigStoreFactory<ConfigStore> getConfigStoreFactory(URI configKeyUri)
throws ConfigStoreFactoryDoesNotExistsException {
@SuppressWarnings("rawtypes")
ConfigStoreFactory csf = this.configStoreFactoryRegister.getConfigStoreFactory(configKeyUri.getScheme());
if (csf == null) {
throw new ConfigStoreFactoryDoesNotExistsException(configKeyUri.getScheme(), "scheme name does not exists");
}
return csf;
}
static class ConfigStoreAccessor {
final ConfigStore configStore;
final ConfigStoreValueInspector valueInspector;
final ConfigStoreTopologyInspector topologyInspector;
ConfigStoreAccessor(ConfigStore cs, ConfigStoreValueInspector valueInspector,
ConfigStoreTopologyInspector topologyInspector) {
this.configStore = cs;
this.valueInspector = valueInspector;
this.topologyInspector = topologyInspector;
}
}
}