blob: c7b70f606b99ba4f5a05ebc4d4b71e7475b90202 [file] [log] [blame]
/*
* 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.servicecomb.registry.discovery;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.servicecomb.foundation.common.cache.VersionedCache;
import org.apache.servicecomb.foundation.common.concurrent.ConcurrentHashMapEx;
import org.apache.servicecomb.foundation.common.exceptions.ServiceCombException;
import org.apache.servicecomb.registry.DiscoveryManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* <pre>
* DiscoveryTree is used to:
* 1.get all instances by app/microserviceName
* 2.filter all instances set, and output another set, the output set is instance or something else, this depend on filter set
*
* DiscoveryFilter have different types:
* 1.normal filter: just filter input set
* 2.grouping filter: will split input set to groups
* 3.convert filter: eg: convert from instance to endpoint
* different types can composite in one filter
*
* features:
* 1.if some filter output set is empty, DiscoveryTree can support try refilter logic
* eg: if there is no available instances in self AZ, can refilter in other AZ
* 2.every filter must try to cache result, avoid calculate every time.
*
* usage:
* 1.declare a field: DiscoveryTree discoveryTree = new DiscoveryTree();
* 2.initialize:
* discoveryTree.loadFromSPI(DiscoveryFilter.class);
* // add filters by your requirement
* discoveryTree.addFilter(new EndpointDiscoveryFilter());
* discoveryTree.sort();
* 3.filter for a invocation:
* DiscoveryContext context = new DiscoveryContext();
* context.setInputParameters(invocation);
* VersionedCache endpointsVersionedCache = discoveryTree.discovery(context,
* invocation.getAppId(),
* invocation.getMicroserviceName(),
* invocation.getMicroserviceVersionRule());
* if (endpointsVersionedCache.isEmpty()) {
* // 404 not found logic
* ......
* return;
* }
*
* // result is endpoints or something else, which is depends on your filter set
* List&lt;Endpoint&gt; endpoints = endpointsVersionedCache.data();
*</pre>
*/
public class DiscoveryTree {
private static final Logger LOGGER = LoggerFactory.getLogger(DiscoveryTree.class);
private final Map<String, Map<String, DiscoveryTreeNode>> root = new ConcurrentHashMapEx<>();
private final Object lock = new Object();
private List<DiscoveryFilter> filters = Collections.emptyList();
private final DiscoveryManager discoveryManager;
public DiscoveryTree(DiscoveryManager discoveryManager) {
this.discoveryManager = discoveryManager;
}
@Autowired
public void setDiscoveryFilters(List<DiscoveryFilter> filters) {
this.filters = filters;
log();
}
private void log() {
StringBuilder sb = new StringBuilder();
sb.append("DiscoveryFilters(name, enabled, order, group):");
for (DiscoveryFilter filter : filters) {
sb.append("(").append(filter.getClass().getName()).append(",")
.append(filter.enabled()).append(",").append(filter.getOrder()).append(",")
.append(filter.isGroupingFilter()).append(")");
}
LOGGER.info(sb.toString());
}
boolean isMatch(VersionedCache existing, VersionedCache inputCache) {
return existing != null && existing.isSameVersion(inputCache);
}
boolean isExpired(VersionedCache existing, VersionedCache inputCache) {
return existing == null || existing.isExpired(inputCache);
}
public DiscoveryTreeNode discovery(DiscoveryContext context, String appId, String microserviceName) {
VersionedCache instanceVersionedCache = this.discoveryManager.getOrCreateVersionedCache(appId, microserviceName);
return discovery(appId, microserviceName, context, instanceVersionedCache);
}
DiscoveryTreeNode discovery(String appId, String microserviceName, DiscoveryContext context,
VersionedCache inputCache) {
DiscoveryTreeNode tmpRoot = getOrCreateRoot(appId, microserviceName, inputCache);
DiscoveryTreeNode parent = tmpRoot.children()
.computeIfAbsent(inputCache.name(), name -> new DiscoveryTreeNode().fromCache(inputCache));
return doDiscovery(context, parent);
}
protected DiscoveryTreeNode getOrCreateRoot(String appId, String microserviceName, VersionedCache inputCache) {
DiscoveryTreeNode tmpRoot = root.computeIfAbsent(appId, k -> new ConcurrentHashMapEx<>()).get(microserviceName);
if (isMatch(tmpRoot, inputCache)) {
return tmpRoot;
}
synchronized (lock) {
if (isExpired(tmpRoot, inputCache)) {
// not initialized or inputCache newer than root, create new root
tmpRoot = new DiscoveryTreeNode().cacheVersion(inputCache.cacheVersion());
root.get(appId).put(microserviceName, tmpRoot);
return tmpRoot;
}
if (tmpRoot.isSameVersion(inputCache)) {
// reuse root directly
return tmpRoot;
}
}
// root newer than inputCache, it's a minimal probability event:
// 1) thread 1, use v1 inputCache, run into getOrCreateRoot, but did not run any code yet, suspend and switch to thread 2
// 2) thread 2, use v2 inputCache, v2 > v1, create new root
// 3) thread 1 go on, then root is newer than inputCache
// but if create old children in new version root, it's a wrong logic
// so just create a temporary root for the inputCache, DO NOT assign to root
tmpRoot = new DiscoveryTreeNode().cacheVersion(inputCache.cacheVersion());
root.get(appId).put(microserviceName, tmpRoot);
return tmpRoot;
}
protected DiscoveryTreeNode doDiscovery(DiscoveryContext context, DiscoveryTreeNode parent) {
for (int idx = 0; idx < filters.size(); ) {
DiscoveryFilter filter = filters.get(idx);
if (!filter.enabled()) {
idx++;
continue;
}
context.setCurrentNode(parent);
DiscoveryTreeNode child = filter.discovery(context, parent);
if (child == null) {
// impossible, throw exception to help fix bug
throw new ServiceCombException(filter.getClass().getName() + " discovery return null.");
}
child.level(idx + 1);
if (!filter.isGroupingFilter()) {
child.name(parent.name());
}
if (child.isEmpty()) {
// maybe need to rerun some filter
DiscoveryTreeNode rerunNode = context.popRerunFilter();
if (rerunNode != null) {
parent = rerunNode;
idx = parent.level();
continue;
}
// no rerun support, go on even result is empty
// because maybe some filter use other mechanism to create an instance(eg:domain name)
}
parent = child;
idx++;
}
return parent;
}
}