blob: aac60bc390ce697a6b9a8961bc42f54ae63ae222 [file] [log] [blame]
/*
* Copyright 2018 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.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import io.grpc.Attributes;
import io.grpc.ChannelLogger.ChannelLogLevel;
import io.grpc.ConnectivityState;
import io.grpc.ConnectivityStateInfo;
import io.grpc.EquivalentAddressGroup;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancer.Helper;
import io.grpc.LoadBalancer.PickResult;
import io.grpc.LoadBalancer.PickSubchannelArgs;
import io.grpc.LoadBalancer.SubchannelPicker;
import io.grpc.LoadBalancerProvider;
import io.grpc.LoadBalancerRegistry;
import io.grpc.Status;
import io.grpc.internal.ServiceConfigUtil.LbConfig;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.annotation.Nullable;
public final class AutoConfiguredLoadBalancerFactory extends LoadBalancer.Factory {
private static final Logger logger =
Logger.getLogger(AutoConfiguredLoadBalancerFactory.class.getName());
private final LoadBalancerRegistry registry;
private final String defaultPolicy;
public AutoConfiguredLoadBalancerFactory(String defaultPolicy) {
this(LoadBalancerRegistry.getDefaultRegistry(), defaultPolicy);
}
@VisibleForTesting
AutoConfiguredLoadBalancerFactory(LoadBalancerRegistry registry, String defaultPolicy) {
this.registry = checkNotNull(registry, "registry");
this.defaultPolicy = checkNotNull(defaultPolicy, "defaultPolicy");
}
@Override
public LoadBalancer newLoadBalancer(Helper helper) {
return new AutoConfiguredLoadBalancer(helper);
}
private static final class NoopLoadBalancer extends LoadBalancer {
@Override
public void handleResolvedAddressGroups(List<EquivalentAddressGroup> s, Attributes a) {}
@Override
public void handleNameResolutionError(Status error) {}
@Override
public void handleSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) {}
@Override
public void shutdown() {}
}
@VisibleForTesting
public final class AutoConfiguredLoadBalancer extends LoadBalancer {
private final Helper helper;
private LoadBalancer delegate;
private LoadBalancerProvider delegateProvider;
private boolean roundRobinDueToGrpclbDepMissing;
AutoConfiguredLoadBalancer(Helper helper) {
this.helper = helper;
delegateProvider = registry.getProvider(defaultPolicy);
if (delegateProvider == null) {
throw new IllegalStateException("Could not find policy '" + defaultPolicy
+ "'. Make sure its implementation is either registered to LoadBalancerRegistry or"
+ " included in META-INF/services/io.grpc.LoadBalancerProvider from your jar files.");
}
delegate = delegateProvider.newLoadBalancer(helper);
}
// Must be run inside ChannelExecutor.
@Override
public void handleResolvedAddressGroups(
List<EquivalentAddressGroup> servers, Attributes attributes) {
if (attributes.get(ATTR_LOAD_BALANCING_CONFIG) != null) {
throw new IllegalArgumentException(
"Unexpected ATTR_LOAD_BALANCING_CONFIG from upstream: "
+ attributes.get(ATTR_LOAD_BALANCING_CONFIG));
}
Map<String, Object> configMap = attributes.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG);
PolicySelection selection;
try {
selection = decideLoadBalancerProvider(servers, configMap);
} catch (PolicyException e) {
Status s = Status.INTERNAL.withDescription(e.getMessage());
helper.updateBalancingState(ConnectivityState.TRANSIENT_FAILURE, new FailingPicker(s));
delegate.shutdown();
delegateProvider = null;
delegate = new NoopLoadBalancer();
return;
}
if (delegateProvider == null
|| !selection.provider.getPolicyName().equals(delegateProvider.getPolicyName())) {
helper.updateBalancingState(ConnectivityState.CONNECTING, new EmptyPicker());
delegate.shutdown();
delegateProvider = selection.provider;
LoadBalancer old = delegate;
delegate = delegateProvider.newLoadBalancer(helper);
helper.getChannelLogger().log(
ChannelLogLevel.INFO, "Load balancer changed from {0} to {1}",
old.getClass().getSimpleName(), delegate.getClass().getSimpleName());
}
if (selection.config != null) {
helper.getChannelLogger().log(
ChannelLogLevel.DEBUG, "Load-balancing config: {0}", selection.config);
attributes =
attributes.toBuilder().set(ATTR_LOAD_BALANCING_CONFIG, selection.config).build();
}
LoadBalancer delegate = getDelegate();
if (selection.serverList.isEmpty()
&& !delegate.canHandleEmptyAddressListFromNameResolution()) {
delegate.handleNameResolutionError(
Status.UNAVAILABLE.withDescription(
"Name resolver returned no usable address. addrs="
+ servers + ", attrs=" + attributes));
} else {
delegate.handleResolvedAddressGroups(selection.serverList, attributes);
}
}
@Override
public void handleNameResolutionError(Status error) {
getDelegate().handleNameResolutionError(error);
}
@Override
public void handleSubchannelState(Subchannel subchannel, ConnectivityStateInfo stateInfo) {
getDelegate().handleSubchannelState(subchannel, stateInfo);
}
@Override
public boolean canHandleEmptyAddressListFromNameResolution() {
return true;
}
@Override
public void shutdown() {
delegate.shutdown();
delegate = null;
}
@VisibleForTesting
public LoadBalancer getDelegate() {
return delegate;
}
@VisibleForTesting
void setDelegate(LoadBalancer lb) {
delegate = lb;
}
@VisibleForTesting
LoadBalancerProvider getDelegateProvider() {
return delegateProvider;
}
/**
* Picks a load balancer based on given criteria. In order of preference:
*
* <ol>
* <li>User provided lb on the channel. This is a degenerate case and not handled here.</li>
* <li>"grpclb" if any gRPC LB balancer addresses are present</li>
* <li>The policy picked by the service config</li>
* <li>"pick_first" if the service config choice does not specify</li>
* </ol>
*
* @param servers The list of servers reported
* @param config the service config object
* @return the new load balancer factory, never null
*/
@VisibleForTesting
PolicySelection decideLoadBalancerProvider(
List<EquivalentAddressGroup> servers, @Nullable Map<String, Object> config)
throws PolicyException {
// Check for balancer addresses
boolean haveBalancerAddress = false;
List<EquivalentAddressGroup> backendAddrs = new ArrayList<>();
for (EquivalentAddressGroup s : servers) {
if (s.getAttributes().get(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY) != null) {
haveBalancerAddress = true;
} else {
backendAddrs.add(s);
}
}
if (haveBalancerAddress) {
// This is a special case where the existence of balancer address in the resolved address
// selects "grpclb" policy regardless of the service config.
LoadBalancerProvider grpclbProvider = registry.getProvider("grpclb");
if (grpclbProvider == null) {
if (backendAddrs.isEmpty()) {
throw new PolicyException(
"Received ONLY balancer addresses but grpclb runtime is missing");
}
if (!roundRobinDueToGrpclbDepMissing) {
roundRobinDueToGrpclbDepMissing = true;
String errorMsg = "Found balancer addresses but grpclb runtime is missing."
+ " Will use round_robin. Please include grpc-grpclb in your runtime depedencies.";
helper.getChannelLogger().log(ChannelLogLevel.ERROR, errorMsg);
logger.warning(errorMsg);
}
return new PolicySelection(
getProviderOrThrow(
"round_robin", "received balancer addresses but grpclb runtime is missing"),
backendAddrs, null);
} else {
return new PolicySelection(grpclbProvider, servers, null);
}
}
roundRobinDueToGrpclbDepMissing = false;
List<LbConfig> lbConfigs = null;
if (config != null) {
List<Map<String, Object>> rawLbConfigs =
ServiceConfigUtil.getLoadBalancingConfigsFromServiceConfig(config);
lbConfigs = ServiceConfigUtil.unwrapLoadBalancingConfigList(rawLbConfigs);
}
if (lbConfigs != null && !lbConfigs.isEmpty()) {
LinkedHashSet<String> policiesTried = new LinkedHashSet<>();
for (LbConfig lbConfig : lbConfigs) {
String policy = lbConfig.getPolicyName();
LoadBalancerProvider provider = registry.getProvider(policy);
if (provider != null) {
if (!policiesTried.isEmpty()) {
helper.getChannelLogger().log(
ChannelLogLevel.DEBUG,
"{0} specified by Service Config are not available", policiesTried);
}
return new PolicySelection(provider, servers, lbConfig.getRawConfigValue());
}
policiesTried.add(policy);
}
throw new PolicyException(
"None of " + policiesTried + " specified by Service Config are available.");
}
return new PolicySelection(
getProviderOrThrow(defaultPolicy, "using default policy"), servers, null);
}
}
private LoadBalancerProvider getProviderOrThrow(String policy, String choiceReason)
throws PolicyException {
LoadBalancerProvider provider = registry.getProvider(policy);
if (provider == null) {
throw new PolicyException(
"Trying to load '" + policy + "' because " + choiceReason + ", but it's unavailable");
}
return provider;
}
@VisibleForTesting
static class PolicyException extends Exception {
private static final long serialVersionUID = 1L;
private PolicyException(String msg) {
super(msg);
}
}
@VisibleForTesting
static final class PolicySelection {
final LoadBalancerProvider provider;
final List<EquivalentAddressGroup> serverList;
@Nullable final Map<String, Object> config;
PolicySelection(
LoadBalancerProvider provider, List<EquivalentAddressGroup> serverList,
@Nullable Map<String, Object> config) {
this.provider = checkNotNull(provider, "provider");
this.serverList = Collections.unmodifiableList(checkNotNull(serverList, "serverList"));
this.config = config;
}
}
private static final class EmptyPicker extends SubchannelPicker {
@Override
public PickResult pickSubchannel(PickSubchannelArgs args) {
return PickResult.withNoResult();
}
}
private static final class FailingPicker extends SubchannelPicker {
private final Status failure;
FailingPicker(Status failure) {
this.failure = failure;
}
@Override
public PickResult pickSubchannel(PickSubchannelArgs args) {
return PickResult.withError(failure);
}
}
}