mirror of https://github.com/grpc/grpc-java.git
xds: implement priority lb
This commit is contained in:
parent
62620ccd00
commit
0cb91d97bf
|
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
* 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 io.grpc.ConnectivityState;
|
||||
import io.grpc.InternalLogId;
|
||||
import io.grpc.LoadBalancer;
|
||||
import io.grpc.LoadBalancerProvider;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.SynchronizationContext;
|
||||
import io.grpc.SynchronizationContext.ScheduledHandle;
|
||||
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
|
||||
import io.grpc.util.ForwardingLoadBalancerHelper;
|
||||
import io.grpc.util.GracefulSwitchLoadBalancer;
|
||||
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig;
|
||||
import io.grpc.xds.XdsLogger.XdsLogLevel;
|
||||
import io.grpc.xds.XdsSubchannelPickers.ErrorPicker;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Load balancer for priority policy. */
|
||||
final class PriorityLoadBalancer extends LoadBalancer {
|
||||
private final Helper helper;
|
||||
private final SynchronizationContext syncContext;
|
||||
private final ScheduledExecutorService executor;
|
||||
private final XdsLogger logger;
|
||||
|
||||
// Includes all active and deactivated children. Mutable. New entries are only added from priority
|
||||
// 0 up to the selected priority. An entry is only deleted 15 minutes after the its deactivation.
|
||||
private final Map<String, ChildLbState> children = new HashMap<>();
|
||||
|
||||
// Following fields are only null initially.
|
||||
private ResolvedAddresses resolvedAddresses;
|
||||
private List<String> priorityNames;
|
||||
private Map<String, Integer> priorityNameToIndex;
|
||||
private ConnectivityState currentConnectivityState;
|
||||
private SubchannelPicker currentPicker;
|
||||
|
||||
PriorityLoadBalancer(Helper helper) {
|
||||
this.helper = checkNotNull(helper, "helper");
|
||||
syncContext = helper.getSynchronizationContext();
|
||||
executor = helper.getScheduledExecutorService();
|
||||
InternalLogId logId = InternalLogId.allocate("priority-lb", helper.getAuthority());
|
||||
logger = XdsLogger.withLogId(logId);
|
||||
logger.log(XdsLogLevel.INFO, "Created");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
|
||||
logger.log(XdsLogLevel.DEBUG, "Received resolution result: {0}", resolvedAddresses);
|
||||
this.resolvedAddresses = resolvedAddresses;
|
||||
PriorityLbConfig config = (PriorityLbConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
|
||||
checkNotNull(config, "missing priority lb config");
|
||||
priorityNames = config.priorities;
|
||||
Map<String, Integer> pToI = new HashMap<>();
|
||||
for (int i = 0; i < priorityNames.size(); i++) {
|
||||
pToI.put(priorityNames.get(i), i);
|
||||
}
|
||||
priorityNameToIndex = Collections.unmodifiableMap(pToI);
|
||||
for (String priority : children.keySet()) {
|
||||
if (!priorityNameToIndex.containsKey(priority)) {
|
||||
children.get(priority).deactivate();
|
||||
}
|
||||
}
|
||||
for (String priority : priorityNames) {
|
||||
if (children.containsKey(priority)) {
|
||||
children.get(priority).updateResolvedAddresses();
|
||||
}
|
||||
}
|
||||
// Not to report connecting in case a pending priority bumps up on top of the current READY
|
||||
// priority.
|
||||
tryNextPriority(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNameResolutionError(Status error) {
|
||||
logger.log(XdsLogLevel.WARNING, "Received name resolution error: {0}", error);
|
||||
if (children.isEmpty()) {
|
||||
updateOverallState(TRANSIENT_FAILURE, new ErrorPicker(error));
|
||||
}
|
||||
for (ChildLbState child : children.values()) {
|
||||
child.lb.handleNameResolutionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
logger.log(XdsLogLevel.INFO, "Shutdown");
|
||||
for (ChildLbState child : children.values()) {
|
||||
child.tearDown();
|
||||
}
|
||||
}
|
||||
|
||||
private void tryNextPriority(boolean reportConnecting) {
|
||||
for (int i = 0; i < priorityNames.size(); i++) {
|
||||
String priority = priorityNames.get(i);
|
||||
if (!children.containsKey(priority)) {
|
||||
ChildLbState child = new ChildLbState(priority);
|
||||
children.put(priority, child);
|
||||
child.updateResolvedAddresses();
|
||||
updateOverallState(CONNECTING, BUFFER_PICKER);
|
||||
return; // Give priority i time to connect.
|
||||
}
|
||||
ChildLbState child = children.get(priority);
|
||||
child.reactivate();
|
||||
if (child.connectivityState.equals(READY) || child.connectivityState.equals(IDLE)) {
|
||||
logger.log(XdsLogLevel.DEBUG, "Shifted to priority {0}", priority);
|
||||
updateOverallState(child.connectivityState, child.picker);
|
||||
for (int j = i + 1; j < priorityNames.size(); j++) {
|
||||
String p = priorityNames.get(j);
|
||||
if (children.containsKey(p)) {
|
||||
children.get(p).deactivate();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (child.failOverTimer != null && child.failOverTimer.isPending()) {
|
||||
if (reportConnecting) {
|
||||
updateOverallState(CONNECTING, BUFFER_PICKER);
|
||||
}
|
||||
return; // Give priority i time to connect.
|
||||
}
|
||||
}
|
||||
// TODO(zdapeng): Include error details of each priority.
|
||||
logger.log(XdsLogLevel.DEBUG, "All priority failed");
|
||||
updateOverallState(TRANSIENT_FAILURE, new ErrorPicker(Status.UNAVAILABLE));
|
||||
}
|
||||
|
||||
private void updateOverallState(ConnectivityState state, SubchannelPicker picker) {
|
||||
if (!state.equals(currentConnectivityState) || !picker.equals(currentPicker)) {
|
||||
currentConnectivityState = state;
|
||||
currentPicker = picker;
|
||||
helper.updateBalancingState(state, picker);
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChildLbState {
|
||||
final String priority;
|
||||
final ChildHelper childHelper;
|
||||
final GracefulSwitchLoadBalancer lb;
|
||||
// Timer to fail over to the next priority if not connected in 10 sec. Scheduled only once at
|
||||
// child initialization.
|
||||
final ScheduledHandle failOverTimer;
|
||||
// Timer to delay shutdown and deletion of the priority. Scheduled whenever the child is
|
||||
// deactivated.
|
||||
@Nullable ScheduledHandle deletionTimer;
|
||||
@Nullable String policy;
|
||||
ConnectivityState connectivityState = CONNECTING;
|
||||
SubchannelPicker picker = BUFFER_PICKER;
|
||||
|
||||
ChildLbState(final String priority) {
|
||||
this.priority = priority;
|
||||
childHelper = new ChildHelper();
|
||||
lb = new GracefulSwitchLoadBalancer(childHelper);
|
||||
|
||||
class FailOverTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
if (deletionTimer != null && deletionTimer.isPending()) {
|
||||
// The child is deactivated.
|
||||
return;
|
||||
}
|
||||
logger.log(XdsLogLevel.DEBUG, "Priority {0} failed over to next", priority);
|
||||
tryNextPriority(true);
|
||||
}
|
||||
}
|
||||
|
||||
failOverTimer = syncContext.schedule(new FailOverTask(), 10, TimeUnit.SECONDS, executor);
|
||||
logger.log(XdsLogLevel.DEBUG, "Priority created: {0}", priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the child becomes a priority that is or appears before the first READY one in the
|
||||
* {@code priorities} list, due to either config update or balancing state update.
|
||||
*/
|
||||
void reactivate() {
|
||||
if (deletionTimer != null && deletionTimer.isPending()) {
|
||||
deletionTimer.cancel();
|
||||
logger.log(XdsLogLevel.DEBUG, "Priority reactivated: {0}", priority);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when either the child is removed by config update, or a higher priority becomes READY.
|
||||
*/
|
||||
void deactivate() {
|
||||
if (deletionTimer != null && deletionTimer.isPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
class DeletionTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
tearDown();
|
||||
children.remove(priority);
|
||||
}
|
||||
}
|
||||
|
||||
deletionTimer = syncContext.schedule(new DeletionTask(), 15, TimeUnit.MINUTES, executor);
|
||||
logger.log(XdsLogLevel.DEBUG, "Priority deactivated: {0}", priority);
|
||||
}
|
||||
|
||||
void tearDown() {
|
||||
if (failOverTimer.isPending()) {
|
||||
failOverTimer.cancel();
|
||||
}
|
||||
if (deletionTimer != null && deletionTimer.isPending()) {
|
||||
deletionTimer.cancel();
|
||||
}
|
||||
lb.shutdown();
|
||||
logger.log(XdsLogLevel.DEBUG, "Priority deleted: {0}", priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called either when the child is just created and in this case updated with the cached {@code
|
||||
* resolvedAddresses}, or when priority lb receives a new resolved addresses while the child
|
||||
* already exists.
|
||||
*/
|
||||
void updateResolvedAddresses() {
|
||||
final ResolvedAddresses addresses = resolvedAddresses;
|
||||
syncContext.execute(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
PriorityLbConfig config = (PriorityLbConfig) addresses.getLoadBalancingPolicyConfig();
|
||||
PolicySelection childPolicySelection = config.childConfigs.get(priority);
|
||||
LoadBalancerProvider lbProvider = childPolicySelection.getProvider();
|
||||
String newPolicy = lbProvider.getPolicyName();
|
||||
if (!newPolicy.equals(policy)) {
|
||||
policy = newPolicy;
|
||||
lb.switchTo(lbProvider);
|
||||
}
|
||||
// TODO(zdapeng): Implement address filtering.
|
||||
lb.handleResolvedAddresses(
|
||||
addresses
|
||||
.toBuilder()
|
||||
.setLoadBalancingPolicyConfig(childPolicySelection.getConfig())
|
||||
.build());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final class ChildHelper extends ForwardingLoadBalancerHelper {
|
||||
@Override
|
||||
public void updateBalancingState(ConnectivityState newState, SubchannelPicker newPicker) {
|
||||
connectivityState = newState;
|
||||
picker = newPicker;
|
||||
if (deletionTimer != null && deletionTimer.isPending()) {
|
||||
return;
|
||||
}
|
||||
if (failOverTimer.isPending()) {
|
||||
if (newState.equals(READY) || newState.equals(TRANSIENT_FAILURE)) {
|
||||
failOverTimer.cancel();
|
||||
}
|
||||
}
|
||||
tryNextPriority(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Helper delegate() {
|
||||
return helper;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import io.grpc.LoadBalancer;
|
||||
import io.grpc.LoadBalancer.Helper;
|
||||
import io.grpc.LoadBalancerProvider;
|
||||
import io.grpc.NameResolver.ConfigOrError;
|
||||
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Provider for priority load balancing policy. */
|
||||
final class PriorityLoadBalancerProvider extends LoadBalancerProvider {
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPolicyName() {
|
||||
return "priority_experimental";
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoadBalancer newLoadBalancer(Helper helper) {
|
||||
return new PriorityLoadBalancer(helper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigOrError parseLoadBalancingPolicyConfig(Map<String, ?> rawConfig) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
static final class PriorityLbConfig {
|
||||
final Map<String, PolicySelection> childConfigs;
|
||||
final List<String> priorities;
|
||||
|
||||
PriorityLbConfig(Map<String, PolicySelection> childConfigs, List<String> priorities) {
|
||||
this.childConfigs = Collections.unmodifiableMap(checkNotNull(childConfigs, "childConfigs"));
|
||||
this.priorities = Collections.unmodifiableList(checkNotNull(priorities, "priorities"));
|
||||
checkArgument(!priorities.isEmpty(), "priority list is empty");
|
||||
checkArgument(
|
||||
childConfigs.keySet().containsAll(priorities),
|
||||
"missing child config for at lease one of the priorities");
|
||||
checkArgument(
|
||||
priorities.size() == new HashSet<>(priorities).size(),
|
||||
"duplicate names in priorities");
|
||||
checkArgument(
|
||||
priorities.size() == childConfigs.keySet().size(),
|
||||
"some names in childConfigs are not referenced by priorities");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("childConfigs", childConfigs)
|
||||
.add("priorities", priorities)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 org.mockito.Mockito.mock;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import io.grpc.LoadBalancerProvider;
|
||||
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
|
||||
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Tests for {@link PriorityLoadBalancerProvider}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class PriorityLoadBalancerProviderTest {
|
||||
@Rule public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void priorityLbConfig_emptyPriorities() {
|
||||
Map<String, PolicySelection> childConfigs =
|
||||
ImmutableMap.of("p0", new PolicySelection(mock(LoadBalancerProvider.class), null, null));
|
||||
List<String> priorities = ImmutableList.of();
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
new PriorityLbConfig(childConfigs, priorities);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void priorityLbConfig_missingChildConfig() {
|
||||
Map<String, PolicySelection> childConfigs =
|
||||
ImmutableMap.of("p1", new PolicySelection(mock(LoadBalancerProvider.class), null, null));
|
||||
List<String> priorities = ImmutableList.of("p0", "p1");
|
||||
|
||||
thrown.expect(IllegalArgumentException.class);
|
||||
new PriorityLbConfig(childConfigs, priorities);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
/*
|
||||
* 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 io.grpc.ConnectivityState.CONNECTING;
|
||||
import static io.grpc.ConnectivityState.READY;
|
||||
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import io.grpc.Attributes;
|
||||
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.SynchronizationContext;
|
||||
import io.grpc.internal.FakeClock;
|
||||
import io.grpc.internal.ServiceConfigUtil.PolicySelection;
|
||||
import io.grpc.internal.TestUtils.StandardLoadBalancerProvider;
|
||||
import io.grpc.xds.PriorityLoadBalancerProvider.PriorityLbConfig;
|
||||
import io.grpc.xds.XdsSubchannelPickers.ErrorPicker;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
/** Tests for {@link PriorityLoadBalancer}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class PriorityLoadBalancerTest {
|
||||
private final List<LoadBalancer> fooBalancers = new ArrayList<>();
|
||||
private final List<LoadBalancer> barBalancers = new ArrayList<>();
|
||||
private final List<Helper> fooHelpers = new ArrayList<>();
|
||||
private final SynchronizationContext syncContext = new SynchronizationContext(
|
||||
new Thread.UncaughtExceptionHandler() {
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
});
|
||||
private final FakeClock fakeClock = new FakeClock();
|
||||
|
||||
private final LoadBalancerProvider fooLbProvider =
|
||||
new StandardLoadBalancerProvider("foo_policy") {
|
||||
@Override
|
||||
public LoadBalancer newLoadBalancer(Helper helper) {
|
||||
fooHelpers.add(helper);
|
||||
LoadBalancer childBalancer = mock(LoadBalancer.class);
|
||||
fooBalancers.add(childBalancer);
|
||||
return childBalancer;
|
||||
}
|
||||
};
|
||||
|
||||
private final LoadBalancerProvider barLbProvider =
|
||||
new StandardLoadBalancerProvider("bar_policy") {
|
||||
@Override
|
||||
public LoadBalancer newLoadBalancer(Helper helper) {
|
||||
LoadBalancer childBalancer = mock(LoadBalancer.class);
|
||||
barBalancers.add(childBalancer);
|
||||
return childBalancer;
|
||||
}
|
||||
};
|
||||
|
||||
@Rule public final MockitoRule mockitoRule = MockitoJUnit.rule();
|
||||
@Mock private Helper helper;
|
||||
@Captor ArgumentCaptor<ResolvedAddresses> resolvedAddressesCaptor;
|
||||
@Captor ArgumentCaptor<ConnectivityState> connectivityStateCaptor;
|
||||
@Captor ArgumentCaptor<SubchannelPicker> pickerCaptor;
|
||||
|
||||
private PriorityLoadBalancer priorityLb;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
doReturn(syncContext).when(helper).getSynchronizationContext();
|
||||
doReturn(fakeClock.getScheduledExecutorService()).when(helper).getScheduledExecutorService();
|
||||
priorityLb = new PriorityLoadBalancer(helper);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
priorityLb.shutdown();
|
||||
for (LoadBalancer lb : fooBalancers) {
|
||||
verify(lb).shutdown();
|
||||
}
|
||||
for (LoadBalancer lb : barBalancers) {
|
||||
verify(lb).shutdown();
|
||||
}
|
||||
assertThat(fakeClock.getPendingTasks()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handleResolvedAddresses() {
|
||||
List<EquivalentAddressGroup> addresses =
|
||||
ImmutableList.of(new EquivalentAddressGroup(new InetSocketAddress(8080)));
|
||||
Attributes attributes =
|
||||
Attributes.newBuilder().set(Attributes.Key.create("fakeKey"), "fakeValue").build();
|
||||
Object fooConfig0 = new Object();
|
||||
PolicySelection fooPolicy0 = new PolicySelection(fooLbProvider, null, fooConfig0);
|
||||
Object barConfig0 = new Object();
|
||||
PolicySelection barPolicy0 = new PolicySelection(barLbProvider, null, barConfig0);
|
||||
Object fooConfig1 = new Object();
|
||||
PolicySelection fooPolicy1 = new PolicySelection(fooLbProvider, null, fooConfig1);
|
||||
PriorityLbConfig priorityLbConfig =
|
||||
new PriorityLbConfig(
|
||||
ImmutableMap.of("p0", fooPolicy0, "p1", barPolicy0, "p2", fooPolicy1),
|
||||
ImmutableList.of("p0", "p1", "p2"));
|
||||
priorityLb.handleResolvedAddresses(
|
||||
ResolvedAddresses.newBuilder()
|
||||
.setAddresses(addresses)
|
||||
.setAttributes(attributes)
|
||||
.setLoadBalancingPolicyConfig(priorityLbConfig)
|
||||
.build());
|
||||
assertThat(fooBalancers).hasSize(1);
|
||||
assertThat(barBalancers).isEmpty();
|
||||
LoadBalancer fooBalancer0 = Iterables.getOnlyElement(fooBalancers);
|
||||
verify(fooBalancer0).handleResolvedAddresses(resolvedAddressesCaptor.capture());
|
||||
ResolvedAddresses addressesReceived = resolvedAddressesCaptor.getValue();
|
||||
assertThat(addressesReceived.getAddresses()).containsAtLeastElementsIn(addresses);
|
||||
assertThat(addressesReceived.getAttributes()).isEqualTo(attributes);
|
||||
assertThat(addressesReceived.getLoadBalancingPolicyConfig()).isEqualTo(fooConfig0);
|
||||
|
||||
// Fail over to p1.
|
||||
fakeClock.forwardTime(10, TimeUnit.SECONDS);
|
||||
assertThat(fooBalancers).hasSize(1);
|
||||
assertThat(barBalancers).hasSize(1);
|
||||
LoadBalancer barBalancer0 = Iterables.getOnlyElement(barBalancers);
|
||||
verify(barBalancer0).handleResolvedAddresses(resolvedAddressesCaptor.capture());
|
||||
addressesReceived = resolvedAddressesCaptor.getValue();
|
||||
assertThat(addressesReceived.getAddresses()).containsAtLeastElementsIn(addresses);
|
||||
assertThat(addressesReceived.getAttributes()).isEqualTo(attributes);
|
||||
assertThat(addressesReceived.getLoadBalancingPolicyConfig()).isEqualTo(barConfig0);
|
||||
|
||||
// Fail over to p2.
|
||||
fakeClock.forwardTime(10, TimeUnit.SECONDS);
|
||||
assertThat(fooBalancers).hasSize(2);
|
||||
assertThat(barBalancers).hasSize(1);
|
||||
LoadBalancer fooBalancer1 = Iterables.getLast(fooBalancers);
|
||||
verify(fooBalancer1).handleResolvedAddresses(resolvedAddressesCaptor.capture());
|
||||
addressesReceived = resolvedAddressesCaptor.getValue();
|
||||
assertThat(addressesReceived.getAddresses()).containsAtLeastElementsIn(addresses);
|
||||
assertThat(addressesReceived.getAttributes()).isEqualTo(attributes);
|
||||
assertThat(addressesReceived.getLoadBalancingPolicyConfig()).isEqualTo(fooConfig1);
|
||||
|
||||
// New update: p0 and p2 deleted; p1 config changed.
|
||||
List<EquivalentAddressGroup> newAddresses =
|
||||
ImmutableList.of(new EquivalentAddressGroup(new InetSocketAddress(8081)));
|
||||
Object newBarConfig = new Object();
|
||||
PolicySelection newBarPolicy = new PolicySelection(barLbProvider, null, newBarConfig);
|
||||
PriorityLbConfig newPriorityLbConfig =
|
||||
new PriorityLbConfig(ImmutableMap.of("p1", newBarPolicy), ImmutableList.of("p1"));
|
||||
priorityLb.handleResolvedAddresses(
|
||||
ResolvedAddresses.newBuilder()
|
||||
.setAddresses(newAddresses)
|
||||
.setLoadBalancingPolicyConfig(newPriorityLbConfig)
|
||||
.build());
|
||||
assertThat(fooBalancers).hasSize(2);
|
||||
assertThat(barBalancers).hasSize(1);
|
||||
verify(barBalancer0, times(2)).handleResolvedAddresses(resolvedAddressesCaptor.capture());
|
||||
addressesReceived = resolvedAddressesCaptor.getValue();
|
||||
assertThat(addressesReceived.getAddresses()).containsAtLeastElementsIn(newAddresses);
|
||||
assertThat(addressesReceived.getAttributes()).isEqualTo(Attributes.EMPTY);
|
||||
assertThat(addressesReceived.getLoadBalancingPolicyConfig()).isEqualTo(newBarConfig);
|
||||
verify(fooBalancer0, never()).shutdown();
|
||||
verify(fooBalancer1, never()).shutdown();
|
||||
fakeClock.forwardTime(15, TimeUnit.MINUTES);
|
||||
verify(fooBalancer0).shutdown();
|
||||
verify(fooBalancer1).shutdown();
|
||||
verify(barBalancer0, never()).shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void typicalPriorityFailOverFlow() {
|
||||
PolicySelection policy0 = new PolicySelection(fooLbProvider, null, new Object());
|
||||
PolicySelection policy1 = new PolicySelection(fooLbProvider, null, new Object());
|
||||
PolicySelection policy2 = new PolicySelection(fooLbProvider, null, new Object());
|
||||
PolicySelection policy3 = new PolicySelection(fooLbProvider, null, new Object());
|
||||
PriorityLbConfig priorityLbConfig =
|
||||
new PriorityLbConfig(
|
||||
ImmutableMap.of("p0", policy0, "p1", policy1, "p2", policy2, "p3", policy3),
|
||||
ImmutableList.of("p0", "p1", "p2", "p3"));
|
||||
priorityLb.handleResolvedAddresses(
|
||||
ResolvedAddresses.newBuilder()
|
||||
.setAddresses(ImmutableList.<EquivalentAddressGroup>of())
|
||||
.setLoadBalancingPolicyConfig(priorityLbConfig)
|
||||
.build());
|
||||
assertThat(fooBalancers).hasSize(1);
|
||||
assertThat(fooHelpers).hasSize(1);
|
||||
LoadBalancer balancer0 = Iterables.getLast(fooBalancers);
|
||||
Helper helper0 = Iterables.getOnlyElement(fooHelpers);
|
||||
|
||||
// p0 gets READY.
|
||||
final Subchannel subchannel0 = mock(Subchannel.class);
|
||||
helper0.updateBalancingState(
|
||||
READY,
|
||||
new SubchannelPicker() {
|
||||
@Override
|
||||
public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||
return PickResult.withSubchannel(subchannel0);
|
||||
}
|
||||
});
|
||||
assertLatestSubchannelPicker(subchannel0);
|
||||
|
||||
// p0 fails over to p1 immediately.
|
||||
helper0.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.ABORTED));
|
||||
assertLatestConnectivityState(CONNECTING);
|
||||
assertThat(fooBalancers).hasSize(2);
|
||||
assertThat(fooHelpers).hasSize(2);
|
||||
LoadBalancer balancer1 = Iterables.getLast(fooBalancers);
|
||||
|
||||
// p1 timeout, and fails over to p2
|
||||
fakeClock.forwardTime(10, TimeUnit.SECONDS);
|
||||
assertLatestConnectivityState(CONNECTING);
|
||||
assertThat(fooBalancers).hasSize(3);
|
||||
assertThat(fooHelpers).hasSize(3);
|
||||
LoadBalancer balancer2 = Iterables.getLast(fooBalancers);
|
||||
Helper helper2 = Iterables.getLast(fooHelpers);
|
||||
|
||||
// p2 gets READY
|
||||
final Subchannel subchannel1 = mock(Subchannel.class);
|
||||
helper2.updateBalancingState(
|
||||
READY,
|
||||
new SubchannelPicker() {
|
||||
@Override
|
||||
public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||
return PickResult.withSubchannel(subchannel1);
|
||||
}
|
||||
});
|
||||
assertLatestSubchannelPicker(subchannel1);
|
||||
|
||||
// p0 gets back to READY
|
||||
final Subchannel subchannel2 = mock(Subchannel.class);
|
||||
helper0.updateBalancingState(
|
||||
READY,
|
||||
new SubchannelPicker() {
|
||||
@Override
|
||||
public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||
return PickResult.withSubchannel(subchannel2);
|
||||
}
|
||||
});
|
||||
assertLatestSubchannelPicker(subchannel2);
|
||||
|
||||
// p2 fails but does not affect overall picker
|
||||
helper2.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.UNAVAILABLE));
|
||||
assertLatestSubchannelPicker(subchannel2);
|
||||
|
||||
// p0 fails over to p3 immediately since p1 already timeout and p2 already in TRANSIENT_FAILURE.
|
||||
helper0.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.UNAVAILABLE));
|
||||
assertLatestConnectivityState(CONNECTING);
|
||||
assertThat(fooBalancers).hasSize(4);
|
||||
assertThat(fooHelpers).hasSize(4);
|
||||
LoadBalancer balancer3 = Iterables.getLast(fooBalancers);
|
||||
Helper helper3 = Iterables.getLast(fooHelpers);
|
||||
|
||||
// p3 fails then the channel should go to TRANSIENT_FAILURE
|
||||
helper3.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.UNAVAILABLE));
|
||||
assertLatestConnectivityState(TRANSIENT_FAILURE);
|
||||
|
||||
// p2 gets back to READY
|
||||
final Subchannel subchannel3 = mock(Subchannel.class);
|
||||
helper2.updateBalancingState(
|
||||
READY,
|
||||
new SubchannelPicker() {
|
||||
@Override
|
||||
public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||
return PickResult.withSubchannel(subchannel3);
|
||||
}
|
||||
});
|
||||
assertLatestSubchannelPicker(subchannel3);
|
||||
|
||||
// p0 gets back to READY
|
||||
final Subchannel subchannel4 = mock(Subchannel.class);
|
||||
helper0.updateBalancingState(
|
||||
READY,
|
||||
new SubchannelPicker() {
|
||||
@Override
|
||||
public PickResult pickSubchannel(PickSubchannelArgs args) {
|
||||
return PickResult.withSubchannel(subchannel4);
|
||||
}
|
||||
});
|
||||
assertLatestSubchannelPicker(subchannel4);
|
||||
|
||||
// p0 fails over to p2 and picker is updated to p2's existing picker.
|
||||
helper0.updateBalancingState(TRANSIENT_FAILURE, new ErrorPicker(Status.UNAVAILABLE));
|
||||
assertLatestSubchannelPicker(subchannel3);
|
||||
|
||||
// Deactivate child balancer get deleted.
|
||||
fakeClock.forwardTime(15, TimeUnit.MINUTES);
|
||||
verify(balancer0, never()).shutdown();
|
||||
verify(balancer1, never()).shutdown();
|
||||
verify(balancer2, never()).shutdown();
|
||||
verify(balancer3).shutdown();
|
||||
}
|
||||
|
||||
private void assertLatestConnectivityState(ConnectivityState expectedState) {
|
||||
verify(helper, atLeastOnce())
|
||||
.updateBalancingState(connectivityStateCaptor.capture(), pickerCaptor.capture());
|
||||
assertThat(connectivityStateCaptor.getValue()).isEqualTo(expectedState);
|
||||
}
|
||||
|
||||
private void assertLatestSubchannelPicker(Subchannel expectedSubchannelToPick) {
|
||||
assertLatestConnectivityState(READY);
|
||||
assertThat(
|
||||
pickerCaptor.getValue().pickSubchannel(mock(PickSubchannelArgs.class)).getSubchannel())
|
||||
.isEqualTo(expectedSubchannelToPick);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue