blob: 10050618e2615b8e941392bcf307ecdbf3156cb0 [file] [log] [blame]
/*
* Copyright 2020 The gRPC Authors
*
* 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 io.grpc.xds;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.ConnectivityState.CONNECTING;
import static io.grpc.ConnectivityState.IDLE;
import static io.grpc.ConnectivityState.READY;
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
import static io.grpc.xds.XdsSubchannelPickers.BUFFER_PICKER;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.grpc.ConnectivityState;
import io.grpc.InternalLogId;
import io.grpc.LoadBalancer;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
import io.grpc.util.ForwardingLoadBalancerHelper;
import io.grpc.util.GracefulSwitchLoadBalancer;
import io.grpc.xds.XdsLogger.XdsLogLevel;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.MethodName;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.Route;
import io.grpc.xds.XdsRoutingLoadBalancerProvider.XdsRoutingConfig;
import io.grpc.xds.XdsSubchannelPickers.ErrorPicker;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/** Load balancer for xds_routing policy. */
final class XdsRoutingLoadBalancer extends LoadBalancer {
private final XdsLogger logger;
private final Helper helper;
private final Map<String, GracefulSwitchLoadBalancer> routeBalancers = new HashMap<>();
private final Map<String, RouteHelper> routeHelpers = new HashMap<>();
private Map<String, PolicySelection> actions = ImmutableMap.of();
private List<Route> routes = ImmutableList.of();
XdsRoutingLoadBalancer(Helper helper) {
this.helper = checkNotNull(helper, "helper");
logger = XdsLogger.withLogId(
InternalLogId.allocate("xds-routing-lb", helper.getAuthority()));
logger.log(XdsLogLevel.INFO, "Created");
}
@Override
public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
XdsRoutingConfig xdsRoutingConfig =
(XdsRoutingConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
checkNotNull(xdsRoutingConfig, "Missing xds_routing lb config");
Map<String, PolicySelection> newActions = xdsRoutingConfig.actions;
for (String actionName : newActions.keySet()) {
PolicySelection action = newActions.get(actionName);
if (!actions.containsKey(actionName)) {
RouteHelper routeHelper = new RouteHelper();
GracefulSwitchLoadBalancer routeBalancer = new GracefulSwitchLoadBalancer(routeHelper);
routeBalancer.switchTo(action.getProvider());
routeHelpers.put(actionName, routeHelper);
routeBalancers.put(actionName, routeBalancer);
} else if (!action.getProvider().equals(actions.get(actionName).getProvider())) {
routeBalancers.get(actionName).switchTo(action.getProvider());
}
}
this.routes = xdsRoutingConfig.routes;
this.actions = newActions;
for (String actionName : actions.keySet()) {
routeBalancers.get(actionName).handleResolvedAddresses(
resolvedAddresses.toBuilder()
.setLoadBalancingPolicyConfig(actions.get(actionName).getConfig())
.build());
}
// Cleanup removed actions.
// TODO(zdapeng): cache removed actions for 15 minutes.
for (String actionName : routeBalancers.keySet()) {
if (!actions.containsKey(actionName)) {
routeBalancers.get(actionName).shutdown();
}
}
routeBalancers.keySet().retainAll(actions.keySet());
routeHelpers.keySet().retainAll(actions.keySet());
}
@Override
public void handleNameResolutionError(Status error) {
logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error);
if (routeBalancers.isEmpty()) {
helper.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(error));
}
for (LoadBalancer routeBalancer : routeBalancers.values()) {
routeBalancer.handleNameResolutionError(error);
}
}
@Override
public void shutdown() {
logger.log(XdsLogLevel.INFO, "Shutdown");
for (LoadBalancer routeBalancer : routeBalancers.values()) {
routeBalancer.shutdown();
}
}
@Override
public boolean canHandleEmptyAddressListFromNameResolution() {
return true;
}
private void updateOverallBalancingState() {
ConnectivityState overallState = null;
// Use LinkedHashMap to preserve the order of routes.
Map<MethodName, SubchannelPicker> routePickers = new LinkedHashMap<>();
for (Route route : routes) {
RouteHelper routeHelper = routeHelpers.get(route.actionName);
routePickers.put(route.methodName, routeHelper.currentPicker);
ConnectivityState routeState = routeHelper.currentState;
overallState = aggregateState(overallState, routeState);
}
if (overallState != null) {
SubchannelPicker picker = new PathMatchingSubchannelPicker(routePickers);
helper.updateBalancingState(overallState, picker);
}
}
@Nullable
private static ConnectivityState aggregateState(
@Nullable ConnectivityState overallState, ConnectivityState childState) {
if (overallState == null) {
return childState;
}
if (overallState == READY || childState == READY) {
return READY;
}
if (overallState == CONNECTING || childState == CONNECTING) {
return CONNECTING;
}
if (overallState == IDLE || childState == IDLE) {
return IDLE;
}
return overallState;
}
/**
* The lb helper for a single route balancer.
*/
private final class RouteHelper extends ForwardingLoadBalancerHelper {
ConnectivityState currentState = CONNECTING;
SubchannelPicker currentPicker = BUFFER_PICKER;
@Override
public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
currentState = newState;
currentPicker = newPicker;
updateOverallBalancingState();
}
@Override
protected Helper delegate() {
return helper;
}
}
private static final class PathMatchingSubchannelPicker extends SubchannelPicker {
final Map<MethodName, SubchannelPicker> routePickers;
/**
* Constructs a picker that will match the path of PickSubchannelArgs with the given map.
* The order of the map entries matters. First match will be picked even if second match is an
* exact (service + method) path match.
*/
PathMatchingSubchannelPicker(Map<MethodName, SubchannelPicker> routePickers) {
this.routePickers = routePickers;
}
@Override
public PickResult pickSubchannel(PickSubchannelArgs args) {
for (MethodName methodName : routePickers.keySet()) {
if (match(args.getMethodDescriptor(), methodName)) {
return routePickers.get(methodName).pickSubchannel(args);
}
}
// At least the default route should match, otherwise there is a bug.
throw new IllegalStateException("PathMatchingSubchannelPicker: error in matching path");
}
boolean match(MethodDescriptor<?, ?> methodDescriptor, MethodName methodName) {
if (methodName.service.isEmpty() && methodName.method.isEmpty()) {
return true;
}
if (methodName.method.isEmpty()) {
return methodName.service.equals(methodDescriptor.getServiceName());
}
return (methodName.service + '/' + methodName.method)
.equals(methodDescriptor.getFullMethodName());
}
}
}