/*
* 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.common.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Preconditions;
import gobblin.config.store.api.ConfigKeyPath;
/**
* InMemoryTopology will return stale data if the internal config store is Not {@link ConfigStoreWithStableVersioning}
*
* @author mitu
*
*/
public class InMemoryTopology implements ConfigStoreTopologyInspector {
private final ConfigStoreTopologyInspector fallback;
// can not use Guava {@link com.google.common.collect.MultiMap} as MultiMap does not store entry pair if the value is empty
private final Map<ConfigKeyPath, Collection<ConfigKeyPath>> childrenMap = new HashMap<>();
private final Map<ConfigKeyPath, List<ConfigKeyPath>> ownImportMap = new HashMap<>();
private final Map<ConfigKeyPath, Collection<ConfigKeyPath>> ownImportedByMap = new HashMap<>();
private final Map<ConfigKeyPath, List<ConfigKeyPath>> recursiveImportMap = new HashMap<>();
private final Map<ConfigKeyPath, Collection<ConfigKeyPath>> recursiveImportedByMap = new HashMap<>();
private boolean initialedTopologyFromFallBack = false;
public InMemoryTopology(ConfigStoreTopologyInspector fallback) {
this.fallback = fallback;
}
private void loadRawTopologyFromFallBack() {
// only initialize the topology once
if (this.initialedTopologyFromFallBack) {
return;
}
// breath first search the whole topology to build ownImports map and ownImportedByMap
// calls to retrieve cache / set cache if not present
Collection<ConfigKeyPath> currentLevel = this.getChildren(SingleLinkedListConfigKeyPath.ROOT);
List<ConfigKeyPath> rootImports = this.getOwnImports(SingleLinkedListConfigKeyPath.ROOT);
Preconditions.checkArgument(rootImports == null || rootImports.size() == 0,
"Root can not import other nodes, otherwise circular dependency will happen");
while (!currentLevel.isEmpty()) {
Collection<ConfigKeyPath> nextLevel = new ArrayList<>();
for (ConfigKeyPath configKeyPath : currentLevel) {
// calls to retrieve cache / set cache if not present
List<ConfigKeyPath> ownImports = this.getOwnImports(configKeyPath);
for (ConfigKeyPath p : ownImports) {
addToCollectionMapForSingleValue(this.ownImportedByMap, p, configKeyPath);
}
// calls to retrieve cache / set cache if not present
Collection<ConfigKeyPath> tmp = this.getChildren(configKeyPath);
nextLevel.addAll(tmp);
}
currentLevel = nextLevel;
}
// traversal the ownImports map to get the recursive imports map and set recursive imported by map
for (ConfigKeyPath configKey : this.ownImportMap.keySet()) {
List<ConfigKeyPath> recursiveImports = null;
// result may in the cache already
if (this.recursiveImportMap.containsKey(configKey)) {
recursiveImports = this.recursiveImportMap.get(configKey);
} else {
recursiveImports = this.buildImportsRecursiveFromCache(configKey);
addToListMapForListValue(this.recursiveImportMap, configKey, recursiveImports);
}
if (recursiveImports != null) {
for (ConfigKeyPath p : recursiveImports) {
addToCollectionMapForSingleValue(this.recursiveImportedByMap, p, configKey);
}
}
}
this.initialedTopologyFromFallBack = true;
}
private List<ConfigKeyPath> buildImportsRecursiveFromCache(ConfigKeyPath configKey) {
return buildImportsRecursiveFromCacheHelper(configKey, configKey, new LinkedHashSet<ConfigKeyPath>());
}
private List<ConfigKeyPath> buildImportsRecursiveFromCacheHelper(ConfigKeyPath initialConfigKey,
ConfigKeyPath currentConfigKey, Set<ConfigKeyPath> previousVisited) {
for (ConfigKeyPath p : previousVisited) {
if (currentConfigKey != null && currentConfigKey.equals(p)) {
previousVisited.add(p);
throw new CircularDependencyException(
getCircularDependencyChain(initialConfigKey, previousVisited, currentConfigKey));
}
}
// root can not include anything, otherwise will have circular dependency
if (currentConfigKey.isRootPath()) {
return Collections.emptyList();
}
List<ConfigKeyPath> result = new ArrayList<>();
Set<ConfigKeyPath> currentVisited = new LinkedHashSet<>(previousVisited);
currentVisited.add(currentConfigKey);
// Own imports map should be filled in already
for (ConfigKeyPath u : this.getOwnImportsFromCache(currentConfigKey)) {
addRecursiveImportsToResult(u, initialConfigKey, currentConfigKey, result, currentVisited);
}
addRecursiveImportsToResult(currentConfigKey.getParent(), initialConfigKey, currentConfigKey, result,
currentVisited);
return dedup(result);
}
private void addRecursiveImportsToResult(ConfigKeyPath u, ConfigKeyPath initialConfigKey,
ConfigKeyPath currentConfigKey, List<ConfigKeyPath> result, Set<ConfigKeyPath> current) {
// do NOT add self parent in result as
// 1. by default import parent
// 2. if added, too many entries in the result
if (!u.equals(currentConfigKey.getParent())) {
result.add(u);
}
List<ConfigKeyPath> subResult = null;
if (this.recursiveImportMap.containsKey(u)) {
subResult = this.recursiveImportMap.get(u);
} else {
subResult = buildImportsRecursiveFromCacheHelper(initialConfigKey, u, current);
addToListMapForListValue(this.recursiveImportMap, u, subResult);
}
result.addAll(subResult);
}
private static void addToCollectionMapForCollectionValue(Map<ConfigKeyPath, Collection<ConfigKeyPath>> theMap,
ConfigKeyPath key, Collection<ConfigKeyPath> value) {
if (theMap.containsKey(key)) {
theMap.get(key).addAll(value);
} else {
theMap.put(key, value);
}
}
private static void addToCollectionMapForSingleValue(Map<ConfigKeyPath, Collection<ConfigKeyPath>> theMap,
ConfigKeyPath key, ConfigKeyPath value) {
if (theMap.containsKey(key)) {
theMap.get(key).add(value);
} else {
List<ConfigKeyPath> list = new ArrayList<>();
list.add(value);
theMap.put(key, list);
}
}
private static void addToListMapForListValue(Map<ConfigKeyPath, List<ConfigKeyPath>> theMap, ConfigKeyPath key,
List<ConfigKeyPath> value) {
if (theMap.containsKey(key)) {
theMap.get(key).addAll(value);
} else {
theMap.put(key, value);
}
}
private static List<ConfigKeyPath> dedup(List<ConfigKeyPath> input) {
List<ConfigKeyPath> result = new ArrayList<>();
Set<ConfigKeyPath> alreadySeen = new HashSet<>();
for (ConfigKeyPath u : input) {
if (!alreadySeen.contains(u)) {
result.add(u);
alreadySeen.add(u);
}
}
return result;
}
// return the circular dependency chain
private static String getCircularDependencyChain(ConfigKeyPath initialConfigKey, Set<ConfigKeyPath> chain,
ConfigKeyPath circular) {
StringBuilder sb = new StringBuilder();
sb.append("Initial configKey : " + initialConfigKey + ", loop is ");
for (ConfigKeyPath u : chain) {
sb.append(" -> " + u);
}
sb.append(" the configKey causing circular dependency: " + circular);
return sb.toString();
}
/**
* {@inheritDoc}.
*
* <p>
* If the result is already in cache, return the result.
* Otherwise, delegate the functionality to the fallback object
* </p>
*/
@Override
public Collection<ConfigKeyPath> getChildren(ConfigKeyPath configKey) {
if (this.childrenMap.containsKey(configKey)) {
return this.childrenMap.get(configKey);
}
Collection<ConfigKeyPath> result = this.fallback.getChildren(configKey);
addToCollectionMapForCollectionValue(this.childrenMap, configKey, result);
return result;
}
/**
* {@inheritDoc}.
*
* <p>
* If the result is already in cache, return the result.
* Otherwise, delegate the functionality to the fallback object
* </p>
*/
@Override
public List<ConfigKeyPath> getOwnImports(ConfigKeyPath configKey) {
if (this.ownImportMap.containsKey(configKey)) {
return this.ownImportMap.get(configKey);
}
List<ConfigKeyPath> result = this.fallback.getOwnImports(configKey);
addToListMapForListValue(this.ownImportMap, configKey, result);
return result;
}
private List<ConfigKeyPath> getOwnImportsFromCache(ConfigKeyPath configKey) {
if (this.ownImportMap.containsKey(configKey)) {
return this.ownImportMap.get(configKey);
}
return Collections.emptyList();
}
/**
* {@inheritDoc}.
*
* <p>
* If the result is already in cache, return the result.
* Otherwise, delegate the functionality to the fallback object.
*
* If the fallback did not support this operation, will build the entire topology of the {@link ConfigStore}
* using default breath first search.
* </p>
*/
@Override
public Collection<ConfigKeyPath> getImportedBy(ConfigKeyPath configKey) {
if (this.ownImportedByMap.containsKey(configKey)) {
return this.ownImportedByMap.get(configKey);
}
try {
Collection<ConfigKeyPath> result = this.fallback.getImportedBy(configKey);
addToCollectionMapForCollectionValue(this.ownImportedByMap, configKey, result);
return result;
} catch (UnsupportedOperationException uoe) {
loadRawTopologyFromFallBack();
return this.ownImportedByMap.get(configKey);
}
}
/**
* {@inheritDoc}.
*
* <p>
* If the result is already in cache, return the result.
* Otherwise, delegate the functionality to the fallback object.
*
* If the fallback did not support this operation, will build the entire topology of the {@link ConfigStore}
* using default breath first search.
* </p>
*/
@Override
public List<ConfigKeyPath> getImportsRecursively(ConfigKeyPath configKey) {
if (this.recursiveImportMap.containsKey(configKey)) {
return this.recursiveImportMap.get(configKey);
}
try {
List<ConfigKeyPath> result = this.fallback.getImportsRecursively(configKey);
addToListMapForListValue(this.recursiveImportMap, configKey, result);
return result;
} catch (UnsupportedOperationException uoe) {
loadRawTopologyFromFallBack();
return this.getImportsRecursivelyIncludePhantomFromCache(configKey);
}
}
// for phantom node, need to return the result of the parent
private List<ConfigKeyPath> getImportsRecursivelyIncludePhantomFromCache(ConfigKeyPath configKey) {
if (this.recursiveImportMap.containsKey(configKey)) {
return this.recursiveImportMap.get(configKey);
}
if (configKey.isRootPath()) {
return Collections.emptyList();
}
return getImportsRecursivelyIncludePhantomFromCache(configKey.getParent());
}
/**
* {@inheritDoc}.
*
* <p>
* If the result is already in cache, return the result.
* Otherwise, delegate the functionality to the fallback object.
*
* If the fallback did not support this operation, will build the entire topology of the {@link ConfigStore}
* using default breath first search.
* </p>
*/
@Override
public Collection<ConfigKeyPath> getImportedByRecursively(ConfigKeyPath configKey) {
if (this.recursiveImportedByMap.containsKey(configKey)) {
return this.recursiveImportedByMap.get(configKey);
}
try {
Collection<ConfigKeyPath> result = this.fallback.getImportedByRecursively(configKey);
addToCollectionMapForCollectionValue(this.recursiveImportedByMap, configKey, result);
return result;
} catch (UnsupportedOperationException uoe) {
loadRawTopologyFromFallBack();
return this.recursiveImportedByMap.get(configKey);
}
}
}