Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ include ":grpc-inprocess"
include ":grpc-util"
include ":grpc-opentelemetry"
include ":grpc-context-override-opentelemetry"
include ":grpc-third-party:zero-allocation-hashing"

project(':grpc-api').projectDir = "$rootDir/api" as File
project(':grpc-core').projectDir = "$rootDir/core" as File
Expand Down Expand Up @@ -130,6 +131,7 @@ project(':grpc-inprocess').projectDir = "$rootDir/inprocess" as File
project(':grpc-util').projectDir = "$rootDir/util" as File
project(':grpc-opentelemetry').projectDir = "$rootDir/opentelemetry" as File
project(':grpc-context-override-opentelemetry').projectDir = "$rootDir/contextstorage" as File
project(':grpc-third-party:zero-allocation-hashing').projectDir = "$rootDir/third-party/zero-allocation-hashing" as File

if (settings.hasProperty('skipCodegen') && skipCodegen.toBoolean()) {
println '*** Skipping the build of codegen and compilation of proto files because skipCodegen=true'
Expand Down
26 changes: 26 additions & 0 deletions third-party/zero-allocation-hashing/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
load("@rules_java//java:defs.bzl", "java_binary", "java_library", "java_test")

java_library(
name = "zero-allocation-hashing",
srcs = [
"src/main/java/io/grpc/tp/zah/XxHash64.java",
],
deps = [
"@maven//:com_google_guava_guava",
],
visibility = [
"//xds:__pkg__",
"//util:__pkg__",
],
)

java_test(
name = "XxHash64Test",
size = "small",
srcs = ["src/test/java/io/grpc/tp/zah/XxHash64Test.java"],
deps = [
":zero-allocation-hashing",
"@maven//:com_google_guava_guava",
"@maven//:junit_junit",
],
)
25 changes: 25 additions & 0 deletions third-party/zero-allocation-hashing/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id "java-library"
}

description = 'gRPC: Zero Allocation Hashing'

dependencies {
implementation libraries.guava

testImplementation libraries.junit
}

tasks.named("jar").configure {
manifest {
attributes('Automatic-Module-Name': 'io.grpc.tp.zah')
}
}

tasks.named("checkstyleMain").configure {
enabled = false
}

tasks.named("checkstyleTest").configure {
enabled = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* Modified by the gRPC Authors
*/

package io.grpc.xds;
package io.grpc.tp.zah;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
Expand All @@ -33,8 +33,8 @@
* <a href="https://github.com/OpenHFT/Zero-Allocation-Hashing/blob/master/src/main/java/net/openhft/hashing/XxHash.java">
* OpenHFT/Zero-Allocation-Hashing</a>.
*/
final class XxHash64 {
static final XxHash64 INSTANCE = new XxHash64(0);
final public class XxHash64 {
static public final XxHash64 INSTANCE = new XxHash64(0);

// Primes if treated as unsigned
private static final long P1 = -7046029288634856825L;
Expand All @@ -47,12 +47,12 @@ final class XxHash64 {
private final long seed;
private final long voidHash;

XxHash64(long seed) {
public XxHash64(long seed) {
this.seed = seed;
this.voidHash = finalize(seed + P5);
}

long hashLong(long input) {
public long hashLong(long input) {
input = byteOrder == ByteOrder.LITTLE_ENDIAN ? input : Long.reverseBytes(input);
long hash = seed + P5 + 8;
input *= P2;
Expand All @@ -63,15 +63,15 @@ long hashLong(long input) {
return finalize(hash);
}

long hashInt(int input) {
public long hashInt(int input) {
input = byteOrder == ByteOrder.LITTLE_ENDIAN ? input : Integer.reverseBytes(input);
long hash = seed + P5 + 4;
hash ^= (input & 0xFFFFFFFFL) * P1;
hash = Long.rotateLeft(hash, 23) * P2 + P3;
return finalize(hash);
}

long hashShort(short input) {
public long hashShort(short input) {
input = byteOrder == ByteOrder.LITTLE_ENDIAN ? input : Short.reverseBytes(input);
long hash = seed + P5 + 2;
hash ^= (input & 0xFFL) * P5;
Expand All @@ -81,22 +81,22 @@ long hashShort(short input) {
return finalize(hash);
}

long hashChar(char input) {
public long hashChar(char input) {
return hashShort((short) input);
}

long hashByte(byte input) {
public long hashByte(byte input) {
long hash = seed + P5 + 1;
hash ^= (input & 0xFF) * P5;
hash = Long.rotateLeft(hash, 11) * P1;
return finalize(hash);
}

long hashVoid() {
public long hashVoid() {
return voidHash;
}

long hashAsciiString(String input) {
public long hashAsciiString(String input) {
ByteSupplier supplier = new AsciiStringByteSupplier(input);
return hashBytes(supplier);
}
Expand All @@ -106,7 +106,7 @@ long hashBytes(byte[] bytes) {
return hashBytes(supplier);
}

long hashBytes(byte[] bytes, int offset, int len) {
public long hashBytes(byte[] bytes, int offset, int len) {
ByteSupplier supplier = new PlainByteSupplier(bytes, offset, len);
return hashBytes(supplier);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* Modified by the gRPC Authors
*/

package io.grpc.xds;
package io.grpc.tp.zah;

import static org.junit.Assert.assertEquals;

Expand Down
1 change: 1 addition & 0 deletions util/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ java_library(
deps = [
"//api",
"//core:internal",
"//third-party/zero-allocation-hashing",
artifact("com.google.code.findbugs:jsr305"),
artifact("com.google.errorprone:error_prone_annotations"),
artifact("com.google.guava:guava"),
Expand Down
1 change: 1 addition & 0 deletions util/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
api project(':grpc-api')

implementation project(':grpc-core'),
project(':grpc-third-party:zero-allocation-hashing'),
libraries.animalsniffer.annotations,
libraries.guava
testImplementation libraries.guava.testlib,
Expand Down
158 changes: 158 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,158 @@
/*
* 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 io.grpc.EquivalentAddressGroup;
import io.grpc.Internal;
import io.grpc.LoadBalancer;
import io.grpc.Status;
import io.grpc.tp.zah.XxHash64;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;


/**
* 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
*/
@Internal
public final class RandomSubsettingLoadBalancer extends LoadBalancer {
private final GracefulSwitchLoadBalancer switchLb;

public RandomSubsettingLoadBalancer(Helper helper) {
switchLb = new GracefulSwitchLoadBalancer(checkNotNull(helper, "helper"));
}

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

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

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, long subsetSize, long seed) {
// configured subset sizes in the range [Integer.MAX_VALUE, Long.MAX_VALUE] will always fall
// into this if statement due to collection indexing limitations in JVM
if (subsetSize >= resolvedAddresses.getAddresses().size()) {
return resolvedAddresses;
}

XxHash64 hashFunc = new XxHash64(seed);
ArrayList<EndpointWithHash> endpointWithHashList = new ArrayList<>();

for (EquivalentAddressGroup addressGroup : resolvedAddresses.getAddresses()) {
endpointWithHashList.add(
new EndpointWithHash(
addressGroup,
hashFunc.hashAsciiString(addressGroup.getAddresses().get(0).toString())));
}

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

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

// for loop is executed for subset sizes in range [0, Integer.MAX_VALUE), therefore indexing
// variable is not going to overflow here
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 hash;

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

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

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

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

public static class Builder {
Long subsetSize;
Object childConfig;

public Builder setSubsetSize(Integer subsetSize) {
checkNotNull(subsetSize, "subsetSize");
// {@code Integer.toUnsignedLong(int)} is not part of Android API level 21, therefore doing
// it manually
Long subsetSizeAsLong = ((long) subsetSize) & 0xFFFFFFFFL;
checkArgument(subsetSizeAsLong > 0L, "Subset size must be greater than 0");
this.subsetSize = subsetSizeAsLong;
return this;
}

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

public RandomSubsettingLoadBalancerConfig build() {
return new RandomSubsettingLoadBalancerConfig(
checkNotNull(subsetSize, "subsetSize"),
checkNotNull(childConfig, "childConfig"));
}
}
}
}
Loading
Loading