Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8d349df
Moved `XxHash64` library to enable shared use across projects
Zgoda91 Sep 24, 2025
8ec136a
Implemented random_subsetting LB policy
Zgoda91 Sep 24, 2025
1870e6f
Implemented random_subsetting converter for xDS
Zgoda91 Sep 24, 2025
40af1a8
Optimized seed lifetime
Zgoda91 Oct 6, 2025
d78cf20
Optimized memory allocations for arrays
Zgoda91 Oct 6, 2025
4dfa2e7
Changed visibility of LB to package-private
Zgoda91 Oct 6, 2025
f5147ee
Revert "Moved `XxHash64` library to enable shared use across projects"
Zgoda91 Oct 6, 2025
e404564
Changed hashing function to `murmur3_128`
Zgoda91 Oct 6, 2025
7684629
Removed leftover dependency from `BUILD.bazel`
Zgoda91 Oct 6, 2025
4fbf81d
Changed hash code comparator
Zgoda91 Oct 7, 2025
5745d0a
Improved test assertion to get better failure message
Zgoda91 Oct 7, 2025
eb06b9e
Excluded new LB from javadoc
Zgoda91 Oct 7, 2025
e168032
Improved `subsetSize` parsing
Zgoda91 Oct 7, 2025
56d33a7
Adjusted expectation for flaky test
Zgoda91 Oct 7, 2025
04c4beb
Changed way of storing hashes
Zgoda91 Oct 8, 2025
9163438
Improved config builder
Zgoda91 Oct 8, 2025
9534d22
Updated xDS converting function
Zgoda91 Oct 8, 2025
94be9ac
Updated status for subset size parsing failure
Zgoda91 Oct 8, 2025
370570b
Added deterministic seed setup for LB tests
Zgoda91 Oct 8, 2025
000e2bb
Reverted xDS changes
Zgoda91 Oct 29, 2025
eb96d36
Reused existing constructor
Zgoda91 Oct 31, 2025
33c6e41
Fixed status codes
Zgoda91 Oct 31, 2025
65140ae
Suffix name with _experimental
ejona86 Nov 4, 2025
a195daf
Suffix name with _experimental in tests, too
ejona86 Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion api/src/test/java/io/grpc/LoadBalancerRegistryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public void getClassesViaHardcoded_classesPresent() throws Exception {
@Test
public void stockProviders() {
LoadBalancerRegistry defaultRegistry = LoadBalancerRegistry.getDefaultRegistry();
assertThat(defaultRegistry.providers()).hasSize(3);
assertThat(defaultRegistry.providers()).hasSize(4);

LoadBalancerProvider pickFirst = defaultRegistry.getProvider("pick_first");
assertThat(pickFirst).isInstanceOf(PickFirstLoadBalancerProvider.class);
Expand All @@ -56,6 +56,12 @@ public void stockProviders() {
assertThat(outlierDetection.getClass().getName()).isEqualTo(
"io.grpc.util.OutlierDetectionLoadBalancerProvider");
assertThat(roundRobin.getPriority()).isEqualTo(5);

LoadBalancerProvider randomSubsetting = defaultRegistry.getProvider(
"random_subsetting_experimental");
assertThat(randomSubsetting.getClass().getName()).isEqualTo(
"io.grpc.util.RandomSubsettingLoadBalancerProvider");
assertThat(randomSubsetting.getPriority()).isEqualTo(5);
}

@Test
Expand Down
1 change: 1 addition & 0 deletions util/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ animalsniffer {
tasks.named("javadoc").configure {
exclude 'io/grpc/util/MultiChildLoadBalancer.java'
exclude 'io/grpc/util/OutlierDetectionLoadBalancer*'
exclude 'io/grpc/util/RandomSubsettingLoadBalancer*'
exclude 'io/grpc/util/RoundRobinLoadBalancer*'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,14 @@ public static ConfigOrError parseLoadBalancingPolicyConfig(
ServiceConfigUtil.unwrapLoadBalancingConfigList(loadBalancingConfigs);
if (childConfigCandidates == null || childConfigCandidates.isEmpty()) {
return ConfigOrError.fromError(
Status.INTERNAL.withDescription("No child LB config specified"));
Status.UNAVAILABLE.withDescription("No child LB config specified"));
}
ConfigOrError selectedConfig =
ServiceConfigUtil.selectLbPolicyFromList(childConfigCandidates, lbRegistry);
if (selectedConfig.getError() != null) {
Status error = selectedConfig.getError();
return ConfigOrError.fromError(
Status.INTERNAL
Status.UNAVAILABLE
.withCause(error.getCause())
.withDescription(error.getDescription())
.augmentDescription("Failed to select child config"));
Expand Down
161 changes: 161 additions & 0 deletions util/src/main/java/io/grpc/util/RandomSubsettingLoadBalancer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright 2025 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.util;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.primitives.Ints;
import io.grpc.EquivalentAddressGroup;
import io.grpc.LoadBalancer;
import io.grpc.Status;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Random;


/**
* Wraps a child {@code LoadBalancer}, separating the total set of backends into smaller subsets for
* the child balancer to balance across.
*
* <p>This implements random subsetting gRFC:
* https://https://github.com/grpc/proposal/blob/master/A68-random-subsetting.md
*/
final class RandomSubsettingLoadBalancer extends LoadBalancer {
private final GracefulSwitchLoadBalancer switchLb;
private final HashFunction hashFunc;

public RandomSubsettingLoadBalancer(Helper helper) {
this(helper, new Random().nextInt());
}

@VisibleForTesting
RandomSubsettingLoadBalancer(Helper helper, int seed) {
switchLb = new GracefulSwitchLoadBalancer(checkNotNull(helper, "helper"));
hashFunc = Hashing.murmur3_128(seed);
}

@Override
public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
RandomSubsettingLoadBalancerConfig config =
(RandomSubsettingLoadBalancerConfig)
resolvedAddresses.getLoadBalancingPolicyConfig();

ResolvedAddresses subsetAddresses = filterEndpoints(resolvedAddresses, config.subsetSize);

return switchLb.acceptResolvedAddresses(
subsetAddresses.toBuilder()
.setLoadBalancingPolicyConfig(config.childConfig)
.build());
}

// implements the subsetting algorithm, as described in A68:
// https://github.com/grpc/proposal/pull/423
private ResolvedAddresses filterEndpoints(ResolvedAddresses resolvedAddresses, int subsetSize) {
if (subsetSize >= resolvedAddresses.getAddresses().size()) {
return resolvedAddresses;
}

ArrayList<EndpointWithHash> endpointWithHashList =
new ArrayList<>(resolvedAddresses.getAddresses().size());

for (EquivalentAddressGroup addressGroup : resolvedAddresses.getAddresses()) {
HashCode hashCode = hashFunc.hashString(
addressGroup.getAddresses().get(0).toString(),
StandardCharsets.UTF_8);
endpointWithHashList.add(new EndpointWithHash(addressGroup, hashCode.asLong()));
}

Collections.sort(endpointWithHashList, new HashAddressComparator());

ArrayList<EquivalentAddressGroup> addressGroups = new ArrayList<>(subsetSize);

for (int idx = 0; idx < subsetSize; ++idx) {
addressGroups.add(endpointWithHashList.get(idx).addressGroup);
}

return resolvedAddresses.toBuilder().setAddresses(addressGroups).build();
}

@Override
public void handleNameResolutionError(Status error) {
switchLb.handleNameResolutionError(error);
}

@Override
public void shutdown() {
switchLb.shutdown();
}

private static final class EndpointWithHash {
public final EquivalentAddressGroup addressGroup;
public final long hashCode;

public EndpointWithHash(EquivalentAddressGroup addressGroup, long hashCode) {
this.addressGroup = addressGroup;
this.hashCode = hashCode;
}
}

private static final class HashAddressComparator implements Comparator<EndpointWithHash> {
@Override
public int compare(EndpointWithHash lhs, EndpointWithHash rhs) {
return Long.compare(lhs.hashCode, rhs.hashCode);
}
}

public static final class RandomSubsettingLoadBalancerConfig {
public final int subsetSize;
public final Object childConfig;

private RandomSubsettingLoadBalancerConfig(int subsetSize, Object childConfig) {
this.subsetSize = subsetSize;
this.childConfig = childConfig;
}

public static class Builder {
int subsetSize;
Object childConfig;

public Builder setSubsetSize(long subsetSize) {
checkArgument(subsetSize > 0L, "Subset size must be greater than 0");
// clamping subset size to Integer.MAX_VALUE due to collection indexing limitations in JVM
this.subsetSize = Ints.saturatedCast(subsetSize);
return this;
}

public Builder setChildConfig(Object childConfig) {
this.childConfig = checkNotNull(childConfig, "childConfig");
return this;
}

public RandomSubsettingLoadBalancerConfig build() {
checkState(subsetSize != 0L, "Subset size must be set before building the config");
return new RandomSubsettingLoadBalancerConfig(
subsetSize,
checkNotNull(childConfig, "childConfig"));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2025 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.util;

import io.grpc.Internal;
import io.grpc.LoadBalancer;
import io.grpc.LoadBalancerProvider;
import io.grpc.NameResolver.ConfigOrError;
import io.grpc.Status;
import io.grpc.internal.JsonUtil;
import java.util.Map;

@Internal
public final class RandomSubsettingLoadBalancerProvider extends LoadBalancerProvider {
private static final String POLICY_NAME = "random_subsetting_experimental";

@Override
public LoadBalancer newLoadBalancer(LoadBalancer.Helper helper) {
return new RandomSubsettingLoadBalancer(helper);
}

@Override
public boolean isAvailable() {
return true;
}

@Override
public int getPriority() {
return 5;
}

@Override
public String getPolicyName() {
return POLICY_NAME;
}

@Override
public ConfigOrError parseLoadBalancingPolicyConfig(Map<String, ?> rawConfig) {
try {
return parseLoadBalancingPolicyConfigInternal(rawConfig);
} catch (RuntimeException e) {
return ConfigOrError.fromError(
Status.UNAVAILABLE
.withCause(e)
.withDescription("Failed parsing configuration for " + getPolicyName()));
}
}

private ConfigOrError parseLoadBalancingPolicyConfigInternal(Map<String, ?> rawConfig) {
Long subsetSize = JsonUtil.getNumberAsLong(rawConfig, "subsetSize");
if (subsetSize == null) {
return ConfigOrError.fromError(
Status.UNAVAILABLE.withDescription(
"Subset size missing in " + getPolicyName() + ", LB policy config=" + rawConfig));
}

ConfigOrError childConfig = GracefulSwitchLoadBalancer.parseLoadBalancingPolicyConfig(
JsonUtil.getListOfObjects(rawConfig, "childPolicy"));
if (childConfig.getError() != null) {
return ConfigOrError.fromError(Status.UNAVAILABLE
.withDescription(
"Failed to parse child in " + getPolicyName() + ", LB policy config=" + rawConfig)
.withCause(childConfig.getError().asRuntimeException()));
}

return ConfigOrError.fromConfig(
new RandomSubsettingLoadBalancer.RandomSubsettingLoadBalancerConfig.Builder()
.setSubsetSize(subsetSize)
.setChildConfig(childConfig.getConfig())
.build());
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
io.grpc.util.SecretRoundRobinLoadBalancerProvider$Provider
io.grpc.util.OutlierDetectionLoadBalancerProvider
io.grpc.util.RandomSubsettingLoadBalancerProvider
Loading