Skip to content

Commit 48a4288

Browse files
authored
util: Add A68 random subsetting LB
implementing [gRFC A68](https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md) This change contains: 1. Usage of `murmur3_128` hashing algorithm from Guava library. 2. Implementation of `RandomSubsettingLoadBalancer` and `RandomSubsettingLoadBalancerProvider` classes and integration into the `util` project. Since envoy extensions does not support `random_subsetting` LB policy yet, xDS related changes will be introduced later. Envoy PR [here](envoyproxy/envoy#41758).
1 parent 7eab160 commit 48a4288

File tree

8 files changed

+723
-3
lines changed

8 files changed

+723
-3
lines changed

api/src/test/java/io/grpc/LoadBalancerRegistryTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public void getClassesViaHardcoded_classesPresent() throws Exception {
4040
@Test
4141
public void stockProviders() {
4242
LoadBalancerRegistry defaultRegistry = LoadBalancerRegistry.getDefaultRegistry();
43-
assertThat(defaultRegistry.providers()).hasSize(3);
43+
assertThat(defaultRegistry.providers()).hasSize(4);
4444

4545
LoadBalancerProvider pickFirst = defaultRegistry.getProvider("pick_first");
4646
assertThat(pickFirst).isInstanceOf(PickFirstLoadBalancerProvider.class);
@@ -56,6 +56,12 @@ public void stockProviders() {
5656
assertThat(outlierDetection.getClass().getName()).isEqualTo(
5757
"io.grpc.util.OutlierDetectionLoadBalancerProvider");
5858
assertThat(roundRobin.getPriority()).isEqualTo(5);
59+
60+
LoadBalancerProvider randomSubsetting = defaultRegistry.getProvider(
61+
"random_subsetting_experimental");
62+
assertThat(randomSubsetting.getClass().getName()).isEqualTo(
63+
"io.grpc.util.RandomSubsettingLoadBalancerProvider");
64+
assertThat(randomSubsetting.getPriority()).isEqualTo(5);
5965
}
6066

6167
@Test

util/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ animalsniffer {
5858
tasks.named("javadoc").configure {
5959
exclude 'io/grpc/util/MultiChildLoadBalancer.java'
6060
exclude 'io/grpc/util/OutlierDetectionLoadBalancer*'
61+
exclude 'io/grpc/util/RandomSubsettingLoadBalancer*'
6162
exclude 'io/grpc/util/RoundRobinLoadBalancer*'
6263
}
6364

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,14 @@ public static ConfigOrError parseLoadBalancingPolicyConfig(
207207
ServiceConfigUtil.unwrapLoadBalancingConfigList(loadBalancingConfigs);
208208
if (childConfigCandidates == null || childConfigCandidates.isEmpty()) {
209209
return ConfigOrError.fromError(
210-
Status.INTERNAL.withDescription("No child LB config specified"));
210+
Status.UNAVAILABLE.withDescription("No child LB config specified"));
211211
}
212212
ConfigOrError selectedConfig =
213213
ServiceConfigUtil.selectLbPolicyFromList(childConfigCandidates, lbRegistry);
214214
if (selectedConfig.getError() != null) {
215215
Status error = selectedConfig.getError();
216216
return ConfigOrError.fromError(
217-
Status.INTERNAL
217+
Status.UNAVAILABLE
218218
.withCause(error.getCause())
219219
.withDescription(error.getDescription())
220220
.augmentDescription("Failed to select child config"));
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2025 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.util;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
import static com.google.common.base.Preconditions.checkState;
22+
23+
import com.google.common.annotations.VisibleForTesting;
24+
import com.google.common.hash.HashCode;
25+
import com.google.common.hash.HashFunction;
26+
import com.google.common.hash.Hashing;
27+
import com.google.common.primitives.Ints;
28+
import io.grpc.EquivalentAddressGroup;
29+
import io.grpc.LoadBalancer;
30+
import io.grpc.Status;
31+
import java.nio.charset.StandardCharsets;
32+
import java.util.ArrayList;
33+
import java.util.Collections;
34+
import java.util.Comparator;
35+
import java.util.Random;
36+
37+
38+
/**
39+
* Wraps a child {@code LoadBalancer}, separating the total set of backends into smaller subsets for
40+
* the child balancer to balance across.
41+
*
42+
* <p>This implements random subsetting gRFC:
43+
* https://https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md
44+
*/
45+
final class RandomSubsettingLoadBalancer extends LoadBalancer {
46+
private final GracefulSwitchLoadBalancer switchLb;
47+
private final HashFunction hashFunc;
48+
49+
public RandomSubsettingLoadBalancer(Helper helper) {
50+
this(helper, new Random().nextInt());
51+
}
52+
53+
@VisibleForTesting
54+
RandomSubsettingLoadBalancer(Helper helper, int seed) {
55+
switchLb = new GracefulSwitchLoadBalancer(checkNotNull(helper, "helper"));
56+
hashFunc = Hashing.murmur3_128(seed);
57+
}
58+
59+
@Override
60+
public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
61+
RandomSubsettingLoadBalancerConfig config =
62+
(RandomSubsettingLoadBalancerConfig)
63+
resolvedAddresses.getLoadBalancingPolicyConfig();
64+
65+
ResolvedAddresses subsetAddresses = filterEndpoints(resolvedAddresses, config.subsetSize);
66+
67+
return switchLb.acceptResolvedAddresses(
68+
subsetAddresses.toBuilder()
69+
.setLoadBalancingPolicyConfig(config.childConfig)
70+
.build());
71+
}
72+
73+
// implements the subsetting algorithm, as described in A68:
74+
// https://github.com/grpc/proposal/pull/423
75+
private ResolvedAddresses filterEndpoints(ResolvedAddresses resolvedAddresses, int subsetSize) {
76+
if (subsetSize >= resolvedAddresses.getAddresses().size()) {
77+
return resolvedAddresses;
78+
}
79+
80+
ArrayList<EndpointWithHash> endpointWithHashList =
81+
new ArrayList<>(resolvedAddresses.getAddresses().size());
82+
83+
for (EquivalentAddressGroup addressGroup : resolvedAddresses.getAddresses()) {
84+
HashCode hashCode = hashFunc.hashString(
85+
addressGroup.getAddresses().get(0).toString(),
86+
StandardCharsets.UTF_8);
87+
endpointWithHashList.add(new EndpointWithHash(addressGroup, hashCode.asLong()));
88+
}
89+
90+
Collections.sort(endpointWithHashList, new HashAddressComparator());
91+
92+
ArrayList<EquivalentAddressGroup> addressGroups = new ArrayList<>(subsetSize);
93+
94+
for (int idx = 0; idx < subsetSize; ++idx) {
95+
addressGroups.add(endpointWithHashList.get(idx).addressGroup);
96+
}
97+
98+
return resolvedAddresses.toBuilder().setAddresses(addressGroups).build();
99+
}
100+
101+
@Override
102+
public void handleNameResolutionError(Status error) {
103+
switchLb.handleNameResolutionError(error);
104+
}
105+
106+
@Override
107+
public void shutdown() {
108+
switchLb.shutdown();
109+
}
110+
111+
private static final class EndpointWithHash {
112+
public final EquivalentAddressGroup addressGroup;
113+
public final long hashCode;
114+
115+
public EndpointWithHash(EquivalentAddressGroup addressGroup, long hashCode) {
116+
this.addressGroup = addressGroup;
117+
this.hashCode = hashCode;
118+
}
119+
}
120+
121+
private static final class HashAddressComparator implements Comparator<EndpointWithHash> {
122+
@Override
123+
public int compare(EndpointWithHash lhs, EndpointWithHash rhs) {
124+
return Long.compare(lhs.hashCode, rhs.hashCode);
125+
}
126+
}
127+
128+
public static final class RandomSubsettingLoadBalancerConfig {
129+
public final int subsetSize;
130+
public final Object childConfig;
131+
132+
private RandomSubsettingLoadBalancerConfig(int subsetSize, Object childConfig) {
133+
this.subsetSize = subsetSize;
134+
this.childConfig = childConfig;
135+
}
136+
137+
public static class Builder {
138+
int subsetSize;
139+
Object childConfig;
140+
141+
public Builder setSubsetSize(long subsetSize) {
142+
checkArgument(subsetSize > 0L, "Subset size must be greater than 0");
143+
// clamping subset size to Integer.MAX_VALUE due to collection indexing limitations in JVM
144+
this.subsetSize = Ints.saturatedCast(subsetSize);
145+
return this;
146+
}
147+
148+
public Builder setChildConfig(Object childConfig) {
149+
this.childConfig = checkNotNull(childConfig, "childConfig");
150+
return this;
151+
}
152+
153+
public RandomSubsettingLoadBalancerConfig build() {
154+
checkState(subsetSize != 0L, "Subset size must be set before building the config");
155+
return new RandomSubsettingLoadBalancerConfig(
156+
subsetSize,
157+
checkNotNull(childConfig, "childConfig"));
158+
}
159+
}
160+
}
161+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2025 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.util;
18+
19+
import io.grpc.Internal;
20+
import io.grpc.LoadBalancer;
21+
import io.grpc.LoadBalancerProvider;
22+
import io.grpc.NameResolver.ConfigOrError;
23+
import io.grpc.Status;
24+
import io.grpc.internal.JsonUtil;
25+
import java.util.Map;
26+
27+
@Internal
28+
public final class RandomSubsettingLoadBalancerProvider extends LoadBalancerProvider {
29+
private static final String POLICY_NAME = "random_subsetting_experimental";
30+
31+
@Override
32+
public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) {
33+
return new RandomSubsettingLoadBalancer(helper);
34+
}
35+
36+
@Override
37+
public boolean isAvailable() {
38+
return true;
39+
}
40+
41+
@Override
42+
public int getPriority() {
43+
return 5;
44+
}
45+
46+
@Override
47+
public String getPolicyName() {
48+
return POLICY_NAME;
49+
}
50+
51+
@Override
52+
public ConfigOrError parseLoadBalancingPolicyConfig(Map<String, ?> rawConfig) {
53+
try {
54+
return parseLoadBalancingPolicyConfigInternal(rawConfig);
55+
} catch (RuntimeException e) {
56+
return ConfigOrError.fromError(
57+
Status.UNAVAILABLE
58+
.withCause(e)
59+
.withDescription("Failed parsing configuration for " + getPolicyName()));
60+
}
61+
}
62+
63+
private ConfigOrError parseLoadBalancingPolicyConfigInternal(Map<String, ?> rawConfig) {
64+
Long subsetSize = JsonUtil.getNumberAsLong(rawConfig, "subsetSize");
65+
if (subsetSize == null) {
66+
return ConfigOrError.fromError(
67+
Status.UNAVAILABLE.withDescription(
68+
"Subset size missing in " + getPolicyName() + ", LB policy config=" + rawConfig));
69+
}
70+
71+
ConfigOrError childConfig = GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig(
72+
JsonUtil.getListOfObjects(rawConfig, "childPolicy"));
73+
if (childConfig.getError() != null) {
74+
return ConfigOrError.fromError(Status.UNAVAILABLE
75+
.withDescription(
76+
"Failed to parse child in " + getPolicyName() + ", LB policy config=" + rawConfig)
77+
.withCause(childConfig.getError().asRuntimeException()));
78+
}
79+
80+
return ConfigOrError.fromConfig(
81+
new RandomSubsettingLoadBalancer.RandomSubsettingLoadBalancerConfig.Builder()
82+
.setSubsetSize(subsetSize)
83+
.setChildConfig(childConfig.getConfig())
84+
.build());
85+
}
86+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
io.grpc.util.SecretRoundRobinLoadBalancerProvider$Provider
22
io.grpc.util.OutlierDetectionLoadBalancerProvider
3+
io.grpc.util.RandomSubsettingLoadBalancerProvider

0 commit comments

Comments
 (0)