| /* |
| * 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<Endpoint> 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; |
| } |
| } |