Skip to content

Commit 4e8f7df

Browse files
authored
util: Remove resolvedAddresses from MultiChildLb.ChildLbState
It isn't actually used by MultiChildLb, and using the health API gives us more confidence that health is properly plumbed.
1 parent b170334 commit 4e8f7df

File tree

5 files changed

+160
-25
lines changed

5 files changed

+160
-25
lines changed

core/src/main/java/io/grpc/internal/PickFirstLoadBalancerProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public final class PickFirstLoadBalancerProvider extends LoadBalancerProvider {
3636
public static final String GRPC_PF_USE_HAPPY_EYEBALLS = "GRPC_PF_USE_HAPPY_EYEBALLS";
3737
private static final String SHUFFLE_ADDRESS_LIST_KEY = "shuffleAddressList";
3838

39-
private static boolean enableNewPickFirst =
39+
static boolean enableNewPickFirst =
4040
GrpcUtil.getFlag("GRPC_EXPERIMENTAL_ENABLE_NEW_PICK_FIRST", false);
4141

4242
public static boolean isEnabledHappyEyeballs() {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2024 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.internal;
18+
19+
/**
20+
* Accessor for PickFirstLoadBalancerProvider, allowing access only during tests.
21+
*/
22+
public final class PickFirstLoadBalancerProviderAccessor {
23+
private PickFirstLoadBalancerProviderAccessor() {}
24+
25+
public static void setEnableNewPickFirst(boolean enableNewPickFirst) {
26+
PickFirstLoadBalancerProvider.enableNewPickFirst = enableNewPickFirst;
27+
}
28+
}

util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ private List<ChildLbState> updateChildrenWithResolvedAddresses(
191191
}
192192
newChildLbStates.add(childLbState);
193193
if (entry.getValue() != null) {
194-
childLbState.setResolvedAddresses(entry.getValue()); // update child
195194
childLbState.lb.handleResolvedAddresses(entry.getValue()); // update child LB
196195
}
197196
}
@@ -262,8 +261,6 @@ protected final List<ChildLbState> getReadyChildren() {
262261
*/
263262
public class ChildLbState {
264263
private final Object key;
265-
private ResolvedAddresses resolvedAddresses;
266-
267264
private final LoadBalancer lb;
268265
private ConnectivityState currentState;
269266
private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult());
@@ -321,16 +318,6 @@ protected final void setCurrentPicker(SubchannelPicker newPicker) {
321318
currentPicker = newPicker;
322319
}
323320

324-
protected final void setResolvedAddresses(ResolvedAddresses newAddresses) {
325-
checkNotNull(newAddresses, "Missing address list for child");
326-
resolvedAddresses = newAddresses;
327-
}
328-
329-
@VisibleForTesting
330-
public final ResolvedAddresses getResolvedAddresses() {
331-
return resolvedAddresses;
332-
}
333-
334321
/**
335322
* ChildLbStateHelper is the glue between ChildLbState and the helpers associated with the
336323
* petiole policy above and the PickFirstLoadBalancer's helper below.

util/src/test/java/io/grpc/util/RoundRobinLoadBalancerTest.java

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import static io.grpc.ConnectivityState.READY;
2323
import static io.grpc.ConnectivityState.SHUTDOWN;
2424
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
25-
import static io.grpc.util.MultiChildLoadBalancer.IS_PETIOLE_POLICY;
2625
import static org.junit.Assert.assertEquals;
2726
import static org.junit.Assert.assertNull;
2827
import static org.junit.Assert.fail;
@@ -54,13 +53,15 @@
5453
import io.grpc.LoadBalancer.Subchannel;
5554
import io.grpc.LoadBalancer.SubchannelPicker;
5655
import io.grpc.Status;
56+
import io.grpc.internal.PickFirstLoadBalancerProvider;
57+
import io.grpc.internal.PickFirstLoadBalancerProviderAccessor;
5758
import io.grpc.internal.TestUtils;
58-
import io.grpc.util.MultiChildLoadBalancer.ChildLbState;
5959
import io.grpc.util.RoundRobinLoadBalancer.ReadyPicker;
6060
import java.net.SocketAddress;
6161
import java.util.ArrayList;
6262
import java.util.Arrays;
6363
import java.util.Collections;
64+
import java.util.HashMap;
6465
import java.util.Iterator;
6566
import java.util.List;
6667
import java.util.Map;
@@ -104,6 +105,7 @@ public class RoundRobinLoadBalancerTest {
104105
private ArgumentCaptor<CreateSubchannelArgs> createArgsCaptor;
105106
private TestHelper testHelperInst = new TestHelper();
106107
private Helper mockHelper = mock(Helper.class, delegatesTo(testHelperInst));
108+
private boolean defaultNewPickFirst = PickFirstLoadBalancerProvider.isEnabledNewPickFirst();
107109

108110
@Mock // This LoadBalancer doesn't use any of the arg fields, as verified in tearDown().
109111
private PickSubchannelArgs mockArgs;
@@ -126,6 +128,7 @@ private Status acceptAddresses(List<EquivalentAddressGroup> eagList, Attributes
126128

127129
@After
128130
public void tearDown() throws Exception {
131+
PickFirstLoadBalancerProviderAccessor.setEnableNewPickFirst(defaultNewPickFirst);
129132
verifyNoMoreInteractions(mockArgs);
130133
}
131134

@@ -201,12 +204,6 @@ public void pickAfterResolvedUpdatedHosts() throws Exception {
201204
verify(removedSubchannel, times(1)).requestConnection();
202205
verify(oldSubchannel, times(1)).requestConnection();
203206

204-
assertThat(loadBalancer.getChildLbStates().size()).isEqualTo(2);
205-
for (ChildLbState childLbState : loadBalancer.getChildLbStates()) {
206-
assertThat(childLbState.getResolvedAddresses().getAttributes().get(IS_PETIOLE_POLICY))
207-
.isTrue();
208-
}
209-
210207
// This time with Attributes
211208
List<EquivalentAddressGroup> latestServers = Lists.newArrayList(oldEag2, newEag);
212209

@@ -464,6 +461,60 @@ public void subchannelStateIsolation() throws Exception {
464461
assertThat(pickers.hasNext()).isFalse();
465462
}
466463

464+
@Test
465+
public void subchannelHealthObserved() throws Exception {
466+
// Only the new PF policy observes the new separate listener for health
467+
PickFirstLoadBalancerProviderAccessor.setEnableNewPickFirst(true);
468+
// PickFirst does most of this work. If the test fails, check IS_PETIOLE_POLICY
469+
Map<Subchannel, LoadBalancer.SubchannelStateListener> healthListeners = new HashMap<>();
470+
loadBalancer = new RoundRobinLoadBalancer(new ForwardingLoadBalancerHelper() {
471+
@Override
472+
public Subchannel createSubchannel(CreateSubchannelArgs args) {
473+
Subchannel subchannel = super.createSubchannel(args.toBuilder()
474+
.setAttributes(args.getAttributes().toBuilder()
475+
.set(LoadBalancer.HAS_HEALTH_PRODUCER_LISTENER_KEY, true)
476+
.build())
477+
.build());
478+
healthListeners.put(
479+
subchannel, args.getOption(LoadBalancer.HEALTH_CONSUMER_LISTENER_ARG_KEY));
480+
return subchannel;
481+
}
482+
483+
@Override
484+
protected Helper delegate() {
485+
return mockHelper;
486+
}
487+
});
488+
489+
InOrder inOrder = inOrder(mockHelper);
490+
Status addressesAcceptanceStatus = acceptAddresses(servers, Attributes.EMPTY);
491+
assertThat(addressesAcceptanceStatus.isOk()).isTrue();
492+
Subchannel subchannel0 = subchannels.get(Arrays.asList(servers.get(0)));
493+
Subchannel subchannel1 = subchannels.get(Arrays.asList(servers.get(1)));
494+
Subchannel subchannel2 = subchannels.get(Arrays.asList(servers.get(2)));
495+
496+
// Subchannels go READY, but the LB waits for health
497+
for (Subchannel subchannel : subchannels.values()) {
498+
deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY));
499+
}
500+
inOrder.verify(mockHelper, times(0))
501+
.updateBalancingState(eq(READY), any(SubchannelPicker.class));
502+
503+
// Health results lets subchannels go READY
504+
healthListeners.get(subchannel0).onSubchannelState(
505+
ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE.withDescription("oh no")));
506+
healthListeners.get(subchannel1).onSubchannelState(ConnectivityStateInfo.forNonError(READY));
507+
healthListeners.get(subchannel2).onSubchannelState(ConnectivityStateInfo.forNonError(READY));
508+
inOrder.verify(mockHelper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture());
509+
SubchannelPicker picker = pickerCaptor.getValue();
510+
List<Subchannel> picks = Arrays.asList(
511+
picker.pickSubchannel(mockArgs).getSubchannel(),
512+
picker.pickSubchannel(mockArgs).getSubchannel(),
513+
picker.pickSubchannel(mockArgs).getSubchannel(),
514+
picker.pickSubchannel(mockArgs).getSubchannel());
515+
assertThat(picks).containsExactly(subchannel1, subchannel2, subchannel1, subchannel2);
516+
}
517+
467518
@Test
468519
public void readyPicker_emptyList() {
469520
// ready picker list must be non-empty

xds/src/test/java/io/grpc/xds/RingHashLoadBalancerTest.java

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import static io.grpc.ConnectivityState.READY;
2424
import static io.grpc.ConnectivityState.SHUTDOWN;
2525
import static io.grpc.ConnectivityState.TRANSIENT_FAILURE;
26-
import static io.grpc.util.MultiChildLoadBalancer.IS_PETIOLE_POLICY;
2726
import static io.grpc.xds.RingHashLoadBalancerTest.InitializationFlags.DO_NOT_RESET_HELPER;
2827
import static io.grpc.xds.RingHashLoadBalancerTest.InitializationFlags.DO_NOT_VERIFY;
2928
import static io.grpc.xds.RingHashLoadBalancerTest.InitializationFlags.RESET_SUBCHANNEL_MOCKS;
@@ -48,6 +47,7 @@
4847
import io.grpc.ConnectivityState;
4948
import io.grpc.ConnectivityStateInfo;
5049
import io.grpc.EquivalentAddressGroup;
50+
import io.grpc.LoadBalancer;
5151
import io.grpc.LoadBalancer.CreateSubchannelArgs;
5252
import io.grpc.LoadBalancer.Helper;
5353
import io.grpc.LoadBalancer.PickDetailsConsumer;
@@ -62,9 +62,11 @@
6262
import io.grpc.SynchronizationContext;
6363
import io.grpc.internal.FakeClock;
6464
import io.grpc.internal.PickFirstLoadBalancerProvider;
65+
import io.grpc.internal.PickFirstLoadBalancerProviderAccessor;
6566
import io.grpc.internal.PickSubchannelArgsImpl;
6667
import io.grpc.testing.TestMethodDescriptors;
6768
import io.grpc.util.AbstractTestHelper;
69+
import io.grpc.util.ForwardingLoadBalancerHelper;
6870
import io.grpc.util.MultiChildLoadBalancer.ChildLbState;
6971
import io.grpc.xds.RingHashLoadBalancer.RingHashConfig;
7072
import java.lang.Thread.UncaughtExceptionHandler;
@@ -74,8 +76,11 @@
7476
import java.util.Collections;
7577
import java.util.Deque;
7678
import java.util.HashMap;
79+
import java.util.HashSet;
7780
import java.util.List;
7881
import java.util.Map;
82+
import java.util.Random;
83+
import java.util.Set;
7984
import org.junit.After;
8085
import org.junit.Before;
8186
import org.junit.Rule;
@@ -115,6 +120,7 @@ public void uncaughtException(Thread t, Throwable e) {
115120
@Captor
116121
private ArgumentCaptor<SubchannelPicker> pickerCaptor;
117122
private RingHashLoadBalancer loadBalancer;
123+
private boolean defaultNewPickFirst = PickFirstLoadBalancerProvider.isEnabledNewPickFirst();
118124

119125
@Before
120126
public void setUp() {
@@ -126,6 +132,7 @@ public void setUp() {
126132

127133
@After
128134
public void tearDown() {
135+
PickFirstLoadBalancerProviderAccessor.setEnableNewPickFirst(defaultNewPickFirst);
129136
loadBalancer.shutdown();
130137
for (Subchannel subchannel : subchannels.values()) {
131138
verify(subchannel).shutdown();
@@ -906,6 +913,70 @@ public void duplicateAddresses() {
906913
assertThat(description).contains("Address: FakeSocketAddress-server2, count: 3");
907914
}
908915

916+
@Test
917+
public void subchannelHealthObserved() throws Exception {
918+
// Only the new PF policy observes the new separate listener for health
919+
PickFirstLoadBalancerProviderAccessor.setEnableNewPickFirst(true);
920+
// PickFirst does most of this work. If the test fails, check IS_PETIOLE_POLICY
921+
Map<Subchannel, LoadBalancer.SubchannelStateListener> healthListeners = new HashMap<>();
922+
loadBalancer = new RingHashLoadBalancer(new ForwardingLoadBalancerHelper() {
923+
@Override
924+
public Subchannel createSubchannel(CreateSubchannelArgs args) {
925+
Subchannel subchannel = super.createSubchannel(args.toBuilder()
926+
.setAttributes(args.getAttributes().toBuilder()
927+
.set(LoadBalancer.HAS_HEALTH_PRODUCER_LISTENER_KEY, true)
928+
.build())
929+
.build());
930+
healthListeners.put(
931+
subchannel, args.getOption(LoadBalancer.HEALTH_CONSUMER_LISTENER_ARG_KEY));
932+
return subchannel;
933+
}
934+
935+
@Override
936+
protected Helper delegate() {
937+
return helper;
938+
}
939+
});
940+
941+
InOrder inOrder = Mockito.inOrder(helper);
942+
List<EquivalentAddressGroup> servers = createWeightedServerAddrs(1, 1);
943+
initializeLbSubchannels(new RingHashConfig(10, 100), servers);
944+
Subchannel subchannel0 = subchannels.get(Collections.singletonList(servers.get(0)));
945+
Subchannel subchannel1 = subchannels.get(Collections.singletonList(servers.get(1)));
946+
947+
// Subchannels go READY, but the LB waits for health
948+
for (Subchannel subchannel : subchannels.values()) {
949+
deliverSubchannelState(subchannel, ConnectivityStateInfo.forNonError(READY));
950+
}
951+
inOrder.verify(helper, times(0)).updateBalancingState(eq(READY), any(SubchannelPicker.class));
952+
953+
// Health results lets subchannels go READY
954+
healthListeners.get(subchannel0).onSubchannelState(ConnectivityStateInfo.forNonError(READY));
955+
healthListeners.get(subchannel1).onSubchannelState(ConnectivityStateInfo.forNonError(READY));
956+
inOrder.verify(helper, times(2)).updateBalancingState(eq(READY), pickerCaptor.capture());
957+
SubchannelPicker picker = pickerCaptor.getValue();
958+
Random random = new Random(1);
959+
Set<Subchannel> picks = new HashSet<>();
960+
for (int i = 0; i < 10; i++) {
961+
picks.add(
962+
picker.pickSubchannel(getDefaultPickSubchannelArgs(random.nextLong())).getSubchannel());
963+
}
964+
assertThat(picks).containsExactly(subchannel0, subchannel1);
965+
966+
// Unhealthy subchannel skipped
967+
healthListeners.get(subchannel0).onSubchannelState(
968+
ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE.withDescription("oh no")));
969+
inOrder.verify(helper).updateBalancingState(eq(READY), pickerCaptor.capture());
970+
picker = pickerCaptor.getValue();
971+
random.setSeed(1);
972+
picks.clear();
973+
for (int i = 0; i < 10; i++) {
974+
picks.add(
975+
picker.pickSubchannel(getDefaultPickSubchannelArgs(random.nextLong())).getSubchannel());
976+
}
977+
assertThat(picks).containsExactly(subchannel1);
978+
}
979+
909980
private List<Subchannel> initializeLbSubchannels(RingHashConfig config,
910981
List<EquivalentAddressGroup> servers, InitializationFlags... initFlags) {
911982

@@ -950,8 +1021,6 @@ private List<Subchannel> initializeLbSubchannels(RingHashConfig config,
9501021
for (ChildLbState childLbState : loadBalancer.getChildLbStates()) {
9511022
childLbState.getCurrentPicker()
9521023
.pickSubchannel(getDefaultPickSubchannelArgs(hashFunc.hashVoid()));
953-
assertThat(childLbState.getResolvedAddresses().getAttributes().get(IS_PETIOLE_POLICY))
954-
.isTrue();
9551024
}
9561025

9571026
if (doVerifies) {

0 commit comments

Comments
 (0)