mirror of https://github.com/grpc/grpc-java.git
xds: implement LRS LB policy (#6858)
Part of xDS LB policy refactoring work. Implement the LRS LB policy for "balancing" endpoints within a certain locality.
This commit is contained in:
parent
58a92b7530
commit
d88f0f19ec
|
|
@ -51,28 +51,26 @@ final class EnvoyProtoData {
|
||||||
static final class Locality {
|
static final class Locality {
|
||||||
private final String region;
|
private final String region;
|
||||||
private final String zone;
|
private final String zone;
|
||||||
private final String subzone;
|
private final String subZone;
|
||||||
|
|
||||||
/** Must only be used for testing. */
|
Locality(String region, String zone, String subZone) {
|
||||||
@VisibleForTesting
|
|
||||||
Locality(String region, String zone, String subzone) {
|
|
||||||
this.region = region;
|
this.region = region;
|
||||||
this.zone = zone;
|
this.zone = zone;
|
||||||
this.subzone = subzone;
|
this.subZone = subZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Locality fromEnvoyProtoLocality(io.envoyproxy.envoy.api.v2.core.Locality locality) {
|
static Locality fromEnvoyProtoLocality(io.envoyproxy.envoy.api.v2.core.Locality locality) {
|
||||||
return new Locality(
|
return new Locality(
|
||||||
/* region = */ locality.getRegion(),
|
/* region = */ locality.getRegion(),
|
||||||
/* zone = */ locality.getZone(),
|
/* zone = */ locality.getZone(),
|
||||||
/* subzone = */ locality.getSubZone());
|
/* subZone = */ locality.getSubZone());
|
||||||
}
|
}
|
||||||
|
|
||||||
io.envoyproxy.envoy.api.v2.core.Locality toEnvoyProtoLocality() {
|
io.envoyproxy.envoy.api.v2.core.Locality toEnvoyProtoLocality() {
|
||||||
return io.envoyproxy.envoy.api.v2.core.Locality.newBuilder()
|
return io.envoyproxy.envoy.api.v2.core.Locality.newBuilder()
|
||||||
.setRegion(region)
|
.setRegion(region)
|
||||||
.setZone(zone)
|
.setZone(zone)
|
||||||
.setSubZone(subzone)
|
.setSubZone(subZone)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,8 +82,8 @@ final class EnvoyProtoData {
|
||||||
return zone;
|
return zone;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getSubzone() {
|
String getSubZone() {
|
||||||
return subzone;
|
return subZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -99,12 +97,12 @@ final class EnvoyProtoData {
|
||||||
Locality locality = (Locality) o;
|
Locality locality = (Locality) o;
|
||||||
return Objects.equals(region, locality.region)
|
return Objects.equals(region, locality.region)
|
||||||
&& Objects.equals(zone, locality.zone)
|
&& Objects.equals(zone, locality.zone)
|
||||||
&& Objects.equals(subzone, locality.subzone);
|
&& Objects.equals(subZone, locality.subZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(region, zone, subzone);
|
return Objects.hash(region, zone, subZone);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -112,7 +110,7 @@ final class EnvoyProtoData {
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
.add("region", region)
|
.add("region", region)
|
||||||
.add("zone", zone)
|
.add("zone", zone)
|
||||||
.add("subzone", subzone)
|
.add("subZone", subZone)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,7 @@ interface LocalityStore {
|
||||||
@Override
|
@Override
|
||||||
public String getAuthority() {
|
public String getAuthority() {
|
||||||
//FIXME: This should be a new proposed field of Locality, locality_name
|
//FIXME: This should be a new proposed field of Locality, locality_name
|
||||||
return locality.getSubzone();
|
return locality.getSubZone();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
orcaReportingHelperWrapper =
|
orcaReportingHelperWrapper =
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* 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 com.google.common.base.Preconditions.checkState;
|
||||||
|
|
||||||
|
import io.grpc.ConnectivityState;
|
||||||
|
import io.grpc.LoadBalancer;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.util.ForwardingLoadBalancerHelper;
|
||||||
|
import io.grpc.util.GracefulSwitchLoadBalancer;
|
||||||
|
import io.grpc.xds.ClientLoadCounter.LoadRecordingSubchannelPicker;
|
||||||
|
import io.grpc.xds.EnvoyProtoData.Locality;
|
||||||
|
import io.grpc.xds.LrsLoadBalancerProvider.LrsConfig;
|
||||||
|
import io.grpc.xds.XdsSubchannelPickers.ErrorPicker;
|
||||||
|
import java.util.Objects;
|
||||||
|
import javax.annotation.CheckForNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load balancer for lrs policy.
|
||||||
|
*/
|
||||||
|
final class LrsLoadBalancer extends LoadBalancer {
|
||||||
|
private final LoadBalancer.Helper helper;
|
||||||
|
@CheckForNull
|
||||||
|
private GracefulSwitchLoadBalancer switchingLoadBalancer;
|
||||||
|
private LoadStatsStore loadStatsStore;
|
||||||
|
private String clusterName;
|
||||||
|
private String edsServiceName;
|
||||||
|
private Locality locality;
|
||||||
|
private String childPolicyName;
|
||||||
|
|
||||||
|
LrsLoadBalancer(LoadBalancer.Helper helper) {
|
||||||
|
this.helper = checkNotNull(helper, "helper");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
|
||||||
|
LrsConfig config = (LrsConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
|
||||||
|
LoadStatsStore store =
|
||||||
|
resolvedAddresses.getAttributes().get(XdsAttributes.ATTR_CLUSTER_SERVICE_LOAD_STATS_STORE);
|
||||||
|
checkNotNull(config, "missing LRS lb config");
|
||||||
|
checkNotNull(store, "missing cluster service stats object");
|
||||||
|
checkAndSetUp(config, store);
|
||||||
|
|
||||||
|
if (switchingLoadBalancer == null) {
|
||||||
|
loadStatsStore.addLocality(config.locality);
|
||||||
|
final ClientLoadCounter counter = loadStatsStore.getLocalityCounter(config.locality);
|
||||||
|
LoadBalancer.Helper loadRecordingHelper = new ForwardingLoadBalancerHelper() {
|
||||||
|
@Override
|
||||||
|
protected Helper delegate() {
|
||||||
|
return helper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
|
||||||
|
SubchannelPicker loadRecordingPicker =
|
||||||
|
new LoadRecordingSubchannelPicker(counter, newPicker);
|
||||||
|
super.updateBalancingState(newState, loadRecordingPicker);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
switchingLoadBalancer = new GracefulSwitchLoadBalancer(loadRecordingHelper);
|
||||||
|
}
|
||||||
|
String updatedChildPolicyName = config.childPolicy.getProvider().getPolicyName();
|
||||||
|
if (!Objects.equals(childPolicyName, updatedChildPolicyName)) {
|
||||||
|
switchingLoadBalancer.switchTo(config.childPolicy.getProvider());
|
||||||
|
childPolicyName = updatedChildPolicyName;
|
||||||
|
}
|
||||||
|
ResolvedAddresses downStreamResult =
|
||||||
|
resolvedAddresses.toBuilder()
|
||||||
|
.setLoadBalancingPolicyConfig(config.childPolicy.getConfig())
|
||||||
|
.build();
|
||||||
|
switchingLoadBalancer.handleResolvedAddresses(downStreamResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleNameResolutionError(Status error) {
|
||||||
|
if (switchingLoadBalancer != null) {
|
||||||
|
switchingLoadBalancer.handleNameResolutionError(error);
|
||||||
|
} else {
|
||||||
|
helper.updateBalancingState(ConnectivityState.TRANSIENT_FAILURE, new ErrorPicker(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
if (switchingLoadBalancer != null) {
|
||||||
|
loadStatsStore.removeLocality(locality);
|
||||||
|
switchingLoadBalancer.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkAndSetUp(LrsConfig config, LoadStatsStore store) {
|
||||||
|
checkState(
|
||||||
|
clusterName == null || clusterName.equals(config.clusterName),
|
||||||
|
"cluster name should not change");
|
||||||
|
checkState(
|
||||||
|
edsServiceName == null || edsServiceName.equals(config.edsServiceName),
|
||||||
|
"edsServiceName should not change");
|
||||||
|
checkState(locality == null || locality.equals(config.locality), "locality should not change");
|
||||||
|
checkState(
|
||||||
|
loadStatsStore == null || loadStatsStore.equals(store),
|
||||||
|
"loadStatsStore should not change");
|
||||||
|
clusterName = config.clusterName;
|
||||||
|
edsServiceName = config.edsServiceName;
|
||||||
|
locality = config.locality;
|
||||||
|
loadStatsStore = store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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 io.grpc.Internal;
|
||||||
|
import io.grpc.LoadBalancer;
|
||||||
|
import io.grpc.LoadBalancerProvider;
|
||||||
|
import io.grpc.NameResolver.ConfigOrError;
|
||||||
|
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
|
||||||
|
import io.grpc.xds.EnvoyProtoData.Locality;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider for lrs load balancing policy.
|
||||||
|
*/
|
||||||
|
@Internal
|
||||||
|
public final class LrsLoadBalancerProvider extends LoadBalancerProvider {
|
||||||
|
|
||||||
|
private static final String LRS_POLICY_NAME = "lrs_experimental";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) {
|
||||||
|
return new LrsLoadBalancer(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPolicyName() {
|
||||||
|
return LRS_POLICY_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConfigOrError parseLoadBalancingPolicyConfig(Map<String, ?> rawConfig) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class LrsConfig {
|
||||||
|
final String clusterName;
|
||||||
|
@Nullable
|
||||||
|
final String edsServiceName;
|
||||||
|
final String lrsServerName;
|
||||||
|
final Locality locality;
|
||||||
|
final PolicySelection childPolicy;
|
||||||
|
|
||||||
|
LrsConfig(
|
||||||
|
String clusterName,
|
||||||
|
@Nullable String edsServiceName,
|
||||||
|
String lrsServerName,
|
||||||
|
Locality locality,
|
||||||
|
PolicySelection childPolicy) {
|
||||||
|
this.clusterName = checkNotNull(clusterName, "clusterName");
|
||||||
|
this.edsServiceName = edsServiceName;
|
||||||
|
this.lrsServerName = checkNotNull(lrsServerName, "lrsServerName");
|
||||||
|
this.locality = checkNotNull(locality, "locality");
|
||||||
|
this.childPolicy = checkNotNull(childPolicy, "childPolicy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -79,5 +79,10 @@ public final class XdsAttributes {
|
||||||
static final Attributes.Key<ObjectPool<XdsClient>> XDS_CLIENT_POOL =
|
static final Attributes.Key<ObjectPool<XdsClient>> XDS_CLIENT_POOL =
|
||||||
Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsClientPool");
|
Attributes.Key.create("io.grpc.xds.XdsAttributes.xdsClientPool");
|
||||||
|
|
||||||
|
// TODO (chengyuanzhang): temporary solution for migrating to LRS policy. Should access
|
||||||
|
// stats object via XdsClient interface.
|
||||||
|
static final Attributes.Key<LoadStatsStore> ATTR_CLUSTER_SERVICE_LOAD_STATS_STORE =
|
||||||
|
Attributes.Key.create("io.grpc.xds.XdsAttributes.loadStatsStore");
|
||||||
|
|
||||||
private XdsAttributes() {}
|
private XdsAttributes() {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ public class EnvoyProtoDataTest {
|
||||||
Locality xdsLocality = Locality.fromEnvoyProtoLocality(locality);
|
Locality xdsLocality = Locality.fromEnvoyProtoLocality(locality);
|
||||||
assertThat(xdsLocality.getRegion()).isEqualTo("test_region");
|
assertThat(xdsLocality.getRegion()).isEqualTo("test_region");
|
||||||
assertThat(xdsLocality.getZone()).isEqualTo("test_zone");
|
assertThat(xdsLocality.getZone()).isEqualTo("test_zone");
|
||||||
assertThat(xdsLocality.getSubzone()).isEqualTo("test_subzone");
|
assertThat(xdsLocality.getSubZone()).isEqualTo("test_subzone");
|
||||||
|
|
||||||
io.envoyproxy.envoy.api.v2.core.Locality convertedLocality = xdsLocality.toEnvoyProtoLocality();
|
io.envoyproxy.envoy.api.v2.core.Locality convertedLocality = xdsLocality.toEnvoyProtoLocality();
|
||||||
assertThat(convertedLocality.getRegion()).isEqualTo("test_region");
|
assertThat(convertedLocality.getRegion()).isEqualTo("test_region");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
/*
|
||||||
|
* 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.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import io.envoyproxy.envoy.api.v2.endpoint.ClusterStats;
|
||||||
|
import io.grpc.Attributes;
|
||||||
|
import io.grpc.ClientStreamTracer;
|
||||||
|
import io.grpc.ConnectivityState;
|
||||||
|
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.ResolvedAddresses;
|
||||||
|
import io.grpc.LoadBalancer.Subchannel;
|
||||||
|
import io.grpc.LoadBalancer.SubchannelPicker;
|
||||||
|
import io.grpc.LoadBalancerProvider;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
|
||||||
|
import io.grpc.xds.ClientLoadCounter.LoadRecordingStreamTracerFactory;
|
||||||
|
import io.grpc.xds.ClientLoadCounter.LoadRecordingSubchannelPicker;
|
||||||
|
import io.grpc.xds.EnvoyProtoData.Locality;
|
||||||
|
import io.grpc.xds.LrsLoadBalancerProvider.LrsConfig;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Queue;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.MockitoJUnit;
|
||||||
|
import org.mockito.junit.MockitoRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link LrsLoadBalancer}.
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class LrsLoadBalancerTest {
|
||||||
|
@Rule
|
||||||
|
public final MockitoRule mockitoRule = MockitoJUnit.rule();
|
||||||
|
|
||||||
|
private static final String CLUSTER_NAME = "cluster-foo.googleapis.com";
|
||||||
|
private static final String EDS_SERVICE_NAME = "cluster-foo:service-blade";
|
||||||
|
private static final String LRS_SERVER_NAME = "trafficdirector.googleapis.com";
|
||||||
|
private static final Locality TEST_LOCALITY =
|
||||||
|
new Locality("test-region", "test-zone", "test-subzone");
|
||||||
|
|
||||||
|
private final ClientLoadCounter counter = new ClientLoadCounter();
|
||||||
|
private final LoadRecorder loadRecorder = new LoadRecorder();
|
||||||
|
private final Queue<LoadBalancer> childBalancers = new ArrayDeque<>();
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Helper helper;
|
||||||
|
private LrsLoadBalancer loadBalancer;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
loadBalancer = new LrsLoadBalancer(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
loadBalancer.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void subchannelPickerInterceptedWithLoadRecording() {
|
||||||
|
List<EquivalentAddressGroup> backendAddrs = createResolvedBackendAddresses(2);
|
||||||
|
deliverResolvedAddresses(backendAddrs, "round_robin");
|
||||||
|
FakeLoadBalancer childBalancer = (FakeLoadBalancer) childBalancers.poll();
|
||||||
|
NoopSubchannel subchannel = childBalancer.subchannels.values().iterator().next();
|
||||||
|
deliverSubchannelState(subchannel, ConnectivityState.READY);
|
||||||
|
assertThat(loadRecorder.recording).isTrue();
|
||||||
|
ArgumentCaptor<SubchannelPicker> pickerCaptor = ArgumentCaptor.forClass(null);
|
||||||
|
verify(helper).updateBalancingState(eq(ConnectivityState.READY), pickerCaptor.capture());
|
||||||
|
SubchannelPicker picker = pickerCaptor.getValue();
|
||||||
|
assertThat(picker).isInstanceOf(LoadRecordingSubchannelPicker.class);
|
||||||
|
PickResult result = picker.pickSubchannel(mock(PickSubchannelArgs.class));
|
||||||
|
ClientStreamTracer.Factory tracerFactory = result.getStreamTracerFactory();
|
||||||
|
assertThat(((LoadRecordingStreamTracerFactory) tracerFactory).getCounter())
|
||||||
|
.isSameInstanceAs(counter);
|
||||||
|
loadBalancer.shutdown();
|
||||||
|
assertThat(childBalancer.shutdown).isTrue();
|
||||||
|
assertThat(loadRecorder.recording).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateChildPolicy() {
|
||||||
|
List<EquivalentAddressGroup> backendAddrs = createResolvedBackendAddresses(2);
|
||||||
|
deliverResolvedAddresses(backendAddrs, "round_robin");
|
||||||
|
FakeLoadBalancer childBalancer = (FakeLoadBalancer) childBalancers.poll();
|
||||||
|
assertThat(childBalancer.name).isEqualTo("round_robin");
|
||||||
|
deliverResolvedAddresses(backendAddrs, "pick_first");
|
||||||
|
assertThat(childBalancer.shutdown).isTrue();
|
||||||
|
childBalancer = (FakeLoadBalancer) childBalancers.poll();
|
||||||
|
assertThat(childBalancer.name).isEqualTo("pick_first");
|
||||||
|
loadBalancer.shutdown();
|
||||||
|
assertThat(childBalancer.shutdown).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void errorPropagation() {
|
||||||
|
loadBalancer.handleNameResolutionError(Status.UNKNOWN.withDescription("I failed"));
|
||||||
|
ArgumentCaptor<SubchannelPicker> pickerCaptor = ArgumentCaptor.forClass(null);
|
||||||
|
verify(helper)
|
||||||
|
.updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
|
||||||
|
Status status =
|
||||||
|
pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)).getStatus();
|
||||||
|
assertThat(status.getDescription()).contains("I failed");
|
||||||
|
|
||||||
|
List<EquivalentAddressGroup> backendAddrs = createResolvedBackendAddresses(2);
|
||||||
|
deliverResolvedAddresses(backendAddrs, "round_robin");
|
||||||
|
// Error after child policy is created.
|
||||||
|
loadBalancer.handleNameResolutionError(Status.UNKNOWN.withDescription("I failed"));
|
||||||
|
verify(helper, times(2))
|
||||||
|
.updateBalancingState(eq(ConnectivityState.TRANSIENT_FAILURE), pickerCaptor.capture());
|
||||||
|
status = pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)).getStatus();
|
||||||
|
assertThat(status.getDescription()).contains("I failed");
|
||||||
|
assertThat(status.getDescription()).contains("handled by downstream balancer");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deliverResolvedAddresses(
|
||||||
|
List<EquivalentAddressGroup> addresses, String childPolicy) {
|
||||||
|
PolicySelection childPolicyConfig =
|
||||||
|
new PolicySelection(new FakeLoadBalancerProvider(childPolicy), null, null);
|
||||||
|
LrsConfig config =
|
||||||
|
new LrsConfig(
|
||||||
|
CLUSTER_NAME, EDS_SERVICE_NAME, LRS_SERVER_NAME, TEST_LOCALITY, childPolicyConfig);
|
||||||
|
ResolvedAddresses resolvedAddresses =
|
||||||
|
ResolvedAddresses.newBuilder()
|
||||||
|
.setAddresses(addresses)
|
||||||
|
.setAttributes(
|
||||||
|
Attributes.newBuilder()
|
||||||
|
.set(XdsAttributes.ATTR_CLUSTER_SERVICE_LOAD_STATS_STORE, loadRecorder)
|
||||||
|
.build())
|
||||||
|
.setLoadBalancingPolicyConfig(config)
|
||||||
|
.build();
|
||||||
|
loadBalancer.handleResolvedAddresses(resolvedAddresses);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<EquivalentAddressGroup> createResolvedBackendAddresses(int n) {
|
||||||
|
List<EquivalentAddressGroup> list = new ArrayList<>();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
SocketAddress addr = new FakeSocketAddress("fake-address-" + i);
|
||||||
|
list.add(new EquivalentAddressGroup(addr));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deliverSubchannelState(
|
||||||
|
final NoopSubchannel subchannel, ConnectivityState state) {
|
||||||
|
SubchannelPicker picker = new SubchannelPicker() {
|
||||||
|
@Override
|
||||||
|
public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||||
|
return PickResult.withSubchannel(subchannel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
subchannel.helper.updateBalancingState(state, picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class FakeLoadBalancerProvider extends LoadBalancerProvider {
|
||||||
|
private final String policyName;
|
||||||
|
|
||||||
|
FakeLoadBalancerProvider(String policyName) {
|
||||||
|
this.policyName = policyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoadBalancer newLoadBalancer(Helper helper) {
|
||||||
|
LoadBalancer balancer = new FakeLoadBalancer(helper, policyName);
|
||||||
|
childBalancers.add(balancer);
|
||||||
|
return balancer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 0; // doesn't matter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPolicyName() {
|
||||||
|
return policyName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FakeLoadBalancer extends LoadBalancer {
|
||||||
|
private final Helper helper;
|
||||||
|
private final String name;
|
||||||
|
private boolean shutdown;
|
||||||
|
private final Map<EquivalentAddressGroup, NoopSubchannel> subchannels = new HashMap<>();
|
||||||
|
|
||||||
|
FakeLoadBalancer(Helper helper, String name) {
|
||||||
|
this.helper = helper;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
|
||||||
|
List<EquivalentAddressGroup> addresses = resolvedAddresses.getAddresses();
|
||||||
|
for (EquivalentAddressGroup eag : addresses) {
|
||||||
|
subchannels.put(eag, new NoopSubchannel(helper));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleNameResolutionError(final Status error) {
|
||||||
|
SubchannelPicker picker = new SubchannelPicker() {
|
||||||
|
@Override
|
||||||
|
public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||||
|
return PickResult.withError(error.augmentDescription("handled by downstream balancer"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
helper.updateBalancingState(ConnectivityState.TRANSIENT_FAILURE, picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
shutdown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class NoopSubchannel extends Subchannel {
|
||||||
|
final Helper helper;
|
||||||
|
|
||||||
|
NoopSubchannel(Helper helper) {
|
||||||
|
this.helper = helper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requestConnection() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Attributes getAttributes() {
|
||||||
|
return Attributes.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FakeSocketAddress extends SocketAddress {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
FakeSocketAddress(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "FakeSocketAddress-" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (other instanceof FakeSocketAddress) {
|
||||||
|
FakeSocketAddress otherAddr = (FakeSocketAddress) other;
|
||||||
|
return name.equals(otherAddr.name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return name.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class LoadRecorder implements LoadStatsStore {
|
||||||
|
private boolean recording = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClusterStats generateLoadReport() {
|
||||||
|
throw new UnsupportedOperationException("should not be called");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addLocality(Locality locality) {
|
||||||
|
assertThat(locality).isEqualTo(TEST_LOCALITY);
|
||||||
|
recording = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeLocality(Locality locality) {
|
||||||
|
assertThat(locality).isEqualTo(TEST_LOCALITY);
|
||||||
|
recording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientLoadCounter getLocalityCounter(Locality locality) {
|
||||||
|
assertThat(locality).isEqualTo(TEST_LOCALITY);
|
||||||
|
return counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void recordDroppedRequest(String category) {
|
||||||
|
throw new UnsupportedOperationException("should not be called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -176,7 +176,7 @@ class XdsClientTestHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
static io.envoyproxy.envoy.api.v2.endpoint.LocalityLbEndpoints buildLocalityLbEndpoints(
|
static io.envoyproxy.envoy.api.v2.endpoint.LocalityLbEndpoints buildLocalityLbEndpoints(
|
||||||
String region, String zone, String subzone,
|
String region, String zone, String subZone,
|
||||||
List<io.envoyproxy.envoy.api.v2.endpoint.LbEndpoint> lbEndpoints,
|
List<io.envoyproxy.envoy.api.v2.endpoint.LbEndpoint> lbEndpoints,
|
||||||
int loadBalancingWeight, int priority) {
|
int loadBalancingWeight, int priority) {
|
||||||
return
|
return
|
||||||
|
|
@ -185,7 +185,7 @@ class XdsClientTestHelper {
|
||||||
io.envoyproxy.envoy.api.v2.core.Locality.newBuilder()
|
io.envoyproxy.envoy.api.v2.core.Locality.newBuilder()
|
||||||
.setRegion(region)
|
.setRegion(region)
|
||||||
.setZone(zone)
|
.setZone(zone)
|
||||||
.setSubZone(subzone))
|
.setSubZone(subZone))
|
||||||
.addAllLbEndpoints(lbEndpoints)
|
.addAllLbEndpoints(lbEndpoints)
|
||||||
.setLoadBalancingWeight(UInt32Value.newBuilder().setValue(loadBalancingWeight))
|
.setLoadBalancingWeight(UInt32Value.newBuilder().setValue(loadBalancingWeight))
|
||||||
.setPriority(priority)
|
.setPriority(priority)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue