Skip to content

Commit 3c01bfe

Browse files
authored
core: Optional address shuffle in PickFirstLoadBalancer (#10110)
If provided with the new PickFirstLoadBalancerConfig, PickFirstLoadBalancer will shuffle the list of addresses it receives from the name resolver. PickFirstLoadBalancerProvider will now support the new config if enabled by an env variable.
1 parent b249a53 commit 3c01bfe

File tree

4 files changed

+153
-1
lines changed

4 files changed

+153
-1
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@
2828
import io.grpc.EquivalentAddressGroup;
2929
import io.grpc.LoadBalancer;
3030
import io.grpc.Status;
31+
import java.util.ArrayList;
32+
import java.util.Collections;
3133
import java.util.List;
34+
import java.util.Random;
3235
import java.util.concurrent.atomic.AtomicBoolean;
36+
import javax.annotation.Nullable;
3337

3438
/**
3539
* A {@link LoadBalancer} that provides no load-balancing over the addresses from the {@link
@@ -55,6 +59,18 @@ public boolean acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
5559
return false;
5660
}
5761

62+
// We can optionally be configured to shuffle the address list. This can help better distribute
63+
// the load.
64+
if (resolvedAddresses.getLoadBalancingPolicyConfig() instanceof PickFirstLoadBalancerConfig) {
65+
PickFirstLoadBalancerConfig config
66+
= (PickFirstLoadBalancerConfig) resolvedAddresses.getLoadBalancingPolicyConfig();
67+
if (config.shuffleAddressList != null && config.shuffleAddressList) {
68+
servers = new ArrayList<EquivalentAddressGroup>(servers);
69+
Collections.shuffle(servers,
70+
config.randomSeed != null ? new Random(config.randomSeed) : new Random());
71+
}
72+
}
73+
5874
if (subchannel == null) {
5975
final Subchannel subchannel = helper.createSubchannel(
6076
CreateSubchannelArgs.newBuilder()
@@ -199,4 +215,22 @@ public void run() {
199215
return PickResult.withNoResult();
200216
}
201217
}
218+
219+
public static final class PickFirstLoadBalancerConfig {
220+
221+
@Nullable
222+
public final Boolean shuffleAddressList;
223+
224+
// For testing purposes only, not meant to be parsed from a real config.
225+
@Nullable final Long randomSeed;
226+
227+
public PickFirstLoadBalancerConfig(@Nullable Boolean shuffleAddressList) {
228+
this(shuffleAddressList, null);
229+
}
230+
231+
PickFirstLoadBalancerConfig(@Nullable Boolean shuffleAddressList, @Nullable Long randomSeed) {
232+
this.shuffleAddressList = shuffleAddressList;
233+
this.randomSeed = randomSeed;
234+
}
235+
}
202236
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616

1717
package io.grpc.internal;
1818

19+
import com.google.common.annotations.VisibleForTesting;
20+
import com.google.common.base.Strings;
1921
import io.grpc.LoadBalancer;
2022
import io.grpc.LoadBalancerProvider;
2123
import io.grpc.NameResolver;
2224
import io.grpc.NameResolver.ConfigOrError;
25+
import io.grpc.internal.PickFirstLoadBalancer.PickFirstLoadBalancerConfig;
2326
import java.util.Map;
2427

2528
/**
@@ -30,6 +33,10 @@
3033
*/
3134
public final class PickFirstLoadBalancerProvider extends LoadBalancerProvider {
3235
private static final String NO_CONFIG = "no service config";
36+
private static final String SHUFFLE_ADDRESS_LIST_KEY = "shuffleAddressList";
37+
private static final String CONFIG_FLAG_NAME = "GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG";
38+
@VisibleForTesting
39+
static boolean enablePickFirstConfig = !Strings.isNullOrEmpty(System.getenv(CONFIG_FLAG_NAME));
3340

3441
@Override
3542
public boolean isAvailable() {
@@ -54,6 +61,12 @@ public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) {
5461
@Override
5562
public ConfigOrError parseLoadBalancingPolicyConfig(
5663
Map<String, ?> rawLoadBalancingPolicyConfig) {
57-
return ConfigOrError.fromConfig(NO_CONFIG);
64+
if (enablePickFirstConfig) {
65+
return ConfigOrError.fromConfig(
66+
new PickFirstLoadBalancerConfig(JsonUtil.getBoolean(rawLoadBalancingPolicyConfig,
67+
SHUFFLE_ADDRESS_LIST_KEY)));
68+
} else {
69+
return ConfigOrError.fromConfig(NO_CONFIG);
70+
}
5871
}
5972
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2023 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+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import io.grpc.NameResolver.ConfigOrError;
22+
import io.grpc.internal.PickFirstLoadBalancer.PickFirstLoadBalancerConfig;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import org.junit.After;
26+
import org.junit.Test;
27+
import org.junit.runner.RunWith;
28+
import org.junit.runners.JUnit4;
29+
30+
@RunWith(JUnit4.class)
31+
public class PickFirstLoadBalancerProviderTest {
32+
33+
@After
34+
public void resetConfigFlag() {
35+
PickFirstLoadBalancerProvider.enablePickFirstConfig = false;
36+
}
37+
38+
@Test
39+
public void parseWithConfigEnabled() {
40+
PickFirstLoadBalancerProvider.enablePickFirstConfig = true;
41+
Map<String, Object> rawConfig = new HashMap<>();
42+
rawConfig.put("shuffleAddressList", true);
43+
ConfigOrError parsedConfig = new PickFirstLoadBalancerProvider().parseLoadBalancingPolicyConfig(
44+
rawConfig);
45+
PickFirstLoadBalancerConfig config = (PickFirstLoadBalancerConfig) parsedConfig.getConfig();
46+
47+
assertThat(config.shuffleAddressList).isTrue();
48+
}
49+
50+
@Test
51+
public void parseWithConfigDisabled() {
52+
PickFirstLoadBalancerProvider.enablePickFirstConfig = false;
53+
Map<String, Object> rawConfig = new HashMap<>();
54+
rawConfig.put("shuffleAddressList", true);
55+
ConfigOrError parsedConfig = new PickFirstLoadBalancerProvider().parseLoadBalancingPolicyConfig(
56+
rawConfig);
57+
String config = (String) parsedConfig.getConfig();
58+
59+
assertThat(config).isEqualTo("no service config");
60+
}
61+
}

core/src/test/java/io/grpc/internal/PickFirstLoadBalancerTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import io.grpc.Status;
5353
import io.grpc.Status.Code;
5454
import io.grpc.SynchronizationContext;
55+
import io.grpc.internal.PickFirstLoadBalancer.PickFirstLoadBalancerConfig;
5556
import java.net.SocketAddress;
5657
import java.util.List;
5758
import org.junit.After;
@@ -140,6 +141,49 @@ public void pickAfterResolved() throws Exception {
140141
verifyNoMoreInteractions(mockHelper);
141142
}
142143

144+
@Test
145+
public void pickAfterResolved_shuffle() throws Exception {
146+
loadBalancer.acceptResolvedAddresses(
147+
ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(affinity)
148+
.setLoadBalancingPolicyConfig(new PickFirstLoadBalancerConfig(true, 123L)).build());
149+
150+
verify(mockHelper).createSubchannel(createArgsCaptor.capture());
151+
CreateSubchannelArgs args = createArgsCaptor.getValue();
152+
// We should still see the same set of addresses.
153+
assertThat(args.getAddresses()).containsExactlyElementsIn(servers);
154+
// Because we use a fixed seed, the addresses should always be shuffled in this order.
155+
assertThat(args.getAddresses().get(0)).isEqualTo(servers.get(1));
156+
assertThat(args.getAddresses().get(1)).isEqualTo(servers.get(0));
157+
assertThat(args.getAddresses().get(2)).isEqualTo(servers.get(2));
158+
verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture());
159+
verify(mockSubchannel).requestConnection();
160+
161+
// Calling pickSubchannel() twice gave the same result
162+
assertEquals(pickerCaptor.getValue().pickSubchannel(mockArgs),
163+
pickerCaptor.getValue().pickSubchannel(mockArgs));
164+
165+
verifyNoMoreInteractions(mockHelper);
166+
}
167+
168+
@Test
169+
public void pickAfterResolved_noShuffle() throws Exception {
170+
loadBalancer.acceptResolvedAddresses(
171+
ResolvedAddresses.newBuilder().setAddresses(servers).setAttributes(affinity)
172+
.setLoadBalancingPolicyConfig(new PickFirstLoadBalancerConfig(false)).build());
173+
174+
verify(mockHelper).createSubchannel(createArgsCaptor.capture());
175+
CreateSubchannelArgs args = createArgsCaptor.getValue();
176+
assertThat(args.getAddresses()).isEqualTo(servers);
177+
verify(mockHelper).updateBalancingState(eq(CONNECTING), pickerCaptor.capture());
178+
verify(mockSubchannel).requestConnection();
179+
180+
// Calling pickSubchannel() twice gave the same result
181+
assertEquals(pickerCaptor.getValue().pickSubchannel(mockArgs),
182+
pickerCaptor.getValue().pickSubchannel(mockArgs));
183+
184+
verifyNoMoreInteractions(mockHelper);
185+
}
186+
143187
@Test
144188
public void requestConnectionPicker() throws Exception {
145189
loadBalancer.acceptResolvedAddresses(

0 commit comments

Comments
 (0)