Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
7 changes: 6 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,11 @@ public void stockProviders() {
assertThat(outlierDetection.getClass().getName()).isEqualTo(
"io.grpc.util.OutlierDetectionLoadBalancerProvider");
assertThat(roundRobin.getPriority()).isEqualTo(5);

LoadBalancerProvider randomSubsetting = defaultRegistry.getProvider("random_subsetting");
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";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to ask around to remember if we decided it should initially be _experimental until a user says it works.


@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