+ * Handshaker service accepts a stream of handshaker request, returning a + * stream of handshaker response. Client is expected to send exactly one + * message with either client_start or server_start followed by one or more + * messages with next. Each time client sends a request, the handshaker + * service expects to respond. Client does not have to wait for service's + * response before sending next request. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall
+ * Gets the backend distribution for RPCs sent by a test client. + *+ */ + public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getGetClientStatsMethod(), getCallOptions(), request); + } + + /** + *
+ * Gets the accumulated stats for RPCs sent by a test client. + *+ */ + public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service LoadBalancerStatsService. + *
+ * A service used to obtain stats for verifying LB behavior. + *+ */ public static final class LoadBalancerStatsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * Returns the values of all the gauges that are currently being maintained by + * the service + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall, io.grpc.testing.integration.Metrics.GaugeResponse> + getAllGauges(io.grpc.testing.integration.Metrics.EmptyMessage request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getGetAllGaugesMethod(), getCallOptions(), request); + } + + /** + *
+ * Returns the value of one gauge + *+ */ + public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getGetGaugeMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service MetricsService. + */ public static final class MetricsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * A service used to control reconnect server. + *+ */ public static final class ReconnectServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * One empty request followed by one empty response. + *+ */ + public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getEmptyCallMethod(), getCallOptions(), request); + } + + /** + *
+ * One request followed by one response. + *+ */ + public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+ * One request followed by one response. Response has cache control + * headers set such that a caching HTTP proxy (such as GFE) can + * satisfy subsequent requests. + *+ */ + public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+ * One request followed by a sequence of responses (streamed download). + * The server returns the payload with client desired type and sizes. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall, io.grpc.testing.integration.Messages.StreamingOutputCallResponse> + streamingOutputCall(io.grpc.testing.integration.Messages.StreamingOutputCallRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamingOutputCallMethod(), getCallOptions(), request); + } + + /** + *
+ * A sequence of requests followed by one response (streamed upload). + * The server returns the aggregated size of client payload as the result. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall
+ * A sequence of requests with each request served by the server immediately. + * As one request could lead to multiple responses, this interface + * demonstrates the idea of full duplexing. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall
+ * A sequence of requests followed by a sequence of responses. + * The server buffers all the client requests and then serves them in order. A + * stream of responses are returned to the client when the server starts with + * first request. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall
+ * The test server will not implement this method. It will be used + * to test the behavior when clients call unimplemented methods. + *+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service TestService. + *
+ * A simple service to test the various types of RPCs and experiment with + * performance with various types of payload. + *+ */ public static final class TestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * A call that no server should implement + *+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service UnimplementedService. + *
+ * A simple service NOT implemented at servers so clients can test for + * that case. + *+ */ public static final class UnimplementedServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * Update the tes client's configuration. + *+ */ + public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getConfigureMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service XdsUpdateClientConfigureService. + *
+ * A service to dynamically update the configuration of an xDS test client. + *+ */ public static final class XdsUpdateClientConfigureServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * A service to remotely control health status of an xDS test server. + *+ */ public static final class XdsUpdateHealthServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * Gets the backend distribution for RPCs sent by a test client. + *+ */ + public io.grpc.testing.integration.Messages.LoadBalancerStatsResponse getClientStats(io.grpc.testing.integration.Messages.LoadBalancerStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getGetClientStatsMethod(), getCallOptions(), request); + } + + /** + *
+ * Gets the accumulated stats for RPCs sent by a test client. + *+ */ + public io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsResponse getClientAccumulatedStats(io.grpc.testing.integration.Messages.LoadBalancerAccumulatedStatsRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getGetClientAccumulatedStatsMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service LoadBalancerStatsService. + *
+ * A service used to obtain stats for verifying LB behavior. + *+ */ public static final class LoadBalancerStatsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * Returns the values of all the gauges that are currently being maintained by + * the service + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall, io.grpc.testing.integration.Metrics.GaugeResponse> + getAllGauges(io.grpc.testing.integration.Metrics.EmptyMessage request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getGetAllGaugesMethod(), getCallOptions(), request); + } + + /** + *
+ * Returns the value of one gauge + *+ */ + public io.grpc.testing.integration.Metrics.GaugeResponse getGauge(io.grpc.testing.integration.Metrics.GaugeRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getGetGaugeMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service MetricsService. + */ public static final class MetricsServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * A service used to control reconnect server. + *+ */ public static final class ReconnectServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * One empty request followed by one empty response. + *+ */ + public io.grpc.testing.integration.EmptyProtos.Empty emptyCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getEmptyCallMethod(), getCallOptions(), request); + } + + /** + *
+ * One request followed by one response. + *+ */ + public io.grpc.testing.integration.Messages.SimpleResponse unaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+ * One request followed by one response. Response has cache control + * headers set such that a caching HTTP proxy (such as GFE) can + * satisfy subsequent requests. + *+ */ + public io.grpc.testing.integration.Messages.SimpleResponse cacheableUnaryCall(io.grpc.testing.integration.Messages.SimpleRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getCacheableUnaryCallMethod(), getCallOptions(), request); + } + + /** + *
+ * One request followed by a sequence of responses (streamed download). + * The server returns the payload with client desired type and sizes. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall, io.grpc.testing.integration.Messages.StreamingOutputCallResponse> + streamingOutputCall(io.grpc.testing.integration.Messages.StreamingOutputCallRequest request) { + return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall( + getChannel(), getStreamingOutputCallMethod(), getCallOptions(), request); + } + + /** + *
+ * A sequence of requests followed by one response (streamed upload). + * The server returns the aggregated size of client payload as the result. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall
+ * A sequence of requests with each request served by the server immediately. + * As one request could lead to multiple responses, this interface + * demonstrates the idea of full duplexing. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall
+ * A sequence of requests followed by a sequence of responses. + * The server buffers all the client requests and then serves them in order. A + * stream of responses are returned to the client when the server starts with + * first request. + *+ */ + @io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918") + public io.grpc.stub.BlockingClientCall
+ * The test server will not implement this method. It will be used + * to test the behavior when clients call unimplemented methods. + *+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service TestService. + *
+ * A simple service to test the various types of RPCs and experiment with + * performance with various types of payload. + *+ */ public static final class TestServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * A call that no server should implement + *+ */ + public io.grpc.testing.integration.EmptyProtos.Empty unimplementedCall(io.grpc.testing.integration.EmptyProtos.Empty request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getUnimplementedCallMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service UnimplementedService. + *
+ * A simple service NOT implemented at servers so clients can test for + * that case. + *+ */ public static final class UnimplementedServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * Update the tes client's configuration. + *+ */ + public io.grpc.testing.integration.Messages.ClientConfigureResponse configure(io.grpc.testing.integration.Messages.ClientConfigureRequest request) throws io.grpc.StatusException { + return io.grpc.stub.ClientCalls.blockingV2UnaryCall( + getChannel(), getConfigureMethod(), getCallOptions(), request); + } + } + + /** + * A stub to allow clients to do limited synchronous rpc calls to service XdsUpdateClientConfigureService. + *
+ * A service to dynamically update the configuration of an xDS test client. + *+ */ public static final class XdsUpdateClientConfigureServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
+ * A service to remotely control health status of an xDS test server. + *+ */ public static final class XdsUpdateHealthServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub
It is called for each individual RPC, within the {@link Context} of the call, before the
* stream is about to be created on a transport. Implementations should not block in this
* method. If metadata is not immediately available, e.g., needs to be fetched from network, the
- * implementation may give the {@code applier} to an asynchronous task which will eventually call
+ * implementation may give the {@code appExecutor} an asynchronous task which will eventually call
* the {@code applier}. The RPC proceeds only after the {@code applier} is called.
*
* @param requestInfo request-related information
diff --git a/api/src/main/java/io/grpc/CallOptions.java b/api/src/main/java/io/grpc/CallOptions.java
index 25c4df386a1..800bdfb6c90 100644
--- a/api/src/main/java/io/grpc/CallOptions.java
+++ b/api/src/main/java/io/grpc/CallOptions.java
@@ -17,16 +17,18 @@
package io.grpc;
import static com.google.common.base.Preconditions.checkArgument;
+import static io.grpc.TimeUtils.convertToNanos;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
+import com.google.errorprone.annotations.CheckReturnValue;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
-import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
@@ -176,6 +178,11 @@ public CallOptions withDeadlineAfter(long duration, TimeUnit unit) {
return withDeadline(Deadline.after(duration, unit));
}
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657")
+ public CallOptions withDeadlineAfter(Duration duration) {
+ return withDeadlineAfter(convertToNanos(duration), TimeUnit.NANOSECONDS);
+ }
+
/**
* Returns the deadline or {@code null} if the deadline is not set.
*/
diff --git a/api/src/main/java/io/grpc/ClientCall.java b/api/src/main/java/io/grpc/ClientCall.java
index df9e15001e1..c915c8beaac 100644
--- a/api/src/main/java/io/grpc/ClientCall.java
+++ b/api/src/main/java/io/grpc/ClientCall.java
@@ -67,7 +67,7 @@
* manner, and notifies gRPC library to receive additional response after one is consumed by
* a fictional processResponse().
*
- *
+ *
* call = channel.newCall(bidiStreamingMethod, callOptions);
* listener = new ClientCall.Listener<FooResponse>() {
* @Override
diff --git a/api/src/main/java/io/grpc/ClientStreamTracer.java b/api/src/main/java/io/grpc/ClientStreamTracer.java
index 2f366b7404c..42e1fdfebea 100644
--- a/api/src/main/java/io/grpc/ClientStreamTracer.java
+++ b/api/src/main/java/io/grpc/ClientStreamTracer.java
@@ -132,12 +132,15 @@ public static final class StreamInfo {
private final CallOptions callOptions;
private final int previousAttempts;
private final boolean isTransparentRetry;
+ private final boolean isHedging;
StreamInfo(
- CallOptions callOptions, int previousAttempts, boolean isTransparentRetry) {
+ CallOptions callOptions, int previousAttempts, boolean isTransparentRetry,
+ boolean isHedging) {
this.callOptions = checkNotNull(callOptions, "callOptions");
this.previousAttempts = previousAttempts;
this.isTransparentRetry = isTransparentRetry;
+ this.isHedging = isHedging;
}
/**
@@ -165,6 +168,15 @@ public boolean isTransparentRetry() {
return isTransparentRetry;
}
+ /**
+ * Whether the stream is hedging.
+ *
+ * @since 1.74.0
+ */
+ public boolean isHedging() {
+ return isHedging;
+ }
+
/**
* Converts this StreamInfo into a new Builder.
*
@@ -174,7 +186,9 @@ public Builder toBuilder() {
return new Builder()
.setCallOptions(callOptions)
.setPreviousAttempts(previousAttempts)
- .setIsTransparentRetry(isTransparentRetry);
+ .setIsTransparentRetry(isTransparentRetry)
+ .setIsHedging(isHedging);
+
}
/**
@@ -192,6 +206,7 @@ public String toString() {
.add("callOptions", callOptions)
.add("previousAttempts", previousAttempts)
.add("isTransparentRetry", isTransparentRetry)
+ .add("isHedging", isHedging)
.toString();
}
@@ -204,6 +219,7 @@ public static final class Builder {
private CallOptions callOptions = CallOptions.DEFAULT;
private int previousAttempts;
private boolean isTransparentRetry;
+ private boolean isHedging;
Builder() {
}
@@ -236,11 +252,21 @@ public Builder setIsTransparentRetry(boolean isTransparentRetry) {
return this;
}
+ /**
+ * Sets whether the stream is hedging.
+ *
+ * @since 1.74.0
+ */
+ public Builder setIsHedging(boolean isHedging) {
+ this.isHedging = isHedging;
+ return this;
+ }
+
/**
* Builds a new StreamInfo.
*/
public StreamInfo build() {
- return new StreamInfo(callOptions, previousAttempts, isTransparentRetry);
+ return new StreamInfo(callOptions, previousAttempts, isTransparentRetry, isHedging);
}
}
}
diff --git a/api/src/main/java/io/grpc/ConfiguratorRegistry.java b/api/src/main/java/io/grpc/ConfiguratorRegistry.java
index b2efcc1cff4..19d6703d308 100644
--- a/api/src/main/java/io/grpc/ConfiguratorRegistry.java
+++ b/api/src/main/java/io/grpc/ConfiguratorRegistry.java
@@ -16,10 +16,10 @@
package io.grpc;
+import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import javax.annotation.concurrent.GuardedBy;
/**
* A registry for {@link Configurator} instances.
@@ -33,9 +33,9 @@ final class ConfiguratorRegistry {
@GuardedBy("this")
private boolean wasConfiguratorsSet;
@GuardedBy("this")
- private boolean configFrozen;
- @GuardedBy("this")
private List configurators = Collections.emptyList();
+ @GuardedBy("this")
+ private int configuratorsCallCountBeforeSet = 0;
ConfiguratorRegistry() {}
@@ -56,11 +56,10 @@ public static synchronized ConfiguratorRegistry getDefaultRegistry() {
* @throws IllegalStateException if this method is called more than once
*/
public synchronized void setConfigurators(List extends Configurator> configurators) {
- if (configFrozen) {
+ if (wasConfiguratorsSet) {
throw new IllegalStateException("Configurators are already set");
}
this.configurators = Collections.unmodifiableList(new ArrayList<>(configurators));
- configFrozen = true;
wasConfiguratorsSet = true;
}
@@ -68,10 +67,20 @@ public synchronized void setConfigurators(List extends Configurator> configura
* Returns a list of the configurators in this registry.
*/
public synchronized List getConfigurators() {
- configFrozen = true;
+ if (!wasConfiguratorsSet) {
+ configuratorsCallCountBeforeSet++;
+ }
return configurators;
}
+ /**
+ * Returns the number of times getConfigurators() was called before
+ * setConfigurators() was successfully invoked.
+ */
+ public synchronized int getConfiguratorsCallCountBeforeSet() {
+ return configuratorsCallCountBeforeSet;
+ }
+
public synchronized boolean wasSetConfiguratorsCalled() {
return wasConfiguratorsSet;
}
diff --git a/api/src/main/java/io/grpc/ConnectivityState.java b/api/src/main/java/io/grpc/ConnectivityState.java
index 677039b2517..a7407efb2e9 100644
--- a/api/src/main/java/io/grpc/ConnectivityState.java
+++ b/api/src/main/java/io/grpc/ConnectivityState.java
@@ -20,7 +20,7 @@
* The connectivity states.
*
* @see
- * more information
+ * more information
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4359")
public enum ConnectivityState {
diff --git a/api/src/main/java/io/grpc/EquivalentAddressGroup.java b/api/src/main/java/io/grpc/EquivalentAddressGroup.java
index 4b3db006684..18151e88aba 100644
--- a/api/src/main/java/io/grpc/EquivalentAddressGroup.java
+++ b/api/src/main/java/io/grpc/EquivalentAddressGroup.java
@@ -50,6 +50,20 @@ public final class EquivalentAddressGroup {
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/6138")
public static final Attributes.Key ATTR_AUTHORITY_OVERRIDE =
Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE");
+ /**
+ * The name of the locality that this EquivalentAddressGroup is in.
+ */
+ public static final Attributes.Key ATTR_LOCALITY_NAME =
+ Attributes.Key.create("io.grpc.EquivalentAddressGroup.LOCALITY");
+ /**
+ * Endpoint weight for load balancing purposes. While the type is Long, it must be a valid uint32.
+ * Must not be zero. The weight is proportional to the other endpoints; if an endpoint's weight is
+ * twice that of another endpoint, it is intended to receive twice the load.
+ */
+ @Attr
+ static final Attributes.Key ATTR_WEIGHT =
+ Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_WEIGHT");
+
private final List addrs;
private final Attributes attrs;
@@ -108,7 +122,9 @@ public Attributes getAttributes() {
@Override
public String toString() {
- // TODO(zpencer): Summarize return value if addr is very large
+ // EquivalentAddressGroup is intended to contain a small number of addresses for the same
+ // endpoint(e.g., IPv4/IPv6). Aggregating many groups into a single EquivalentAddressGroup
+ // is no longer done, so this no longer needs summarization.
return "[" + addrs + "/" + attrs + "]";
}
diff --git a/api/src/main/java/io/grpc/FeatureFlags.java b/api/src/main/java/io/grpc/FeatureFlags.java
new file mode 100644
index 00000000000..0e414ed7b31
--- /dev/null
+++ b/api/src/main/java/io/grpc/FeatureFlags.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2026 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;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+
+class FeatureFlags {
+ private static boolean enableRfc3986Uris = getFlag("GRPC_ENABLE_RFC3986_URIS", false);
+
+ /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */
+ @VisibleForTesting
+ static boolean setRfc3986UrisEnabled(boolean value) {
+ boolean prevValue = enableRfc3986Uris;
+ enableRfc3986Uris = value;
+ return prevValue;
+ }
+
+ /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */
+ static boolean getRfc3986UrisEnabled() {
+ return enableRfc3986Uris;
+ }
+
+ static boolean getFlag(String envVarName, boolean enableByDefault) {
+ String envVar = System.getenv(envVarName);
+ if (envVar == null) {
+ envVar = System.getProperty(envVarName);
+ }
+ if (envVar != null) {
+ envVar = envVar.trim();
+ }
+ if (enableByDefault) {
+ return Strings.isNullOrEmpty(envVar) || Boolean.parseBoolean(envVar);
+ } else {
+ return !Strings.isNullOrEmpty(envVar) && Boolean.parseBoolean(envVar);
+ }
+ }
+
+ private FeatureFlags() {}
+}
diff --git a/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java b/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java
index 7f21a57ec80..78fe730d91a 100644
--- a/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java
+++ b/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java
@@ -263,6 +263,12 @@ protected T addMetricSink(MetricSink metricSink) {
return thisT();
}
+ @Override
+ public T setNameResolverArg(NameResolver.Args.Key key, X value) {
+ delegate().setNameResolverArg(key, value);
+ return thisT();
+ }
+
/**
* Returns the {@link ManagedChannel} built by the delegate by default. Overriding method can
* return different value.
diff --git a/api/src/main/java/io/grpc/ForwardingServerBuilder.java b/api/src/main/java/io/grpc/ForwardingServerBuilder.java
index 9cef7cfa331..d1f183dd824 100644
--- a/api/src/main/java/io/grpc/ForwardingServerBuilder.java
+++ b/api/src/main/java/io/grpc/ForwardingServerBuilder.java
@@ -201,6 +201,12 @@ public Server build() {
return delegate().build();
}
+ @Override
+ public T addMetricSink(MetricSink metricSink) {
+ delegate().addMetricSink(metricSink);
+ return thisT();
+ }
+
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("delegate", delegate()).toString();
diff --git a/api/src/main/java/io/grpc/Grpc.java b/api/src/main/java/io/grpc/Grpc.java
index baa9f5f0ab6..a45c613fd18 100644
--- a/api/src/main/java/io/grpc/Grpc.java
+++ b/api/src/main/java/io/grpc/Grpc.java
@@ -56,6 +56,13 @@ private Grpc() {
public static final Attributes.Key TRANSPORT_ATTR_SSL_SESSION =
Attributes.Key.create("io.grpc.Grpc.TRANSPORT_ATTR_SSL_SESSION");
+ /**
+ * The value for the custom label of per-RPC metrics. Defaults to empty string when unset. Must
+ * not be set to {@code null}.
+ */
+ public static final CallOptions.Key CALL_OPTION_CUSTOM_LABEL =
+ CallOptions.Key.createWithDefault("io.grpc.Grpc.CALL_OPTION_CUSTOM_LABEL", "");
+
/**
* Annotation for transport attributes. It follows the annotation semantics defined
* by {@link Attributes}.
diff --git a/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java b/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java
index d59c53db1d1..0df8dc452c1 100644
--- a/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java
+++ b/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java
@@ -23,6 +23,9 @@
import com.google.common.base.Objects;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import javax.annotation.Nullable;
/**
@@ -33,6 +36,8 @@ public final class HttpConnectProxiedSocketAddress extends ProxiedSocketAddress
private final SocketAddress proxyAddress;
private final InetSocketAddress targetAddress;
+ @SuppressWarnings("serial")
+ private final Map headers;
@Nullable
private final String username;
@Nullable
@@ -41,6 +46,7 @@ public final class HttpConnectProxiedSocketAddress extends ProxiedSocketAddress
private HttpConnectProxiedSocketAddress(
SocketAddress proxyAddress,
InetSocketAddress targetAddress,
+ Map headers,
@Nullable String username,
@Nullable String password) {
checkNotNull(proxyAddress, "proxyAddress");
@@ -53,6 +59,7 @@ private HttpConnectProxiedSocketAddress(
}
this.proxyAddress = proxyAddress;
this.targetAddress = targetAddress;
+ this.headers = headers;
this.username = username;
this.password = password;
}
@@ -87,6 +94,14 @@ public InetSocketAddress getTargetAddress() {
return targetAddress;
}
+ /**
+ * Returns the custom HTTP headers to be sent during the HTTP CONNECT handshake.
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12479")
+ public Map getHeaders() {
+ return headers;
+ }
+
@Override
public boolean equals(Object o) {
if (!(o instanceof HttpConnectProxiedSocketAddress)) {
@@ -95,13 +110,14 @@ public boolean equals(Object o) {
HttpConnectProxiedSocketAddress that = (HttpConnectProxiedSocketAddress) o;
return Objects.equal(proxyAddress, that.proxyAddress)
&& Objects.equal(targetAddress, that.targetAddress)
+ && Objects.equal(headers, that.headers)
&& Objects.equal(username, that.username)
&& Objects.equal(password, that.password);
}
@Override
public int hashCode() {
- return Objects.hashCode(proxyAddress, targetAddress, username, password);
+ return Objects.hashCode(proxyAddress, targetAddress, username, password, headers);
}
@Override
@@ -109,6 +125,7 @@ public String toString() {
return MoreObjects.toStringHelper(this)
.add("proxyAddr", proxyAddress)
.add("targetAddr", targetAddress)
+ .add("headers", headers)
.add("username", username)
// Intentionally mask out password
.add("hasPassword", password != null)
@@ -129,6 +146,7 @@ public static final class Builder {
private SocketAddress proxyAddress;
private InetSocketAddress targetAddress;
+ private Map headers = Collections.emptyMap();
@Nullable
private String username;
@Nullable
@@ -153,6 +171,18 @@ public Builder setTargetAddress(InetSocketAddress targetAddress) {
return this;
}
+ /**
+ * Sets custom HTTP headers to be sent during the HTTP CONNECT handshake. This is an optional
+ * field. The headers will be sent in addition to any authentication headers (if username and
+ * password are set).
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12479")
+ public Builder setHeaders(Map headers) {
+ this.headers = Collections.unmodifiableMap(
+ new HashMap<>(checkNotNull(headers, "headers")));
+ return this;
+ }
+
/**
* Sets the username used to connect to the proxy. This is an optional field and can be {@code
* null}.
@@ -175,7 +205,8 @@ public Builder setPassword(@Nullable String password) {
* Creates an {@code HttpConnectProxiedSocketAddress}.
*/
public HttpConnectProxiedSocketAddress build() {
- return new HttpConnectProxiedSocketAddress(proxyAddress, targetAddress, username, password);
+ return new HttpConnectProxiedSocketAddress(
+ proxyAddress, targetAddress, headers, username, password);
}
}
}
diff --git a/api/src/main/java/io/grpc/IgnoreJRERequirement.java b/api/src/main/java/io/grpc/IgnoreJRERequirement.java
new file mode 100644
index 00000000000..2db406c5953
--- /dev/null
+++ b/api/src/main/java/io/grpc/IgnoreJRERequirement.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Disables Animal Sniffer's signature checking. This is our own package-private version to avoid
+ * dependening on animalsniffer-annotations.
+ *
+ * FIELD is purposefully not supported, as Android wouldn't be able to ignore a field. Instead,
+ * the entire class would need to be avoided on Android.
+ */
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
+@interface IgnoreJRERequirement {}
diff --git a/api/src/main/java/io/grpc/InternalConfigSelector.java b/api/src/main/java/io/grpc/InternalConfigSelector.java
index 38856f440b4..a63009361d4 100644
--- a/api/src/main/java/io/grpc/InternalConfigSelector.java
+++ b/api/src/main/java/io/grpc/InternalConfigSelector.java
@@ -35,7 +35,7 @@ public abstract class InternalConfigSelector {
= Attributes.Key.create("internal:io.grpc.config-selector");
// Use PickSubchannelArgs for SelectConfigArgs for now. May change over time.
- /** Selects the config for an PRC. */
+ /** Selects the config for an RPC. */
public abstract Result selectConfig(LoadBalancer.PickSubchannelArgs args);
public static final class Result {
diff --git a/api/src/main/java/io/grpc/InternalConfiguratorRegistry.java b/api/src/main/java/io/grpc/InternalConfiguratorRegistry.java
index b495800ff13..f567dab74c4 100644
--- a/api/src/main/java/io/grpc/InternalConfiguratorRegistry.java
+++ b/api/src/main/java/io/grpc/InternalConfiguratorRegistry.java
@@ -48,4 +48,8 @@ public static void configureServerBuilder(ServerBuilder> serverBuilder) {
public static boolean wasSetConfiguratorsCalled() {
return ConfiguratorRegistry.getDefaultRegistry().wasSetConfiguratorsCalled();
}
+
+ public static int getConfiguratorsCallCountBeforeSet() {
+ return ConfiguratorRegistry.getDefaultRegistry().getConfiguratorsCallCountBeforeSet();
+ }
}
diff --git a/api/src/main/java/io/grpc/InternalEquivalentAddressGroup.java b/api/src/main/java/io/grpc/InternalEquivalentAddressGroup.java
new file mode 100644
index 00000000000..d4bed4d81bc
--- /dev/null
+++ b/api/src/main/java/io/grpc/InternalEquivalentAddressGroup.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2026 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;
+
+@Internal
+public final class InternalEquivalentAddressGroup {
+ private InternalEquivalentAddressGroup() {}
+
+ /**
+ * Endpoint weight for load balancing purposes. While the type is Long, it must be a valid uint32.
+ * Must not be zero. The weight is proportional to the other endpoints; if an endpoint's weight is
+ * twice that of another endpoint, it is intended to receive twice the load.
+ */
+ public static final Attributes.Key ATTR_WEIGHT = EquivalentAddressGroup.ATTR_WEIGHT;
+}
diff --git a/api/src/main/java/io/grpc/InternalFeatureFlags.java b/api/src/main/java/io/grpc/InternalFeatureFlags.java
new file mode 100644
index 00000000000..a1e771a7571
--- /dev/null
+++ b/api/src/main/java/io/grpc/InternalFeatureFlags.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2026 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;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/** Global variables that govern major changes to the behavior of more than one grpc module. */
+@Internal
+public class InternalFeatureFlags {
+
+ /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */
+ @VisibleForTesting
+ public static boolean setRfc3986UrisEnabled(boolean value) {
+ return FeatureFlags.setRfc3986UrisEnabled(value);
+ }
+
+ /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */
+ public static boolean getRfc3986UrisEnabled() {
+ return FeatureFlags.getRfc3986UrisEnabled();
+ }
+
+ public static boolean getFlag(String envVarName, boolean enableByDefault) {
+ return FeatureFlags.getFlag(envVarName, enableByDefault);
+ }
+
+ private InternalFeatureFlags() {}
+}
diff --git a/api/src/main/java/io/grpc/InternalServiceProviders.java b/api/src/main/java/io/grpc/InternalServiceProviders.java
index c72e01db67a..debc786a82a 100644
--- a/api/src/main/java/io/grpc/InternalServiceProviders.java
+++ b/api/src/main/java/io/grpc/InternalServiceProviders.java
@@ -17,7 +17,9 @@
package io.grpc;
import com.google.common.annotations.VisibleForTesting;
+import java.util.Iterator;
import java.util.List;
+import java.util.ServiceLoader;
@Internal
public final class InternalServiceProviders {
@@ -27,12 +29,17 @@ private InternalServiceProviders() {
/**
* Accessor for method.
*/
- public static T load(
+ @Deprecated
+ public static List loadAll(
Class klass,
- Iterable> hardcoded,
+ Iterable> hardCodedClasses,
ClassLoader classLoader,
PriorityAccessor priorityAccessor) {
- return ServiceProviders.load(klass, hardcoded, classLoader, priorityAccessor);
+ return loadAll(
+ klass,
+ ServiceLoader.load(klass, classLoader).iterator(),
+ () -> hardCodedClasses,
+ priorityAccessor);
}
/**
@@ -40,10 +47,10 @@ public static T load(
*/
public static List loadAll(
Class klass,
- Iterable> hardCodedClasses,
- ClassLoader classLoader,
+ Iterator serviceLoader,
+ Supplier>> hardCodedClasses,
PriorityAccessor priorityAccessor) {
- return ServiceProviders.loadAll(klass, hardCodedClasses, classLoader, priorityAccessor);
+ return ServiceProviders.loadAll(klass, serviceLoader, hardCodedClasses::get, priorityAccessor);
}
/**
@@ -71,4 +78,8 @@ public static boolean isAndroid(ClassLoader cl) {
}
public interface PriorityAccessor extends ServiceProviders.PriorityAccessor {}
+
+ public interface Supplier {
+ T get();
+ }
}
diff --git a/api/src/main/java/io/grpc/InternalStatus.java b/api/src/main/java/io/grpc/InternalStatus.java
index b6549bb435f..56df1decf38 100644
--- a/api/src/main/java/io/grpc/InternalStatus.java
+++ b/api/src/main/java/io/grpc/InternalStatus.java
@@ -38,12 +38,11 @@ private InternalStatus() {}
public static final Metadata.Key CODE_KEY = Status.CODE_KEY;
/**
- * Create a new {@link StatusRuntimeException} with the internal option of skipping the filling
- * of the stack trace.
+ * Create a new {@link StatusRuntimeException} skipping the filling of the stack trace.
*/
@Internal
- public static final StatusRuntimeException asRuntimeException(Status status,
- @Nullable Metadata trailers, boolean fillInStackTrace) {
- return new StatusRuntimeException(status, trailers, fillInStackTrace);
+ public static StatusRuntimeException asRuntimeExceptionWithoutStacktrace(Status status,
+ @Nullable Metadata trailers) {
+ return new InternalStatusRuntimeException(status, trailers);
}
}
diff --git a/api/src/main/java/io/grpc/InternalStatusRuntimeException.java b/api/src/main/java/io/grpc/InternalStatusRuntimeException.java
new file mode 100644
index 00000000000..6090b701f0b
--- /dev/null
+++ b/api/src/main/java/io/grpc/InternalStatusRuntimeException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2015 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;
+
+import javax.annotation.Nullable;
+
+/**
+ * StatusRuntimeException without stack trace, implemented as a subclass, as the
+ * {@code String, Throwable, boolean, boolean} constructor is not available in the supported
+ * version of Android.
+ *
+ * @see StatusRuntimeException
+ */
+class InternalStatusRuntimeException extends StatusRuntimeException {
+ private static final long serialVersionUID = 0;
+
+ public InternalStatusRuntimeException(Status status, @Nullable Metadata trailers) {
+ super(status, trailers);
+ }
+
+ @Override
+ public synchronized Throwable fillInStackTrace() {
+ return this;
+ }
+}
diff --git a/api/src/main/java/io/grpc/InternalTcpMetrics.java b/api/src/main/java/io/grpc/InternalTcpMetrics.java
new file mode 100644
index 00000000000..3dd89b6f76c
--- /dev/null
+++ b/api/src/main/java/io/grpc/InternalTcpMetrics.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2026 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;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * TCP Metrics defined to be shared across transport implementations.
+ * These metrics and their definitions are specified in
+ * gRFC
+ * A80.
+ */
+@Internal
+public final class InternalTcpMetrics {
+
+ private InternalTcpMetrics() {
+ }
+
+ private static final List OPTIONAL_LABELS = Arrays.asList(
+ "network.local.address",
+ "network.local.port",
+ "network.peer.address",
+ "network.peer.port");
+
+ public static final DoubleHistogramMetricInstrument MIN_RTT_INSTRUMENT =
+ MetricInstrumentRegistry.getDefaultRegistry()
+ .registerDoubleHistogram(
+ "grpc.tcp.min_rtt",
+ "Minimum round-trip time of a TCP connection",
+ "s",
+ Collections.emptyList(),
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+ public static final LongCounterMetricInstrument CONNECTIONS_CREATED_INSTRUMENT =
+ MetricInstrumentRegistry
+ .getDefaultRegistry()
+ .registerLongCounter(
+ "grpc.tcp.connections_created",
+ "The total number of TCP connections established.",
+ "{connection}",
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+ public static final LongUpDownCounterMetricInstrument CONNECTION_COUNT_INSTRUMENT =
+ MetricInstrumentRegistry
+ .getDefaultRegistry()
+ .registerLongUpDownCounter(
+ "grpc.tcp.connection_count",
+ "The current number of active TCP connections.",
+ "{connection}",
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+ public static final LongCounterMetricInstrument PACKETS_RETRANSMITTED_INSTRUMENT =
+ MetricInstrumentRegistry
+ .getDefaultRegistry()
+ .registerLongCounter(
+ "grpc.tcp.packets_retransmitted",
+ "The total number of packets retransmitted for all TCP connections.",
+ "{packet}",
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+ public static final LongCounterMetricInstrument RECURRING_RETRANSMITS_INSTRUMENT =
+ MetricInstrumentRegistry
+ .getDefaultRegistry()
+ .registerLongCounter(
+ "grpc.tcp.recurring_retransmits",
+ "The total number of times the retransmit timer "
+ + "popped for all TCP connections.",
+ "{timeout}",
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+}
diff --git a/api/src/main/java/io/grpc/InternalTimeUtils.java b/api/src/main/java/io/grpc/InternalTimeUtils.java
new file mode 100644
index 00000000000..ef8022f53c5
--- /dev/null
+++ b/api/src/main/java/io/grpc/InternalTimeUtils.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 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;
+
+import java.time.Duration;
+
+@Internal
+public final class InternalTimeUtils {
+ public static long convert(Duration duration) {
+ return TimeUtils.convertToNanos(duration);
+ }
+}
diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java
index 0fbce5fa5be..3187ae8ef1b 100644
--- a/api/src/main/java/io/grpc/LoadBalancer.java
+++ b/api/src/main/java/io/grpc/LoadBalancer.java
@@ -121,6 +121,12 @@ public abstract class LoadBalancer {
HEALTH_CONSUMER_LISTENER_ARG_KEY =
LoadBalancer.CreateSubchannelArgs.Key.create("internal:health-check-consumer-listener");
+ @Internal
+ public static final LoadBalancer.CreateSubchannelArgs.Key
+ DISABLE_SUBCHANNEL_RECONNECT_KEY =
+ LoadBalancer.CreateSubchannelArgs.Key.createWithDefault(
+ "internal:disable-subchannel-reconnect", Boolean.FALSE);
+
@Internal
public static final Attributes.Key
HAS_HEALTH_PRODUCER_LISTENER_KEY =
@@ -150,15 +156,16 @@ public String toString() {
private int recursionCount;
/**
- * Handles newly resolved server groups and metadata attributes from name resolution system.
- * {@code servers} contained in {@link EquivalentAddressGroup} should be considered equivalent
- * but may be flattened into a single list if needed.
- *
- * Implementations should not modify the given {@code servers}.
+ * Handles newly resolved addresses and metadata attributes from name resolution system.
+ * Addresses in {@link EquivalentAddressGroup} should be considered equivalent but may be
+ * flattened into a single list if needed.
*
* @param resolvedAddresses the resolved server addresses, attributes, and config.
* @since 1.21.0
+ *
+ * @deprecated Use instead {@link #acceptResolvedAddresses(ResolvedAddresses)}
*/
+ @Deprecated
public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
if (recursionCount++ == 0) {
// Note that the information about the addresses actually being accepted will be lost
@@ -173,12 +180,10 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
* EquivalentAddressGroup} addresses should be considered equivalent but may be flattened into a
* single list if needed.
*
- *
Implementations can choose to reject the given addresses by returning {@code false}.
- *
- *
Implementations should not modify the given {@code addresses}.
+ * @param resolvedAddresses the resolved server addresses, attributes, and config
+ * @return {@code Status.OK} if the resolved addresses were accepted, otherwise an error to report
+ * to the name resolver
*
- * @param resolvedAddresses the resolved server addresses, attributes, and config.
- * @return {@code true} if the resolved addresses were accepted. {@code false} if rejected.
* @since 1.49.0
*/
public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
@@ -206,7 +211,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
*
* @since 1.21.0
*/
- @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1771")
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657")
public static final class ResolvedAddresses {
private final List addresses;
@NameResolver.ResolutionResultAttr
@@ -412,7 +417,16 @@ public void handleSubchannelState(
*
* This method should always return a constant value. It's not specified when this will be
* called.
+ *
+ *
Note that this method is only called when implementing {@code handleResolvedAddresses()}
+ * instead of {@code acceptResolvedAddresses()}.
+ *
+ * @deprecated Instead of overwriting this and {@code handleResolvedAddresses()}, only
+ * overwrite {@code acceptResolvedAddresses()} which indicates if the addresses provided
+ * by the name resolver are acceptable with the {@code boolean} return value.
*/
+ @Deprecated
+ @SuppressWarnings("InlineMeSuggester")
public boolean canHandleEmptyAddressListFromNameResolution() {
return false;
}
@@ -446,18 +460,6 @@ public abstract static class SubchannelPicker {
* @since 1.3.0
*/
public abstract PickResult pickSubchannel(PickSubchannelArgs args);
-
- /**
- * Tries to establish connections now so that the upcoming RPC may then just pick a ready
- * connection without having to connect first.
- *
- *
No-op if unsupported.
- *
- * @deprecated override {@link LoadBalancer#requestConnection} instead.
- * @since 1.11.0
- */
- @Deprecated
- public void requestConnection() {}
}
/**
@@ -546,6 +548,7 @@ public static final class PickResult {
private final Status status;
// True if the result is created by withDrop()
private final boolean drop;
+ @Nullable private final String authorityOverride;
private PickResult(
@Nullable Subchannel subchannel, @Nullable ClientStreamTracer.Factory streamTracerFactory,
@@ -554,6 +557,17 @@ private PickResult(
this.streamTracerFactory = streamTracerFactory;
this.status = checkNotNull(status, "status");
this.drop = drop;
+ this.authorityOverride = null;
+ }
+
+ private PickResult(
+ @Nullable Subchannel subchannel, @Nullable ClientStreamTracer.Factory streamTracerFactory,
+ Status status, boolean drop, @Nullable String authorityOverride) {
+ this.subchannel = subchannel;
+ this.streamTracerFactory = streamTracerFactory;
+ this.status = checkNotNull(status, "status");
+ this.drop = drop;
+ this.authorityOverride = authorityOverride;
}
/**
@@ -626,6 +640,8 @@ private PickResult(
* stream is created at all in some cases.
* @since 1.3.0
*/
+ // TODO(shivaspeaks): Need to deprecate old APIs and create new ones,
+ // per https://github.com/grpc/grpc-java/issues/12662.
public static PickResult withSubchannel(
Subchannel subchannel, @Nullable ClientStreamTracer.Factory streamTracerFactory) {
return new PickResult(
@@ -633,6 +649,19 @@ public static PickResult withSubchannel(
false);
}
+ /**
+ * Same as {@code withSubchannel(subchannel, streamTracerFactory)} but with an authority name
+ * to override in the host header.
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11656")
+ public static PickResult withSubchannel(
+ Subchannel subchannel, @Nullable ClientStreamTracer.Factory streamTracerFactory,
+ @Nullable String authorityOverride) {
+ return new PickResult(
+ checkNotNull(subchannel, "subchannel"), streamTracerFactory, Status.OK,
+ false, authorityOverride);
+ }
+
/**
* Equivalent to {@code withSubchannel(subchannel, null)}.
*
@@ -642,6 +671,28 @@ public static PickResult withSubchannel(Subchannel subchannel) {
return withSubchannel(subchannel, null);
}
+ /**
+ * Creates a new {@code PickResult} with the given {@code subchannel},
+ * but retains all other properties from this {@code PickResult}.
+ *
+ * @since 1.80.0
+ */
+ public PickResult copyWithSubchannel(Subchannel subchannel) {
+ return new PickResult(checkNotNull(subchannel, "subchannel"), streamTracerFactory,
+ status, drop, authorityOverride);
+ }
+
+ /**
+ * Creates a new {@code PickResult} with the given {@code streamTracerFactory},
+ * but retains all other properties from this {@code PickResult}.
+ *
+ * @since 1.80.0
+ */
+ public PickResult copyWithStreamTracerFactory(
+ @Nullable ClientStreamTracer.Factory streamTracerFactory) {
+ return new PickResult(subchannel, streamTracerFactory, status, drop, authorityOverride);
+ }
+
/**
* A decision to report a connectivity error to the RPC. If the RPC is {@link
* CallOptions#withWaitForReady wait-for-ready}, it will stay buffered. Otherwise, it will fail
@@ -676,6 +727,13 @@ public static PickResult withNoResult() {
return NO_RESULT;
}
+ /** Returns the authority override if any. */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11656")
+ @Nullable
+ public String getAuthorityOverride() {
+ return authorityOverride;
+ }
+
/**
* The Subchannel if this result was created by {@link #withSubchannel withSubchannel()}, or
* null otherwise.
@@ -730,6 +788,7 @@ public String toString() {
.add("streamTracerFactory", streamTracerFactory)
.add("status", status)
.add("drop", drop)
+ .add("authority-override", authorityOverride)
.toString();
}
@@ -828,9 +887,11 @@ public String toString() {
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1771")
public static final class Builder {
+ private static final Object[][] EMPTY_CUSTOM_OPTIONS = new Object[0][2];
+
private List addrs;
private Attributes attrs = Attributes.EMPTY;
- private Object[][] customOptions = new Object[0][2];
+ private Object[][] customOptions = EMPTY_CUSTOM_OPTIONS;
Builder() {
}
@@ -994,8 +1055,8 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) {
}
/**
- * Out-of-band channel for LoadBalancer’s own RPC needs, e.g., talking to an external
- * load-balancer service.
+ * Create an out-of-band channel for the LoadBalancer’s own RPC needs, e.g., talking to an
+ * external load-balancer service.
*
* The LoadBalancer is responsible for closing unused OOB channels, and closing all OOB
* channels within {@link #shutdown}.
@@ -1005,7 +1066,12 @@ public Subchannel createSubchannel(CreateSubchannelArgs args) {
public abstract ManagedChannel createOobChannel(EquivalentAddressGroup eag, String authority);
/**
- * Accept a list of EAG for multiple authorities: https://github.com/grpc/grpc-java/issues/4618
+ * Create an out-of-band channel for the LoadBalancer's own RPC needs, e.g., talking to an
+ * external load-balancer service. This version of the method allows multiple EAGs, so different
+ * addresses can have different authorities.
+ *
+ *
The LoadBalancer is responsible for closing unused OOB channels, and closing all OOB
+ * channels within {@link #shutdown}.
* */
public ManagedChannel createOobChannel(List eag,
String authority) {
@@ -1157,6 +1223,10 @@ public void ignoreRefreshNameResolutionCheck() {
* Returns a {@link SynchronizationContext} that runs tasks in the same Synchronization Context
* as that the callback methods on the {@link LoadBalancer} interface are run in.
*
+ * Work added to the synchronization context might not run immediately, so LB implementations
+ * must be careful to ensure that any assumptions still hold when it is executed. In particular,
+ * the LB might have been shut down or subchannels might have changed state.
+ *
*
Pro-tip: in order to call {@link SynchronizationContext#schedule}, you need to provide a
* {@link ScheduledExecutorService}. {@link #getScheduledExecutorService} is provided for your
* convenience.
diff --git a/api/src/main/java/io/grpc/LoadBalancerProvider.java b/api/src/main/java/io/grpc/LoadBalancerProvider.java
index bb4c574211e..7dc30d6baaf 100644
--- a/api/src/main/java/io/grpc/LoadBalancerProvider.java
+++ b/api/src/main/java/io/grpc/LoadBalancerProvider.java
@@ -81,7 +81,7 @@ public abstract class LoadBalancerProvider extends LoadBalancer.Factory {
* @return a tuple of the fully parsed and validated balancer configuration, else the Status.
* @since 1.20.0
* @see
- * A24-lb-policy-config.md
+ * A24-lb-policy-config.md
*/
public ConfigOrError parseLoadBalancingPolicyConfig(Map rawLoadBalancingPolicyConfig) {
return UNKNOWN_CONFIG;
diff --git a/api/src/main/java/io/grpc/LoadBalancerRegistry.java b/api/src/main/java/io/grpc/LoadBalancerRegistry.java
index f6b69f978b8..a8fbc102f5f 100644
--- a/api/src/main/java/io/grpc/LoadBalancerRegistry.java
+++ b/api/src/main/java/io/grpc/LoadBalancerRegistry.java
@@ -26,6 +26,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@@ -42,7 +43,6 @@
public final class LoadBalancerRegistry {
private static final Logger logger = Logger.getLogger(LoadBalancerRegistry.class.getName());
private static LoadBalancerRegistry instance;
- private static final Iterable> HARDCODED_CLASSES = getHardCodedClasses();
private final LinkedHashSet allProviders =
new LinkedHashSet<>();
@@ -101,8 +101,10 @@ public static synchronized LoadBalancerRegistry getDefaultRegistry() {
if (instance == null) {
List providerList = ServiceProviders.loadAll(
LoadBalancerProvider.class,
- HARDCODED_CLASSES,
- LoadBalancerProvider.class.getClassLoader(),
+ ServiceLoader
+ .load(LoadBalancerProvider.class, LoadBalancerProvider.class.getClassLoader())
+ .iterator(),
+ LoadBalancerRegistry::getHardCodedClasses,
new LoadBalancerPriorityAccessor());
instance = new LoadBalancerRegistry();
for (LoadBalancerProvider provider : providerList) {
diff --git a/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java b/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java
new file mode 100644
index 00000000000..07e099cde5d
--- /dev/null
+++ b/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+import java.util.List;
+
+/**
+ * Represents a long-valued up down counter metric instrument.
+ */
+@Internal
+public final class LongUpDownCounterMetricInstrument extends PartialMetricInstrument {
+ public LongUpDownCounterMetricInstrument(int index, String name, String description, String unit,
+ List requiredLabelKeys,
+ List optionalLabelKeys,
+ boolean enableByDefault) {
+ super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/grpc/ManagedChannelBuilder.java b/api/src/main/java/io/grpc/ManagedChannelBuilder.java
index 6e30d8eae04..3f370ab3003 100644
--- a/api/src/main/java/io/grpc/ManagedChannelBuilder.java
+++ b/api/src/main/java/io/grpc/ManagedChannelBuilder.java
@@ -374,9 +374,17 @@ public T maxInboundMetadataSize(int bytes) {
* notice when they are causing excessive load. Clients are strongly encouraged to use only as
* small of a value as necessary.
*
+ * When the channel implementation supports TCP_USER_TIMEOUT, enabling keepalive will also
+ * enable TCP_USER_TIMEOUT for the connection. This requires all sent packets to receive
+ * a TCP acknowledgement before the keepalive timeout. The keepalive time is not used for
+ * TCP_USER_TIMEOUT, except as a signal to enable the feature. grpc-netty supports
+ * TCP_USER_TIMEOUT on Linux platforms supported by netty-transport-native-epoll.
+ *
* @throws UnsupportedOperationException if unsupported
* @see gRFC A8
* Client-side Keepalive
+ * @see gRFC A18
+ * TCP User Timeout
* @since 1.7.0
*/
public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
@@ -393,6 +401,8 @@ public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
* @throws UnsupportedOperationException if unsupported
* @see gRFC A8
* Client-side Keepalive
+ * @see gRFC A18
+ * TCP User Timeout
* @since 1.7.0
*/
public T keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) {
@@ -633,6 +643,23 @@ protected T addMetricSink(MetricSink metricSink) {
throw new UnsupportedOperationException();
}
+ /**
+ * Provides a "custom" argument for the {@link NameResolver}, if applicable, replacing any 'value'
+ * previously provided for 'key'.
+ *
+ *
NB: If the selected {@link NameResolver} does not understand 'key', or target URI resolution
+ * isn't needed at all, your custom argument will be silently ignored.
+ *
+ *
See {@link NameResolver.Args#getArg(NameResolver.Args.Key)} for more.
+ *
+ * @param key identifies the argument in a type-safe manner
+ * @param value the argument itself
+ * @return this
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
+ public T setNameResolverArg(NameResolver.Args.Key key, X value) {
+ throw new UnsupportedOperationException();
+ }
/**
* Builds a channel using the given parameters.
diff --git a/api/src/main/java/io/grpc/ManagedChannelRegistry.java b/api/src/main/java/io/grpc/ManagedChannelRegistry.java
index 31f874b8094..ec47b325ffc 100644
--- a/api/src/main/java/io/grpc/ManagedChannelRegistry.java
+++ b/api/src/main/java/io/grpc/ManagedChannelRegistry.java
@@ -18,6 +18,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
+import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
@@ -28,9 +29,9 @@
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
-import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
@@ -100,8 +101,10 @@ public static synchronized ManagedChannelRegistry getDefaultRegistry() {
if (instance == null) {
List providerList = ServiceProviders.loadAll(
ManagedChannelProvider.class,
- getHardCodedClasses(),
- ManagedChannelProvider.class.getClassLoader(),
+ ServiceLoader
+ .load(ManagedChannelProvider.class, ManagedChannelProvider.class.getClassLoader())
+ .iterator(),
+ ManagedChannelRegistry::getHardCodedClasses,
new ManagedChannelPriorityAccessor());
instance = new ManagedChannelRegistry();
for (ManagedChannelProvider provider : providerList) {
@@ -160,8 +163,11 @@ ManagedChannelBuilder> newChannelBuilder(NameResolverRegistry nameResolverRegi
String target, ChannelCredentials creds) {
NameResolverProvider nameResolverProvider = null;
try {
- URI uri = new URI(target);
- nameResolverProvider = nameResolverRegistry.getProviderForScheme(uri.getScheme());
+ String scheme =
+ FeatureFlags.getRfc3986UrisEnabled()
+ ? Uri.parse(target).getScheme()
+ : new URI(target).getScheme();
+ nameResolverProvider = nameResolverRegistry.getProviderForScheme(scheme);
} catch (URISyntaxException ignore) {
// bad URI found, just ignore and continue
}
diff --git a/api/src/main/java/io/grpc/Metadata.java b/api/src/main/java/io/grpc/Metadata.java
index fba2659776b..8a958d127df 100644
--- a/api/src/main/java/io/grpc/Metadata.java
+++ b/api/src/main/java/io/grpc/Metadata.java
@@ -22,6 +22,8 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
@@ -32,8 +34,6 @@
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@@ -325,7 +325,7 @@ public Set keys() {
if (isEmpty()) {
return Collections.emptySet();
}
- Set ks = new HashSet<>(size);
+ Set ks = Sets.newHashSetWithExpectedSize(size);
for (int i = 0; i < size; i++) {
ks.add(new String(name(i), 0 /* hibyte */));
}
@@ -526,7 +526,7 @@ public void merge(Metadata other) {
public void merge(Metadata other, Set> keys) {
Preconditions.checkNotNull(other, "other");
// Use ByteBuffer for equals and hashCode.
- Map> asciiKeys = new HashMap<>(keys.size());
+ Map> asciiKeys = Maps.newHashMapWithExpectedSize(keys.size());
for (Key> key : keys) {
asciiKeys.put(ByteBuffer.wrap(key.asciiName()), key);
}
diff --git a/api/src/main/java/io/grpc/MethodDescriptor.java b/api/src/main/java/io/grpc/MethodDescriptor.java
index 1bfaccb4201..a02eb840deb 100644
--- a/api/src/main/java/io/grpc/MethodDescriptor.java
+++ b/api/src/main/java/io/grpc/MethodDescriptor.java
@@ -20,9 +20,9 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
+import com.google.errorprone.annotations.CheckReturnValue;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReferenceArray;
-import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
diff --git a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java
index a61ac058a61..ce0f8f1b5cb 100644
--- a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java
+++ b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java
@@ -21,12 +21,12 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
+import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import javax.annotation.concurrent.GuardedBy;
/**
* A registry for globally registered metric instruments.
@@ -144,6 +144,47 @@ public LongCounterMetricInstrument registerLongCounter(String name,
}
}
+ /**
+ * Registers a new Long Up Down Counter metric instrument.
+ *
+ * @param name the name of the metric
+ * @param description a description of the metric
+ * @param unit the unit of measurement for the metric
+ * @param requiredLabelKeys a list of required label keys
+ * @param optionalLabelKeys a list of optional label keys
+ * @param enableByDefault whether the metric should be enabled by default
+ * @return the newly created LongUpDownCounterMetricInstrument
+ * @throws IllegalStateException if a metric with the same name already exists
+ */
+ public LongUpDownCounterMetricInstrument registerLongUpDownCounter(String name,
+ String description,
+ String unit,
+ List requiredLabelKeys,
+ List optionalLabelKeys,
+ boolean enableByDefault) {
+ checkArgument(!Strings.isNullOrEmpty(name), "missing metric name");
+ checkNotNull(description, "description");
+ checkNotNull(unit, "unit");
+ checkNotNull(requiredLabelKeys, "requiredLabelKeys");
+ checkNotNull(optionalLabelKeys, "optionalLabelKeys");
+ synchronized (lock) {
+ if (registeredMetricNames.contains(name)) {
+ throw new IllegalStateException("Metric with name " + name + " already exists");
+ }
+ int index = nextAvailableMetricIndex;
+ if (index + 1 == metricInstruments.length) {
+ resizeMetricInstruments();
+ }
+ LongUpDownCounterMetricInstrument instrument = new LongUpDownCounterMetricInstrument(
+ index, name, description, unit, requiredLabelKeys, optionalLabelKeys,
+ enableByDefault);
+ metricInstruments[index] = instrument;
+ registeredMetricNames.add(name);
+ nextAvailableMetricIndex += 1;
+ return instrument;
+ }
+ }
+
/**
* Registers a new Double Histogram metric instrument.
*
diff --git a/api/src/main/java/io/grpc/MetricRecorder.java b/api/src/main/java/io/grpc/MetricRecorder.java
index d418dcbf590..897c28011cd 100644
--- a/api/src/main/java/io/grpc/MetricRecorder.java
+++ b/api/src/main/java/io/grpc/MetricRecorder.java
@@ -50,7 +50,7 @@ default void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, do
* Adds a value for a long valued counter metric instrument.
*
* @param metricInstrument The counter metric instrument to add the value against.
- * @param value The value to add.
+ * @param value The value to add. MUST be non-negative.
* @param requiredLabelValues A list of required label values for the metric.
* @param optionalLabelValues A list of additional, optional label values for the metric.
*/
@@ -66,6 +66,29 @@ default void addLongCounter(LongCounterMetricInstrument metricInstrument, long v
metricInstrument.getOptionalLabelKeys().size());
}
+ /**
+ * Adds a value for a long valued up down counter metric instrument.
+ *
+ * @param metricInstrument The counter metric instrument to add the value against.
+ * @param value The value to add. May be positive, negative or zero.
+ * @param requiredLabelValues A list of required label values for the metric.
+ * @param optionalLabelValues A list of additional, optional label values for the metric.
+ */
+ default void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument,
+ long value,
+ List requiredLabelValues,
+ List optionalLabelValues) {
+ checkArgument(requiredLabelValues != null
+ && requiredLabelValues.size() == metricInstrument.getRequiredLabelKeys().size(),
+ "Incorrect number of required labels provided. Expected: %s",
+ metricInstrument.getRequiredLabelKeys().size());
+ checkArgument(optionalLabelValues != null
+ && optionalLabelValues.size() == metricInstrument.getOptionalLabelKeys().size(),
+ "Incorrect number of optional labels provided. Expected: %s",
+ metricInstrument.getOptionalLabelKeys().size());
+ }
+
+
/**
* Records a value for a double-precision histogram metric instrument.
*
diff --git a/api/src/main/java/io/grpc/MetricSink.java b/api/src/main/java/io/grpc/MetricSink.java
index 0f56b1acb73..ce5d3822520 100644
--- a/api/src/main/java/io/grpc/MetricSink.java
+++ b/api/src/main/java/io/grpc/MetricSink.java
@@ -65,12 +65,26 @@ default void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, do
* Adds a value for a long valued counter metric associated with specified metric instrument.
*
* @param metricInstrument The counter metric instrument identifies metric measure to add.
- * @param value The value to record.
+ * @param value The value to record. MUST be non-negative.
* @param requiredLabelValues A list of required label values for the metric.
* @param optionalLabelValues A list of additional, optional label values for the metric.
*/
default void addLongCounter(LongCounterMetricInstrument metricInstrument, long value,
- List requiredLabelValues, List optionalLabelValues) {
+ List requiredLabelValues, List optionalLabelValues) {
+ }
+
+ /**
+ * Adds a value for a long valued up down counter metric associated with specified metric
+ * instrument.
+ *
+ * @param metricInstrument The counter metric instrument identifies metric measure to add.
+ * @param value The value to record. May be positive, negative or zero.
+ * @param requiredLabelValues A list of required label values for the metric.
+ * @param optionalLabelValues A list of additional, optional label values for the metric.
+ */
+ default void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value,
+ List requiredLabelValues,
+ List optionalLabelValues) {
}
/**
diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java
index bfb9c2a43a1..e44a26309ae 100644
--- a/api/src/main/java/io/grpc/NameResolver.java
+++ b/api/src/main/java/io/grpc/NameResolver.java
@@ -20,19 +20,21 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.InlineMe;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URI;
-import java.util.ArrayList;
import java.util.Collections;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
/**
@@ -95,7 +97,14 @@ public void onError(Status error) {
@Override
public void onResult(ResolutionResult resolutionResult) {
- listener.onAddresses(resolutionResult.getAddresses(), resolutionResult.getAttributes());
+ StatusOr> addressesOrError =
+ resolutionResult.getAddressesOrError();
+ if (addressesOrError.hasValue()) {
+ listener.onAddresses(addressesOrError.getValue(),
+ resolutionResult.getAttributes());
+ } else {
+ listener.onError(addressesOrError.getStatus());
+ }
}
});
}
@@ -149,6 +158,10 @@ public abstract static class Factory {
* cannot be resolved by this factory. The decision should be solely based on the scheme of the
* URI.
*
+ * This method will eventually be deprecated and removed as part of a migration from {@code
+ * java.net.URI} to {@code io.grpc.Uri}. Implementations will override {@link
+ * #newNameResolver(Uri, Args)} instead.
+ *
* @param targetUri the target URI to be resolved, whose scheme must not be {@code null}
* @param args other information that may be useful
*
@@ -156,6 +169,37 @@ public abstract static class Factory {
*/
public abstract NameResolver newNameResolver(URI targetUri, final Args args);
+ /**
+ * Creates a {@link NameResolver} for the given target URI.
+ *
+ *
Implementations return {@code null} if 'targetUri' cannot be resolved by this factory. The
+ * decision should be solely based on the target's scheme.
+ *
+ *
All {@link NameResolver.Factory} implementations should override this method, as it will
+ * eventually replace {@link #newNameResolver(URI, Args)}. For backwards compatibility, this
+ * default implementation delegates to {@link #newNameResolver(URI, Args)} if 'targetUri' can be
+ * converted to a java.net.URI.
+ *
+ *
NB: Conversion is not always possible, for example {@code scheme:#frag} is a valid {@link
+ * Uri} but not a valid {@link URI} because its path is empty. The default implementation throws
+ * IllegalArgumentException in these cases.
+ *
+ * @param targetUri the target URI to be resolved
+ * @param args other information that may be useful
+ * @throws IllegalArgumentException if targetUri does not have the expected form
+ * @since 1.79
+ */
+ public NameResolver newNameResolver(Uri targetUri, final Args args) {
+ // Not every io.grpc.Uri can be converted but in the ordinary ManagedChannel creation flow,
+ // any IllegalArgumentException thrown here would happened anyway, just earlier. That's
+ // because parse/toString is transparent so java.net.URI#create here sees the original target
+ // string just like it did before the io.grpc.Uri migration.
+ //
+ // Throwing IAE shouldn't surprise non-framework callers either. After all, many existing
+ // Factory impls are picky about targetUri and throw IAE when it doesn't look how they expect.
+ return newNameResolver(URI.create(targetUri.toString()), args);
+ }
+
/**
* Returns the default scheme, which will be used to construct a URI when {@link
* ManagedChannelBuilder#forTarget(String)} is given an authority string instead of a compliant
@@ -218,19 +262,26 @@ public abstract static class Listener2 implements Listener {
@Override
@Deprecated
@InlineMe(
- replacement = "this.onResult(ResolutionResult.newBuilder().setAddresses(servers)"
- + ".setAttributes(attributes).build())",
- imports = "io.grpc.NameResolver.ResolutionResult")
+ replacement = "this.onResult(ResolutionResult.newBuilder().setAddressesOrError("
+ + "StatusOr.fromValue(servers)).setAttributes(attributes).build())",
+ imports = {"io.grpc.NameResolver.ResolutionResult", "io.grpc.StatusOr"})
public final void onAddresses(
List servers, @ResolutionResultAttr Attributes attributes) {
// TODO(jihuncho) need to promote Listener2 if we want to use ConfigOrError
+ // Calling onResult and not onResult2 because onResult2 can only be called from a
+ // synchronization context.
onResult(
- ResolutionResult.newBuilder().setAddresses(servers).setAttributes(attributes).build());
+ ResolutionResult.newBuilder().setAddressesOrError(
+ StatusOr.fromValue(servers)).setAttributes(attributes).build());
}
/**
* Handles updates on resolved addresses and attributes. If
- * {@link ResolutionResult#getAddresses()} is empty, {@link #onError(Status)} will be called.
+ * {@link ResolutionResult#getAddressesOrError()} is empty, {@link #onError(Status)} will be
+ * called.
+ *
+ * Newer NameResolver implementations should prefer calling onResult2. This method exists to
+ * facilitate older {@link Listener} implementations to migrate to {@link Listener2}.
*
* @param resolutionResult the resolved server addresses, attributes, and Service Config.
* @since 1.21.0
@@ -241,6 +292,10 @@ public final void onAddresses(
* Handles a name resolving error from the resolver. The listener is responsible for eventually
* invoking {@link NameResolver#refresh()} to re-attempt resolution.
*
+ *
New NameResolver implementations should prefer calling onResult2 which will have the
+ * address resolution error in {@link ResolutionResult}'s addressesOrError. This method exists
+ * to facilitate older implementations using {@link Listener} to migrate to {@link Listener2}.
+ *
* @param error a non-OK status
* @since 1.21.0
*/
@@ -248,9 +303,14 @@ public final void onAddresses(
public abstract void onError(Status error);
/**
- * Handles updates on resolved addresses and attributes.
+ * Handles updates on resolved addresses and attributes. Must be called from the same
+ * {@link SynchronizationContext} available in {@link NameResolver.Args} that is passed
+ * from the channel.
*
- * @param resolutionResult the resolved server addresses, attributes, and Service Config.
+ * @param resolutionResult the resolved server addresses or error in address resolution,
+ * attributes, and Service Config or error
+ * @return status indicating whether the resolutionResult was accepted by the listener,
+ * typically the result from a load balancer.
* @since 1.66
*/
public Status onResult2(ResolutionResult resolutionResult) {
@@ -268,10 +328,20 @@ public Status onResult2(ResolutionResult resolutionResult) {
@Documented
public @interface ResolutionResultAttr {}
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11989")
+ @ResolutionResultAttr
+ public static final Attributes.Key ATTR_BACKEND_SERVICE =
+ Attributes.Key.create("io.grpc.NameResolver.ATTR_BACKEND_SERVICE");
+
/**
* Information that a {@link Factory} uses to create a {@link NameResolver}.
*
- * Note this class doesn't override neither {@code equals()} nor {@code hashCode()}.
+ *
Args applicable to all {@link NameResolver}s are defined here using ordinary setters and
+ * getters. This container can also hold externally-defined "custom" args that aren't so widely
+ * useful or that would be inappropriate dependencies for this low level API. See {@link
+ * Args#getArg} for more.
+ *
+ *
Note this class overrides neither {@code equals()} nor {@code hashCode()}.
*
* @since 1.21.0
*/
@@ -285,24 +355,24 @@ public static final class Args {
@Nullable private final ChannelLogger channelLogger;
@Nullable private final Executor executor;
@Nullable private final String overrideAuthority;
-
- private Args(
- Integer defaultPort,
- ProxyDetector proxyDetector,
- SynchronizationContext syncContext,
- ServiceConfigParser serviceConfigParser,
- @Nullable ScheduledExecutorService scheduledExecutorService,
- @Nullable ChannelLogger channelLogger,
- @Nullable Executor executor,
- @Nullable String overrideAuthority) {
- this.defaultPort = checkNotNull(defaultPort, "defaultPort not set");
- this.proxyDetector = checkNotNull(proxyDetector, "proxyDetector not set");
- this.syncContext = checkNotNull(syncContext, "syncContext not set");
- this.serviceConfigParser = checkNotNull(serviceConfigParser, "serviceConfigParser not set");
- this.scheduledExecutorService = scheduledExecutorService;
- this.channelLogger = channelLogger;
- this.executor = executor;
- this.overrideAuthority = overrideAuthority;
+ private final MetricRecorder metricRecorder;
+ @Nullable private final NameResolverRegistry nameResolverRegistry;
+ @Nullable private final IdentityHashMap, Object> customArgs;
+
+ private Args(Builder builder) {
+ this.defaultPort = checkNotNull(builder.defaultPort, "defaultPort not set");
+ this.proxyDetector = checkNotNull(builder.proxyDetector, "proxyDetector not set");
+ this.syncContext = checkNotNull(builder.syncContext, "syncContext not set");
+ this.serviceConfigParser =
+ checkNotNull(builder.serviceConfigParser, "serviceConfigParser not set");
+ this.scheduledExecutorService = builder.scheduledExecutorService;
+ this.channelLogger = builder.channelLogger;
+ this.executor = builder.executor;
+ this.overrideAuthority = builder.overrideAuthority;
+ this.metricRecorder = builder.metricRecorder != null ? builder.metricRecorder
+ : new MetricRecorder() {};
+ this.nameResolverRegistry = builder.nameResolverRegistry;
+ this.customArgs = cloneCustomArgs(builder.customArgs);
}
/**
@@ -311,6 +381,7 @@ private Args(
*
* @since 1.21.0
*/
+ // TODO: Only meaningful for InetSocketAddress producers. Make this a custom arg?
public int getDefaultPort() {
return defaultPort;
}
@@ -363,6 +434,30 @@ public ServiceConfigParser getServiceConfigParser() {
return serviceConfigParser;
}
+ /**
+ * Returns the value of a custom arg named 'key', or {@code null} if it's not set.
+ *
+ *
While ordinary {@link Args} should be universally useful and meaningful, custom arguments
+ * can apply just to resolvers of a certain URI scheme, just to resolvers producing a particular
+ * type of {@link java.net.SocketAddress}, or even an individual {@link NameResolver} subclass.
+ * Custom args are identified by an instance of {@link Args.Key} which should be a constant
+ * defined in a java package and class appropriate for the argument's scope.
+ *
+ *
{@link Args} are normally reserved for information in *support* of name resolution, not
+ * the name to be resolved itself. However, there are rare cases where all or part of the target
+ * name can't be represented by any standard URI scheme or can't be encoded as a String at all.
+ * Custom args, in contrast, can hold arbitrary Java types, making them a useful work around in
+ * these cases.
+ *
+ *
Custom args can also be used simply to avoid adding inappropriate deps to the low level
+ * io.grpc package.
+ */
+ @SuppressWarnings("unchecked") // Cast is safe because all put()s go through the setArg() API.
+ @Nullable
+ public T getArg(Key key) {
+ return customArgs != null ? (T) customArgs.get(key) : null;
+ }
+
/**
* Returns the {@link ChannelLogger} for the Channel served by this NameResolver.
*
@@ -400,6 +495,25 @@ public String getOverrideAuthority() {
return overrideAuthority;
}
+ /**
+ * Returns the {@link MetricRecorder} that the channel uses to record metrics.
+ */
+ public MetricRecorder getMetricRecorder() {
+ return metricRecorder;
+ }
+
+ /**
+ * Returns the {@link NameResolverRegistry} that the Channel uses to look for {@link
+ * NameResolver}s.
+ *
+ * @since 1.74.0
+ */
+ public NameResolverRegistry getNameResolverRegistry() {
+ if (nameResolverRegistry == null) {
+ throw new IllegalStateException("NameResolverRegistry is not set in Builder");
+ }
+ return nameResolverRegistry;
+ }
@Override
public String toString() {
@@ -408,10 +522,13 @@ public String toString() {
.add("proxyDetector", proxyDetector)
.add("syncContext", syncContext)
.add("serviceConfigParser", serviceConfigParser)
+ .add("customArgs", customArgs)
.add("scheduledExecutorService", scheduledExecutorService)
.add("channelLogger", channelLogger)
.add("executor", executor)
.add("overrideAuthority", overrideAuthority)
+ .add("metricRecorder", metricRecorder)
+ .add("nameResolverRegistry", nameResolverRegistry)
.toString();
}
@@ -430,6 +547,9 @@ public Builder toBuilder() {
builder.setChannelLogger(channelLogger);
builder.setOffloadExecutor(executor);
builder.setOverrideAuthority(overrideAuthority);
+ builder.setMetricRecorder(metricRecorder);
+ builder.setNameResolverRegistry(nameResolverRegistry);
+ builder.customArgs = cloneCustomArgs(customArgs);
return builder;
}
@@ -456,6 +576,9 @@ public static final class Builder {
private ChannelLogger channelLogger;
private Executor executor;
private String overrideAuthority;
+ private MetricRecorder metricRecorder;
+ private NameResolverRegistry nameResolverRegistry;
+ private IdentityHashMap, Object> customArgs;
Builder() {
}
@@ -542,16 +665,75 @@ public Builder setOverrideAuthority(String authority) {
return this;
}
+ /** See {@link Args#getArg(Key)}. */
+ public Builder setArg(Key key, T value) {
+ checkNotNull(key, "key");
+ checkNotNull(value, "value");
+ if (customArgs == null) {
+ customArgs = new IdentityHashMap<>();
+ }
+ customArgs.put(key, value);
+ return this;
+ }
+
+ /**
+ * See {@link Args#getMetricRecorder()}. This is an optional field.
+ */
+ public Builder setMetricRecorder(MetricRecorder metricRecorder) {
+ this.metricRecorder = checkNotNull(metricRecorder, "metricRecorder");
+ return this;
+ }
+
+ /**
+ * See {@link Args#getNameResolverRegistry}. This is an optional field.
+ *
+ * @since 1.74.0
+ */
+ public Builder setNameResolverRegistry(NameResolverRegistry registry) {
+ this.nameResolverRegistry = registry;
+ return this;
+ }
+
/**
* Builds an {@link Args}.
*
* @since 1.21.0
*/
public Args build() {
- return
- new Args(
- defaultPort, proxyDetector, syncContext, serviceConfigParser,
- scheduledExecutorService, channelLogger, executor, overrideAuthority);
+ return new Args(this);
+ }
+ }
+
+ /**
+ * Identifies an externally-defined custom argument that can be stored in {@link Args}.
+ *
+ * Uses reference equality so keys should be defined as global constants.
+ *
+ * @param type of values that can be stored under this key
+ */
+ @Immutable
+ @SuppressWarnings("UnusedTypeParameter")
+ public static final class Key {
+ private final String debugString;
+
+ private Key(String debugString) {
+ this.debugString = debugString;
+ }
+
+ @Override
+ public String toString() {
+ return debugString;
+ }
+
+ /**
+ * Creates a new instance of {@link Key}.
+ *
+ * @param debugString a string used to describe the key, used for debugging.
+ * @param Key type
+ * @return a new instance of Key
+ */
+ public static Key create(String debugString) {
+ return new Key<>(debugString);
}
}
}
@@ -584,17 +766,17 @@ public abstract static class ServiceConfigParser {
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
public static final class ResolutionResult {
- private final List addresses;
+ private final StatusOr> addressesOrError;
@ResolutionResultAttr
private final Attributes attributes;
@Nullable
private final ConfigOrError serviceConfig;
ResolutionResult(
- List addresses,
+ StatusOr> addressesOrError,
@ResolutionResultAttr Attributes attributes,
ConfigOrError serviceConfig) {
- this.addresses = Collections.unmodifiableList(new ArrayList<>(addresses));
+ this.addressesOrError = addressesOrError;
this.attributes = checkNotNull(attributes, "attributes");
this.serviceConfig = serviceConfig;
}
@@ -615,7 +797,7 @@ public static Builder newBuilder() {
*/
public Builder toBuilder() {
return newBuilder()
- .setAddresses(addresses)
+ .setAddressesOrError(addressesOrError)
.setAttributes(attributes)
.setServiceConfig(serviceConfig);
}
@@ -624,9 +806,20 @@ public Builder toBuilder() {
* Gets the addresses resolved by name resolution.
*
* @since 1.21.0
+ * @deprecated Will be superseded by getAddressesOrError
*/
+ @Deprecated
public List getAddresses() {
- return addresses;
+ return addressesOrError.getValue();
+ }
+
+ /**
+ * Gets the addresses resolved by name resolution or the error in doing so.
+ *
+ * @since 1.65.0
+ */
+ public StatusOr> getAddressesOrError() {
+ return addressesOrError;
}
/**
@@ -652,11 +845,11 @@ public ConfigOrError getServiceConfig() {
@Override
public String toString() {
- return MoreObjects.toStringHelper(this)
- .add("addresses", addresses)
- .add("attributes", attributes)
- .add("serviceConfig", serviceConfig)
- .toString();
+ ToStringHelper stringHelper = MoreObjects.toStringHelper(this);
+ stringHelper.add("addressesOrError", addressesOrError.toString());
+ stringHelper.add("attributes", attributes);
+ stringHelper.add("serviceConfigOrError", serviceConfig);
+ return stringHelper.toString();
}
/**
@@ -668,7 +861,7 @@ public boolean equals(Object obj) {
return false;
}
ResolutionResult that = (ResolutionResult) obj;
- return Objects.equal(this.addresses, that.addresses)
+ return Objects.equal(this.addressesOrError, that.addressesOrError)
&& Objects.equal(this.attributes, that.attributes)
&& Objects.equal(this.serviceConfig, that.serviceConfig);
}
@@ -678,7 +871,7 @@ public boolean equals(Object obj) {
*/
@Override
public int hashCode() {
- return Objects.hashCode(addresses, attributes, serviceConfig);
+ return Objects.hashCode(addressesOrError, attributes, serviceConfig);
}
/**
@@ -688,7 +881,8 @@ public int hashCode() {
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
public static final class Builder {
- private List addresses = Collections.emptyList();
+ private StatusOr> addresses =
+ StatusOr.fromValue(Collections.emptyList());
private Attributes attributes = Attributes.EMPTY;
@Nullable
private ConfigOrError serviceConfig;
@@ -700,9 +894,21 @@ public static final class Builder {
* Sets the addresses resolved by name resolution. This field is required.
*
* @since 1.21.0
+ * @deprecated Will be superseded by setAddressesOrError
*/
+ @Deprecated
public Builder setAddresses(List addresses) {
- this.addresses = addresses;
+ setAddressesOrError(StatusOr.fromValue(addresses));
+ return this;
+ }
+
+ /**
+ * Sets the addresses resolved by name resolution or the error in doing so. This field is
+ * required.
+ * @param addresses Resolved addresses or an error in resolving addresses
+ */
+ public Builder setAddressesOrError(StatusOr> addresses) {
+ this.addresses = checkNotNull(addresses, "StatusOr addresses cannot be null.");
return this;
}
@@ -825,4 +1031,10 @@ public String toString() {
}
}
}
+
+ @Nullable
+ private static IdentityHashMap, Object> cloneCustomArgs(
+ @Nullable IdentityHashMap, Object> customArgs) {
+ return customArgs != null ? new IdentityHashMap<>(customArgs) : null;
+ }
}
diff --git a/api/src/main/java/io/grpc/NameResolverRegistry.java b/api/src/main/java/io/grpc/NameResolverRegistry.java
index 23eec23fd6a..c5e9f7467ab 100644
--- a/api/src/main/java/io/grpc/NameResolverRegistry.java
+++ b/api/src/main/java/io/grpc/NameResolverRegistry.java
@@ -20,6 +20,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
@@ -28,10 +29,10 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
-import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
@@ -125,8 +126,10 @@ public static synchronized NameResolverRegistry getDefaultRegistry() {
if (instance == null) {
List providerList = ServiceProviders.loadAll(
NameResolverProvider.class,
- getHardCodedClasses(),
- NameResolverProvider.class.getClassLoader(),
+ ServiceLoader
+ .load(NameResolverProvider.class, NameResolverProvider.class.getClassLoader())
+ .iterator(),
+ NameResolverRegistry::getHardCodedClasses,
new NameResolverPriorityAccessor());
if (providerList.isEmpty()) {
logger.warning("No NameResolverProviders found via ServiceLoader, including for DNS. This "
@@ -166,6 +169,11 @@ static List> getHardCodedClasses() {
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find DNS NameResolver", e);
}
+ try {
+ list.add(Class.forName("io.grpc.binder.internal.IntentNameResolverProvider"));
+ } catch (ClassNotFoundException e) {
+ logger.log(Level.FINE, "Unable to find IntentNameResolverProvider", e);
+ }
return Collections.unmodifiableList(list);
}
@@ -177,6 +185,13 @@ public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
return provider == null ? null : provider.newNameResolver(targetUri, args);
}
+ @Override
+ @Nullable
+ public NameResolver newNameResolver(io.grpc.Uri targetUri, NameResolver.Args args) {
+ NameResolverProvider provider = getProviderForScheme(targetUri.getScheme());
+ return provider == null ? null : provider.newNameResolver(targetUri, args);
+ }
+
@Override
public String getDefaultScheme() {
return NameResolverRegistry.this.getDefaultScheme();
diff --git a/api/src/main/java/io/grpc/ServerBuilder.java b/api/src/main/java/io/grpc/ServerBuilder.java
index cd1cddbb93f..3effe593e57 100644
--- a/api/src/main/java/io/grpc/ServerBuilder.java
+++ b/api/src/main/java/io/grpc/ServerBuilder.java
@@ -435,6 +435,17 @@ public T setBinaryLog(BinaryLog binaryLog) {
*/
public abstract Server build();
+ /**
+ * Adds a metric sink to the server.
+ *
+ * @param metricSink the metric sink to add.
+ * @return this
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12693")
+ public T addMetricSink(MetricSink metricSink) {
+ return thisT();
+ }
+
/**
* Returns the correctly typed version of the builder.
*/
diff --git a/api/src/main/java/io/grpc/ServerRegistry.java b/api/src/main/java/io/grpc/ServerRegistry.java
index a083e45a000..1ec7030b82b 100644
--- a/api/src/main/java/io/grpc/ServerRegistry.java
+++ b/api/src/main/java/io/grpc/ServerRegistry.java
@@ -18,14 +18,15 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
+import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
-import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
@@ -93,8 +94,9 @@ public static synchronized ServerRegistry getDefaultRegistry() {
if (instance == null) {
List providerList = ServiceProviders.loadAll(
ServerProvider.class,
- getHardCodedClasses(),
- ServerProvider.class.getClassLoader(),
+ ServiceLoader.load(ServerProvider.class, ServerProvider.class.getClassLoader())
+ .iterator(),
+ ServerRegistry::getHardCodedClasses,
new ServerPriorityAccessor());
instance = new ServerRegistry();
for (ServerProvider provider : providerList) {
diff --git a/api/src/main/java/io/grpc/ServiceProviders.java b/api/src/main/java/io/grpc/ServiceProviders.java
index ac4b27d8783..861688be9fb 100644
--- a/api/src/main/java/io/grpc/ServiceProviders.java
+++ b/api/src/main/java/io/grpc/ServiceProviders.java
@@ -17,10 +17,13 @@
package io.grpc;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Supplier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Iterator;
import java.util.List;
+import java.util.ListIterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
@@ -29,42 +32,44 @@ private ServiceProviders() {
// do not instantiate
}
- /**
- * If this is not Android, returns the highest priority implementation of the class via
- * {@link ServiceLoader}.
- * If this is Android, returns an instance of the highest priority class in {@code hardcoded}.
- */
- public static T load(
- Class klass,
- Iterable> hardcoded,
- ClassLoader cl,
- PriorityAccessor priorityAccessor) {
- List candidates = loadAll(klass, hardcoded, cl, priorityAccessor);
- if (candidates.isEmpty()) {
- return null;
- }
- return candidates.get(0);
- }
-
/**
* If this is not Android, returns all available implementations discovered via
* {@link ServiceLoader}.
* If this is Android, returns all available implementations in {@code hardcoded}.
* The list is sorted in descending priority order.
+ *
+ * {@code serviceLoader} should be created with {@code ServiceLoader.load(MyClass.class,
+ * MyClass.class.getClassLoader()).iterator()} in order to be detected by R8 so that R8 full mode
+ * will keep the constructors for the provider classes.
*/
public static List loadAll(
Class klass,
- Iterable> hardcoded,
- ClassLoader cl,
+ Iterator serviceLoader,
+ Supplier>> hardcoded,
final PriorityAccessor priorityAccessor) {
- Iterable candidates;
- if (isAndroid(cl)) {
- candidates = getCandidatesViaHardCoded(klass, hardcoded);
+ Iterator candidates;
+ if (serviceLoader instanceof ListIterator) {
+ // A rewriting tool has replaced the ServiceLoader with a List of some sort (R8 uses
+ // ArrayList, AppReduce uses singletonList). We prefer to use such iterators on Android as
+ // they won't need reflection like the hard-coded list does. In addition, the provider
+ // instances will have already been created, so it seems we should use them.
+ //
+ // R8: https://r8.googlesource.com/r8/+/490bc53d9310d4cc2a5084c05df4aadaec8c885d/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+ // AppReduce: service_loader_pass.cc
+ candidates = serviceLoader;
+ } else if (isAndroid(klass.getClassLoader())) {
+ // Avoid getResource() on Android, which must read from a zip which uses a lot of memory
+ candidates = getCandidatesViaHardCoded(klass, hardcoded.get()).iterator();
+ } else if (!serviceLoader.hasNext()) {
+ // Attempt to load using the context class loader and ServiceLoader.
+ // This allows frameworks like http://aries.apache.org/modules/spi-fly.html to plug in.
+ candidates = ServiceLoader.load(klass).iterator();
} else {
- candidates = getCandidatesViaServiceLoader(klass, cl);
+ candidates = serviceLoader;
}
List list = new ArrayList<>();
- for (T current: candidates) {
+ while (candidates.hasNext()) {
+ T current = candidates.next();
if (!priorityAccessor.isAvailable(current)) {
continue;
}
@@ -101,15 +106,14 @@ static boolean isAndroid(ClassLoader cl) {
}
/**
- * Loads service providers for the {@code klass} service using {@link ServiceLoader}.
+ * For testing only: Loads service providers for the {@code klass} service using {@link
+ * ServiceLoader}. Does not support spi-fly and related tricks.
*/
@VisibleForTesting
public static Iterable getCandidatesViaServiceLoader(Class klass, ClassLoader cl) {
Iterable i = ServiceLoader.load(klass, cl);
- // Attempt to load using the context class loader and ServiceLoader.
- // This allows frameworks like http://aries.apache.org/modules/spi-fly.html to plug in.
if (!i.iterator().hasNext()) {
- i = ServiceLoader.load(klass);
+ return null;
}
return i;
}
diff --git a/api/src/main/java/io/grpc/Status.java b/api/src/main/java/io/grpc/Status.java
index 5d7dd30df01..38cd9581f8e 100644
--- a/api/src/main/java/io/grpc/Status.java
+++ b/api/src/main/java/io/grpc/Status.java
@@ -23,6 +23,7 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
+import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.Metadata.TrustedAsciiMarshaller;
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -30,7 +31,6 @@
import java.util.Collections;
import java.util.List;
import java.util.TreeMap;
-import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
diff --git a/api/src/main/java/io/grpc/StatusException.java b/api/src/main/java/io/grpc/StatusException.java
index b719f881132..c0a67a375b2 100644
--- a/api/src/main/java/io/grpc/StatusException.java
+++ b/api/src/main/java/io/grpc/StatusException.java
@@ -25,7 +25,9 @@
*/
public class StatusException extends Exception {
private static final long serialVersionUID = -660954903976144640L;
+ @SuppressWarnings("serial") // https://github.com/grpc/grpc-java/issues/1913
private final Status status;
+ @SuppressWarnings("serial")
private final Metadata trailers;
/**
@@ -44,12 +46,7 @@ public StatusException(Status status) {
* @since 1.0.0
*/
public StatusException(Status status, @Nullable Metadata trailers) {
- this(status, trailers, /*fillInStackTrace=*/ true);
- }
-
- StatusException(Status status, @Nullable Metadata trailers, boolean fillInStackTrace) {
- super(Status.formatThrowableMessage(status), status.getCause(),
- /* enableSuppression */ true, /* writableStackTrace */fillInStackTrace);
+ super(Status.formatThrowableMessage(status), status.getCause());
this.status = status;
this.trailers = trailers;
}
@@ -68,6 +65,7 @@ public final Status getStatus() {
*
* @since 1.0.0
*/
+ @Nullable
public final Metadata getTrailers() {
return trailers;
}
diff --git a/api/src/main/java/io/grpc/StatusOr.java b/api/src/main/java/io/grpc/StatusOr.java
new file mode 100644
index 00000000000..b7dd68cfd7b
--- /dev/null
+++ b/api/src/main/java/io/grpc/StatusOr.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.base.Objects;
+import javax.annotation.Nullable;
+
+/** Either a Status or a value. */
+@ExperimentalApi("https://github.com/grpc/grpc-java/issues/11563")
+public class StatusOr {
+ private StatusOr(Status status, T value) {
+ this.status = status;
+ this.value = value;
+ }
+
+ /** Construct from a value. */
+ public static StatusOr fromValue(T value) {
+ StatusOr result = new StatusOr(null, value);
+ return result;
+ }
+
+ /** Construct from a non-Ok status. */
+ public static StatusOr fromStatus(Status status) {
+ StatusOr result = new StatusOr(checkNotNull(status, "status"), null);
+ checkArgument(!status.isOk(), "cannot use OK status: %s", status);
+ return result;
+ }
+
+ /** Returns whether there is a value. */
+ public boolean hasValue() {
+ return status == null;
+ }
+
+ /**
+ * Returns the value if set or throws exception if there is no value set. This method is meant
+ * to be called after checking the return value of hasValue() first.
+ */
+ public T getValue() {
+ if (status != null) {
+ throw new IllegalStateException("No value present.");
+ }
+ return value;
+ }
+
+ /** Returns the status. If there is a value (which can be null), returns OK. */
+ public Status getStatus() {
+ return status == null ? Status.OK : status;
+ }
+
+ /**
+ * Note that StatusOr containing statuses, the equality comparision is delegated to
+ * {@link Status#equals} which just does a reference equality check because equality on
+ * Statuses is not well defined.
+ * Instead, do comparison based on their Code with {@link Status#getCode}. The description and
+ * cause of the Status are unlikely to be stable, and additional fields may be added to Status
+ * in the future.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof StatusOr)) {
+ return false;
+ }
+ StatusOr> otherStatus = (StatusOr>) other;
+ if (hasValue() != otherStatus.hasValue()) {
+ return false;
+ }
+ if (hasValue()) {
+ return Objects.equal(value, otherStatus.value);
+ }
+ return Objects.equal(status, otherStatus.status);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(status, value);
+ }
+
+ @Override
+ public String toString() {
+ ToStringHelper stringHelper = MoreObjects.toStringHelper(this);
+ if (status == null) {
+ stringHelper.add("value", value);
+ } else {
+ stringHelper.add("error", status);
+ }
+ return stringHelper.toString();
+ }
+
+ @Nullable
+ private final Status status;
+ private final T value;
+}
diff --git a/api/src/main/java/io/grpc/StatusRuntimeException.java b/api/src/main/java/io/grpc/StatusRuntimeException.java
index 70c4d10f0b2..ebcc2f0d671 100644
--- a/api/src/main/java/io/grpc/StatusRuntimeException.java
+++ b/api/src/main/java/io/grpc/StatusRuntimeException.java
@@ -26,11 +26,13 @@
public class StatusRuntimeException extends RuntimeException {
private static final long serialVersionUID = 1950934672280720624L;
+ @SuppressWarnings("serial") // https://github.com/grpc/grpc-java/issues/1913
private final Status status;
+ @SuppressWarnings("serial")
private final Metadata trailers;
/**
- * Constructs the exception with both a status. See also {@link Status#asRuntimeException()}.
+ * Constructs the exception with a status. See also {@link Status#asRuntimeException()}.
*
* @since 1.0.0
*/
@@ -45,12 +47,7 @@ public StatusRuntimeException(Status status) {
* @since 1.0.0
*/
public StatusRuntimeException(Status status, @Nullable Metadata trailers) {
- this(status, trailers, /*fillInStackTrace=*/ true);
- }
-
- StatusRuntimeException(Status status, @Nullable Metadata trailers, boolean fillInStackTrace) {
- super(Status.formatThrowableMessage(status), status.getCause(),
- /* enable suppressions */ true, /* writableStackTrace */ fillInStackTrace);
+ super(Status.formatThrowableMessage(status), status.getCause());
this.status = status;
this.trailers = trailers;
}
diff --git a/api/src/main/java/io/grpc/SynchronizationContext.java b/api/src/main/java/io/grpc/SynchronizationContext.java
index 5a7677ac15f..94916a1b473 100644
--- a/api/src/main/java/io/grpc/SynchronizationContext.java
+++ b/api/src/main/java/io/grpc/SynchronizationContext.java
@@ -18,8 +18,10 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import static io.grpc.TimeUtils.convertToNanos;
import java.lang.Thread.UncaughtExceptionHandler;
+import java.time.Duration;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
@@ -162,6 +164,12 @@ public String toString() {
return new ScheduledHandle(runnable, future);
}
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657")
+ public final ScheduledHandle schedule(
+ final Runnable task, Duration delay, ScheduledExecutorService timerService) {
+ return schedule(task, convertToNanos(delay), TimeUnit.NANOSECONDS, timerService);
+ }
+
/**
* Schedules a task to be added and run via {@link #execute} after an initial delay and then
* repeated after the delay until cancelled.
@@ -193,6 +201,14 @@ public String toString() {
return new ScheduledHandle(runnable, future);
}
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/11657")
+ public final ScheduledHandle scheduleWithFixedDelay(
+ final Runnable task, Duration initialDelay, Duration delay,
+ ScheduledExecutorService timerService) {
+ return scheduleWithFixedDelay(task, convertToNanos(initialDelay), convertToNanos(delay),
+ TimeUnit.NANOSECONDS, timerService);
+ }
+
private static class ManagedRunnable implements Runnable {
final Runnable task;
@@ -246,4 +262,4 @@ public boolean isPending() {
return !(runnable.hasStarted || runnable.isCancelled);
}
}
-}
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/grpc/TimeUtils.java b/api/src/main/java/io/grpc/TimeUtils.java
new file mode 100644
index 00000000000..01b8c158822
--- /dev/null
+++ b/api/src/main/java/io/grpc/TimeUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 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;
+
+import java.time.Duration;
+
+final class TimeUtils {
+ private TimeUtils() {}
+
+ @IgnoreJRERequirement
+ static long convertToNanos(Duration duration) {
+ try {
+ return duration.toNanos();
+ } catch (ArithmeticException tooBig) {
+ return duration.isNegative() ? Long.MIN_VALUE : Long.MAX_VALUE;
+ }
+ }
+}
diff --git a/api/src/main/java/io/grpc/Uri.java b/api/src/main/java/io/grpc/Uri.java
new file mode 100644
index 00000000000..9f8a5a87848
--- /dev/null
+++ b/api/src/main/java/io/grpc/Uri.java
@@ -0,0 +1,1143 @@
+/*
+ * 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;
+
+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.base.VerifyException;
+import com.google.common.collect.ImmutableList;
+import com.google.common.net.InetAddresses;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.net.InetAddress;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.MalformedInputException;
+import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * A not-quite-general-purpose representation of a Uniform Resource Identifier (URI), as defined by
+ * RFC 3986.
+ *
+ * The URI
+ *
+ * A URI identifies a resource by its name or location or both. The resource could be a file,
+ * service, or some other abstract entity.
+ *
+ *
Examples
+ *
+ *
+ * http://admin@example.com:8080/controlpanel?filter=users#settings
+ * ftp://[2001:db8::7]/docs/report.pdf
+ * file:///My%20Computer/Documents/letter.doc
+ * dns://8.8.8.8/storage.googleapis.com
+ * mailto:John.Doe@example.com
+ * tel:+1-206-555-1212
+ * urn:isbn:978-1492082798
+ *
+ *
+ * Limitations
+ *
+ * This class aims to meet the needs of grpc-java itself and RPC related code that depend on it.
+ * It isn't quite general-purpose. It definitely would not be suitable for building an HTTP user
+ * agent or proxy server. In particular, it:
+ *
+ *
+ * - Can only represent a URI, not a "URI-reference" or "relative reference". In other words, a
+ * "scheme" is always required.
+ *
- Has no knowledge of the particulars of any scheme, with respect to normalization and
+ * comparison. We don't know
https://google.com is the same as
+ * https://google.com:443, that file:/// is the same as
+ * file://localhost, or that joe@example.com is the same as
+ * joe@EXAMPLE.COM. No one class can or should know everything about every scheme so
+ * all this is better handled at a higher layer.
+ * - Implements {@link #equals(Object)} as a char-by-char comparison. Expect false negatives.
+ *
- Does not support "IPvFuture" literal addresses.
+ *
- Does not reflect how web browsers parse user input or the URL Living Standard.
+ *
- Does not support different character encodings. Assumes UTF-8 in several places.
+ *
+ *
+ * Migrating from RFC 2396 and {@link java.net.URI}
+ *
+ * Those migrating from {@link java.net.URI} and/or its primary specification in RFC 2396 should
+ * note some differences.
+ *
+ *
Uniform Hierarchical Syntax
+ *
+ * RFC 3986 unifies the older ideas of "hierarchical" and "opaque" URIs into a single generic
+ * syntax. What RFC 2396 called an opaque "scheme-specific part" is always broken out by RFC 3986
+ * into an authority and path hierarchy, followed by query and fragment components. Accordingly,
+ * this class has only getters for those components but no {@link
+ * java.net.URI#getSchemeSpecificPart()} analog.
+ *
+ *
The RFC 3986 definition of path is now more liberal to accommodate this:
+ *
+ *
+ * - Path doesn't have to start with a slash. For example, the path of
+ * urn:isbn:978-1492082798 is isbn:978-1492082798 even though it doesn't
+ * look much like a file system path.
+ * - The path can now be empty. So Android's
+ * intent:#Intent;action=MAIN;category=LAUNCHER;end is now a valid {@link Uri}. Even
+ * the scheme-only about: is now valid.
+ *
+ *
+ * The uniform syntax always understands what follows a '?' to be a query string. For example,
+ * mailto:me@example.com?subject=foo now has a query component whereas RFC 2396
+ * considered everything after the mailto: scheme to be opaque.
+ *
+ *
Same goes for fragment. data:image/png;...#xywh=0,0,10,10 now has a fragment
+ * whereas RFC 2396 considered everything after the scheme to be opaque.
+ *
+ *
Uniform Authority Syntax
+ *
+ * RFC 2396 tried to guess if an authority was a "server" (host:port) or "registry-based"
+ * (arbitrary string) based on its contents. RFC 3986 expects every authority to look like
+ * [userinfo@]host[:port] and loosens the definition of a "host" to accommodate. Accordingly, this
+ * class has no equivalent to {@link java.net.URI#parseServerAuthority()} -- authority was parsed
+ * into its components and checked for validity when the {@link Uri} was created.
+ *
+ *
Other Specific Differences
+ *
+ * RFC 2396 does not allow underscores in a host name, meaning {@link java.net.URI} switches to
+ * opaque mode when it sees one. {@link Uri} does allow underscores in host, to accommodate
+ * registries other than DNS. So http://my_site.com:8080/index.html now parses as a
+ * host, port and path rather than a single opaque scheme-specific part.
+ *
+ *
{@link Uri} strictly *requires* square brackets in the query string and fragment to be
+ * percent-encoded whereas RFC 2396 merely recommended doing so.
+ *
+ *
Other URx classes are "liberal in what they accept and strict in what they produce." {@link
+ * Uri#parse(String)} and {@link Uri#create(String)}, however, are strict in what they accept and
+ * transparent when asked to reproduce it via {@link Uri#toString()}. The former policy may be
+ * appropriate for parsing user input or web content, but this class is meant for gRPC clients,
+ * servers and plugins like name resolvers where human error at runtime is less likely and best
+ * detected early. {@link java.net.URI#create(String)} is similarly strict, which makes migration
+ * easy, except for the server/registry-based ambiguity addressed by {@link
+ * java.net.URI#parseServerAuthority()}.
+ *
+ *
{@link java.net.URI} and {@link Uri} both support IPv6 literals in square brackets as defined
+ * by RFC 2732.
+ *
+ *
{@link java.net.URI} supports IPv6 scope IDs but accepts and emits a non-standard syntax.
+ * {@link Uri} implements the newer RFC 6874, which percent encodes scope IDs and the % delimiter
+ * itself. RFC 9844 claims to obsolete RFC 6874 because web browsers would not support it. This
+ * class implements RFC 6874 anyway, mostly to avoid creating a barrier to migration away from
+ * {@link java.net.URI}.
+ *
+ *
Some URI components, e.g. scheme, are required while others may or may not be present, e.g.
+ * authority. {@link Uri} is careful to preserve the distinction between an absent string component
+ * (getter returns null) and one with an empty value (getter returns ""). {@link java.net.URI} makes
+ * this distinction too, *except* when it comes to the authority and host components: {@link
+ * java.net.URI#getAuthority()} and {@link java.net.URI#getHost()} return null when an authority is
+ * absent, e.g. file:/path as expected. But these methods surprisingly also return null
+ * when the authority is the empty string, e.g.file:///path. {@link Uri}'s getters
+ * correctly return null and "" in these cases, respectively, as one would expect.
+ */
+@Internal
+public final class Uri {
+ // Components are stored percent-encoded, just as originally parsed for transparent parse/toString
+ // round-tripping.
+ private final String scheme; // != null since we don't support relative references.
+ @Nullable private final String userInfo;
+ @Nullable private final String host;
+ @Nullable private final String port;
+ private final String path; // In RFC 3986, path is always defined (but can be empty).
+ @Nullable private final String query;
+ @Nullable private final String fragment;
+
+ private Uri(Builder builder) {
+ this.scheme = checkNotNull(builder.scheme, "scheme");
+ this.userInfo = builder.userInfo;
+ this.host = builder.host;
+ this.port = builder.port;
+ this.path = builder.path;
+ this.query = builder.query;
+ this.fragment = builder.fragment;
+
+ // Checks common to the parse() and Builder code paths.
+ if (hasAuthority()) {
+ if (!path.isEmpty() && !path.startsWith("/")) {
+ throw new IllegalArgumentException("Has authority -- Non-empty path must start with '/'");
+ }
+ } else {
+ if (path.startsWith("//")) {
+ throw new IllegalArgumentException("No authority -- Path cannot start with '//'");
+ }
+ }
+ }
+
+ /**
+ * Parses a URI from its string form.
+ *
+ * @throws URISyntaxException if 's' is not a valid RFC 3986 URI.
+ */
+ public static Uri parse(String s) throws URISyntaxException {
+ try {
+ return create(s);
+ } catch (IllegalArgumentException e) {
+ throw new URISyntaxException(s, e.getMessage());
+ }
+ }
+
+ /**
+ * Creates a URI from a string assumed to be valid.
+ *
+ *
Useful for defining URI constants in code. Not for user input.
+ *
+ * @throws IllegalArgumentException if 's' is not a valid RFC 3986 URI.
+ */
+ public static Uri create(String s) {
+ Builder builder = new Builder();
+ int i = 0;
+ final int n = s.length();
+
+ // 3.1. Scheme: Look for a ':' before '/', '?', or '#'.
+ int schemeColon = -1;
+ for (; i < n; ++i) {
+ char c = s.charAt(i);
+ if (c == ':') {
+ schemeColon = i;
+ break;
+ } else if (c == '/' || c == '?' || c == '#') {
+ break;
+ }
+ }
+ if (schemeColon < 0) {
+ throw new IllegalArgumentException("Missing required scheme.");
+ }
+ builder.setRawScheme(s.substring(0, schemeColon));
+
+ // 3.2. Authority. Look for '//' then keep scanning until '/', '?', or '#'.
+ i = schemeColon + 1;
+ if (i + 1 < n && s.charAt(i) == '/' && s.charAt(i + 1) == '/') {
+ // "//" just means we have an authority. Skip over it.
+ i += 2;
+
+ int authorityStart = i;
+ for (; i < n; ++i) {
+ char c = s.charAt(i);
+ if (c == '/' || c == '?' || c == '#') {
+ break;
+ }
+ }
+ builder.setRawAuthority(s.substring(authorityStart, i));
+ }
+
+ // 3.3. Path: Whatever is left before '?' or '#'.
+ int pathStart = i;
+ for (; i < n; ++i) {
+ char c = s.charAt(i);
+ if (c == '?' || c == '#') {
+ break;
+ }
+ }
+ builder.setRawPath(s.substring(pathStart, i));
+
+ // 3.4. Query, if we stopped at '?'.
+ if (i < n && s.charAt(i) == '?') {
+ i++; // Skip '?'
+ int queryStart = i;
+ for (; i < n; ++i) {
+ char c = s.charAt(i);
+ if (c == '#') {
+ break;
+ }
+ }
+ builder.setRawQuery(s.substring(queryStart, i));
+ }
+
+ // 3.5. Fragment, if we stopped at '#'.
+ if (i < n && s.charAt(i) == '#') {
+ ++i; // Skip '#'
+ builder.setRawFragment(s.substring(i));
+ }
+
+ return builder.build();
+ }
+
+ private static int findPortStartColon(String authority, int hostStart) {
+ for (int i = authority.length() - 1; i >= hostStart; --i) {
+ char c = authority.charAt(i);
+ if (c == ':') {
+ return i;
+ }
+ if (c == ']') {
+ // Hit the end of IP-literal. Any further colon is inside it and couldn't indicate a port.
+ break;
+ }
+ if (!digitChars.get(c)) {
+ // Found a non-digit, non-colon, non-bracket.
+ // This means there is no valid port (e.g. host is "example.com")
+ break;
+ }
+ }
+ return -1;
+ }
+
+ // Checks a raw path for validity and parses it into segments. Let 'out' be null to just validate.
+ private static void parseAssumedUtf8PathIntoSegments(
+ String path, ImmutableList.Builder out) {
+ // Skip the first slash so it doesn't count as an empty segment at the start.
+ // (e.g., "/a" -> ["a"], not ["", "a"])
+ int start = path.startsWith("/") ? 1 : 0;
+
+ for (int i = start; i < path.length(); ) {
+ int nextSlash = path.indexOf('/', i);
+ String segment;
+ if (nextSlash >= 0) {
+ // Typical segment case (e.g., "foo" in "/foo/bar").
+ segment = path.substring(i, nextSlash);
+ i = nextSlash + 1;
+ } else {
+ // Final segment case (e.g., "bar" in "/foo/bar").
+ segment = path.substring(i);
+ i = path.length();
+ }
+ if (out != null) {
+ out.add(percentDecodeAssumedUtf8(segment));
+ } else {
+ checkPercentEncodedArg(segment, "path segment", pChars);
+ }
+ }
+
+ // RFC 3986 says a trailing slash creates a final empty segment.
+ // (e.g., "/foo/" -> ["foo", ""])
+ if (path.endsWith("/") && out != null) {
+ out.add("");
+ }
+ }
+
+ /** Returns the scheme of this URI. */
+ public String getScheme() {
+ return scheme;
+ }
+
+ /**
+ * Returns the percent-decoded "Authority" component of this URI, or null if not present.
+ *
+ * NB: This method's decoding is lossy -- It only exists for compatibility with {@link
+ * java.net.URI}. Prefer {@link #getRawAuthority()} or work instead with authority in terms of its
+ * individual components ({@link #getUserInfo()}, {@link #getHost()} and {@link #getPort()}). The
+ * problem with getAuthority() is that it returns the delimited concatenation of the percent-
+ * decoded userinfo, host and port components. But both userinfo and host can contain the '@'
+ * character, which becomes indistinguishable from the userinfo/host delimiter after decoding. For
+ * example, URIs scheme://x@y%40z and scheme://x%40y@z have different
+ * userinfo and host components but getAuthority() returns "x@y@z" for both of them.
+ *
+ *
NB: This method assumes the "host" component was encoded as UTF-8, as mandated by RFC 3986.
+ * This method also assumes the "user information" part of authority was encoded as UTF-8,
+ * although RFC 3986 doesn't specify an encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawAuthority()}, {@link #percentDecode(CharSequence)}, then decode the bytes for
+ * themselves.
+ */
+ @Nullable
+ public String getAuthority() {
+ return percentDecodeAssumedUtf8(getRawAuthority());
+ }
+
+ private boolean hasAuthority() {
+ return host != null;
+ }
+
+ /**
+ * Returns the "authority" component of this URI in its originally parsed, possibly
+ * percent-encoded form.
+ */
+ @Nullable
+ public String getRawAuthority() {
+ if (hasAuthority()) {
+ StringBuilder sb = new StringBuilder();
+ appendAuthority(sb);
+ return sb.toString();
+ }
+ return null;
+ }
+
+ private void appendAuthority(StringBuilder sb) {
+ if (userInfo != null) {
+ sb.append(userInfo).append('@');
+ }
+ if (host != null) {
+ sb.append(host);
+ }
+ if (port != null) {
+ sb.append(':').append(port);
+ }
+ }
+
+ /**
+ * Returns the percent-decoded "User Information" component of this URI, or null if not present.
+ *
+ *
NB: This method *assumes* this component was encoded as UTF-8, although RFC 3986 doesn't
+ * specify an encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawUserInfo()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ */
+ @Nullable
+ public String getUserInfo() {
+ return percentDecodeAssumedUtf8(userInfo);
+ }
+
+ /**
+ * Returns the "User Information" component of this URI in its originally parsed, possibly
+ * percent-encoded form.
+ */
+ @Nullable
+ public String getRawUserInfo() {
+ return userInfo;
+ }
+
+ /**
+ * Returns the percent-decoded "host" component of this URI, or null if not present.
+ *
+ *
This method assumes the host was encoded as UTF-8, as mandated by RFC 3986.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawHost()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ */
+ @Nullable
+ public String getHost() {
+ return percentDecodeAssumedUtf8(host);
+ }
+
+ /**
+ * Returns the host component of this URI in its originally parsed, possibly percent-encoded form.
+ */
+ @Nullable
+ public String getRawHost() {
+ return host;
+ }
+
+ /** Returns the "port" component of this URI, or -1 if empty or not present. */
+ public int getPort() {
+ return port != null && !port.isEmpty() ? Integer.parseInt(port) : -1;
+ }
+
+ /** Returns the raw port component of this URI in its originally parsed form. */
+ @Nullable
+ public String getRawPort() {
+ return port;
+ }
+
+ /**
+ * Returns the (possibly empty) percent-decoded "path" component of this URI.
+ *
+ *
NB: This method *assumes* the path was encoded as UTF-8, although RFC 3986 doesn't specify
+ * an encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawPath()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ *
+ *
NB: Prefer {@link #getPathSegments()} because this method's decoding is lossy. For example,
+ * consider these (different) URIs:
+ *
+ *
+ * - file:///home%2Ffolder/my%20file
+ *
- file:///home/folder/my%20file
+ *
+ *
+ * Calling getPath() on each returns the same string: /home/folder/my file. You
+ * can't tell whether the second '/' character is part of the first path segment or separates the
+ * first and second path segments. This method only exists to ease migration from {@link
+ * java.net.URI}.
+ */
+ public String getPath() {
+ return percentDecodeAssumedUtf8(path);
+ }
+
+ /**
+ * Returns this URI's path as a list of path segments not including the '/' segment delimiters.
+ *
+ *
Prefer this method over {@link #getPath()} because it preserves the distinction between
+ * segment separators and literal '/'s within a path segment.
+ *
+ *
A trailing '/' delimiter in the path results in the empty string as the last element in the
+ * returned list. For example, file://localhost/foo/bar/ has path segments
+ * ["foo", "bar", ""]
+ *
+ *
A leading '/' delimiter cannot be detected using this method. For example, both
+ * dns:example.com and dns:///example.com have the same list of path segments:
+ * ["example.com"]. Use {@link #isPathAbsolute()} or {@link #isPathRootless()} to
+ * distinguish these cases.
+ *
+ *
The returned list is immutable.
+ */
+ public List getPathSegments() {
+ // Returned list must be immutable but we intentionally keep guava out of the public API.
+ ImmutableList.Builder segmentsBuilder = ImmutableList.builder();
+ parseAssumedUtf8PathIntoSegments(path, segmentsBuilder);
+ return segmentsBuilder.build();
+ }
+
+ /**
+ * Returns true iff this URI's path component starts with a path segment (rather than the '/'
+ * segment delimiter).
+ *
+ * The path of an RFC 3986 URI is either empty, absolute (starts with the '/' segment
+ * delimiter) or rootless (starts with a path segment). For example, tel:+1-206-555-1212
+ * , mailto:me@example.com and urn:isbn:978-1492082798 all have
+ * rootless paths. mailto:%2Fdev%2Fnull@example.com is also rootless because its
+ * percent-encoded slashes are not segment delimiters but rather part of the first and only path
+ * segment.
+ *
+ *
Contrast rootless paths with absolute ones (see {@link #isPathAbsolute()}.
+ */
+ public boolean isPathRootless() {
+ return !path.isEmpty() && !path.startsWith("/");
+ }
+
+ /**
+ * Returns true iff this URI's path component starts with the '/' segment delimiter (rather than a
+ * path segment).
+ *
+ *
The path of an RFC 3986 URI is either empty, absolute (starts with the '/' segment
+ * delimiter) or rootless (starts with a path segment). For example, file:///resume.txt
+ * , file:/resume.txt and file://localhost/ all have absolute
+ * paths while tel:+1-206-555-1212's path is not absolute.
+ * mailto:%2Fdev%2Fnull@example.com is also not absolute because its percent-encoded
+ * slashes are not segment delimiters but rather part of the first and only path segment.
+ *
+ *
Contrast absolute paths with rootless ones (see {@link #isPathRootless()}.
+ *
+ *
NB: The term "absolute" has two different meanings in RFC 3986 which are easily confused.
+ * This method tests for a property of this URI's path component. Contrast with {@link
+ * #isAbsolute()} which tests the URI itself for a different property.
+ */
+ public boolean isPathAbsolute() {
+ return path.startsWith("/");
+ }
+
+ /**
+ * Returns the path component of this URI in its originally parsed, possibly percent-encoded form.
+ */
+ public String getRawPath() {
+ return path;
+ }
+
+ /**
+ * Returns the percent-decoded "query" component of this URI, or null if not present.
+ *
+ *
NB: This method assumes the query was encoded as UTF-8, although RFC 3986 doesn't specify an
+ * encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawQuery()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ */
+ @Nullable
+ public String getQuery() {
+ return percentDecodeAssumedUtf8(query);
+ }
+
+ /**
+ * Returns the query component of this URI in its originally parsed, possibly percent-encoded
+ * form, without any leading '?' character.
+ */
+ @Nullable
+ public String getRawQuery() {
+ return query;
+ }
+
+ /**
+ * Returns the percent-decoded "fragment" component of this URI, or null if not present.
+ *
+ *
NB: This method assumes the fragment was encoded as UTF-8, although RFC 3986 doesn't specify
+ * an encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawFragment()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ */
+ @Nullable
+ public String getFragment() {
+ return percentDecodeAssumedUtf8(fragment);
+ }
+
+ /**
+ * Returns the fragment component of this URI in its original, possibly percent-encoded form, and
+ * without any leading '#' character.
+ */
+ @Nullable
+ public String getRawFragment() {
+ return fragment;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
If this URI was created by {@link #parse(String)} or {@link #create(String)}, then the
+ * returned string will match that original input exactly.
+ */
+ @Override
+ public String toString() {
+ // https://datatracker.ietf.org/doc/html/rfc3986#section-5.3
+ StringBuilder sb = new StringBuilder();
+ sb.append(scheme).append(':');
+ if (hasAuthority()) {
+ sb.append("//");
+ appendAuthority(sb);
+ }
+ sb.append(path);
+ if (query != null) {
+ sb.append('?').append(query);
+ }
+ if (fragment != null) {
+ sb.append('#').append(fragment);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns true iff this URI has a scheme and an authority/path hierarchy, but no fragment.
+ *
+ *
All instances of {@link Uri} are RFC 3986 URIs, not "relative references", so this method is
+ * equivalent to {@code getFragment() == null}. It mostly exists for compatibility with {@link
+ * java.net.URI}.
+ */
+ public boolean isAbsolute() {
+ return scheme != null && fragment == null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
Two instances of {@link Uri} are equal if and only if they have the same string
+ * representation, which RFC 3986 calls "Simple String Comparison" (6.2.1). Callers with a higher
+ * layer expectation of equality (e.g. http://some%2Dhost:80/foo/./bar.txt ~=
+ * http://some-host/foo/bar.txt) will experience false negatives.
+ */
+ @Override
+ public boolean equals(Object otherObj) {
+ if (!(otherObj instanceof Uri)) {
+ return false;
+ }
+ Uri other = (Uri) otherObj;
+ return Objects.equals(scheme, other.scheme)
+ && Objects.equals(userInfo, other.userInfo)
+ && Objects.equals(host, other.host)
+ && Objects.equals(port, other.port)
+ && Objects.equals(path, other.path)
+ && Objects.equals(query, other.query)
+ && Objects.equals(fragment, other.fragment);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(scheme, userInfo, host, port, path, query, fragment);
+ }
+
+ /** Returns a new Builder initialized with the fields of this URI. */
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /** Creates a new {@link Builder} with all fields uninitialized or set to their default values. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Builder for {@link Uri}. */
+ public static final class Builder {
+ private String scheme;
+ private String path = "";
+ private String query;
+ private String fragment;
+ private String userInfo;
+ private String host;
+ private String port;
+
+ private Builder() {}
+
+ Builder(Uri prototype) {
+ this.scheme = prototype.scheme;
+ this.userInfo = prototype.userInfo;
+ this.host = prototype.host;
+ this.port = prototype.port;
+ this.path = prototype.path;
+ this.query = prototype.query;
+ this.fragment = prototype.fragment;
+ }
+
+ /**
+ * Sets the scheme, e.g. "https", "dns" or "xds".
+ *
+ *
This field is required.
+ *
+ * @return this, for fluent building
+ * @throws IllegalArgumentException if the scheme is invalid.
+ */
+ @CanIgnoreReturnValue
+ public Builder setScheme(String scheme) {
+ return setRawScheme(scheme.toLowerCase(Locale.ROOT));
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawScheme(String scheme) {
+ if (scheme.isEmpty() || !alphaChars.get(scheme.charAt(0))) {
+ throw new IllegalArgumentException("Scheme must start with an alphabetic char");
+ }
+ for (int i = 0; i < scheme.length(); i++) {
+ char c = scheme.charAt(i);
+ if (!schemeChars.get(c)) {
+ throw new IllegalArgumentException("Invalid character in scheme at index " + i);
+ }
+ }
+ this.scheme = scheme;
+ return this;
+ }
+
+ /**
+ * Specifies the new URI's path component as a string of zero or more '/' delimited segments.
+ *
+ *
Path segments can consist of any string of codepoints. Codepoints that can't be encoded
+ * literally will be percent-encoded for you.
+ *
+ *
If a URI contains an authority component, then the path component must either be empty or
+ * begin with a slash ("/") character. If a URI does not contain an authority component, then
+ * the path cannot begin with two slash characters ("//").
+ *
+ *
This method interprets all '/' characters in 'path' as segment delimiters. If any of your
+ * segments contain literal '/' characters, call {@link #setRawPath(String)} instead.
+ *
+ *
See RFC 3986 3.3
+ * for more.
+ *
+ *
This field is required but can be empty (its default value).
+ *
+ * @param path the new path
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setPath(String path) {
+ checkArgument(path != null, "Path can be empty but not null");
+ this.path = percentEncode(path, pCharsAndSlash);
+ return this;
+ }
+
+ /**
+ * Specifies the new URI's path component as a string of zero or more '/' delimited segments.
+ *
+ *
Path segments can consist of any string of codepoints but the caller must first percent-
+ * encode anything other than RFC 3986's "pchar" character class using UTF-8.
+ *
+ *
If a URI contains an authority component, then the path component must either be empty or
+ * begin with a slash ("/") character. If a URI does not contain an authority component, then
+ * the path cannot begin with two slash characters ("//").
+ *
+ *
This method interprets all '/' characters in 'path' as segment delimiters. If any of your
+ * segments contain literal '/' characters, you must percent-encode them.
+ *
+ *
See RFC 3986 3.3
+ * for more.
+ *
+ *
This field is required but can be empty (its default value).
+ *
+ * @param path the new path, a string consisting of characters from "pchar"
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setRawPath(String path) {
+ checkArgument(path != null, "Path can be empty but not null");
+ parseAssumedUtf8PathIntoSegments(path, null);
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * Specifies the query component of the new URI (not including the leading '?').
+ *
+ *
Query can contain any string of codepoints. Codepoints that can't be encoded literally
+ * will be percent-encoded for you as UTF-8.
+ *
+ *
This field is optional.
+ *
+ * @param query the new query component, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setQuery(@Nullable String query) {
+ this.query = percentEncode(query, queryChars);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawQuery(String query) {
+ checkPercentEncodedArg(query, "query", queryChars);
+ this.query = query;
+ return this;
+ }
+
+ /**
+ * Specifies the fragment component of the new URI (not including the leading '#').
+ *
+ *
The fragment can contain any string of codepoints. Codepoints that can't be encoded
+ * literally will be percent-encoded for you as UTF-8.
+ *
+ *
This field is optional.
+ *
+ * @param fragment the new fragment component, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setFragment(@Nullable String fragment) {
+ this.fragment = percentEncode(fragment, fragmentChars);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawFragment(String fragment) {
+ checkPercentEncodedArg(fragment, "fragment", fragmentChars);
+ this.fragment = fragment;
+ return this;
+ }
+
+ /**
+ * Set the "user info" component of the new URI, e.g. "username:password", not including the
+ * trailing '@' character.
+ *
+ *
User info can contain any string of codepoints. Codepoints that can't be encoded literally
+ * will be percent-encoded for you as UTF-8.
+ *
+ *
This field is optional.
+ *
+ * @param userInfo the new "user info" component, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setUserInfo(@Nullable String userInfo) {
+ this.userInfo = percentEncode(userInfo, userInfoChars);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawUserInfo(String userInfo) {
+ checkPercentEncodedArg(userInfo, "userInfo", userInfoChars);
+ this.userInfo = userInfo;
+ return this;
+ }
+
+ /**
+ * Specifies the "host" component of the new URI in its "registered name" form (usually DNS),
+ * e.g. "server.com".
+ *
+ *
The registered name can contain any string of codepoints. Codepoints that can't be encoded
+ * literally will be percent-encoded for you as UTF-8.
+ *
+ *
This field is optional.
+ *
+ * @param regName the new host component in "registered name" form, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setHost(@Nullable String regName) {
+ if (regName != null) {
+ regName = regName.toLowerCase(Locale.ROOT);
+ regName = percentEncode(regName, regNameChars);
+ }
+ this.host = regName;
+ return this;
+ }
+
+ /**
+ * Specifies the "host" component of the new URI as an IP address.
+ *
+ *
This field is optional.
+ *
+ * @param addr the new "host" component in InetAddress form, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setHost(@Nullable InetAddress addr) {
+ this.host = addr != null ? toUriString(addr) : null;
+ return this;
+ }
+
+ private static String toUriString(InetAddress addr) {
+ // InetAddresses.toUriString(addr) is almost enough but neglects RFC 6874 percent encoding.
+ String inetAddrStr = InetAddresses.toUriString(addr);
+ int percentIndex = inetAddrStr.indexOf('%');
+ if (percentIndex < 0) {
+ return inetAddrStr;
+ }
+
+ String scope = inetAddrStr.substring(percentIndex, inetAddrStr.length() - 1);
+ return inetAddrStr.substring(0, percentIndex) + percentEncode(scope, unreservedChars) + "]";
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawHost(String host) {
+ if (host.startsWith("[") && host.endsWith("]")) {
+ // IP-literal: Guava's isUriInetAddress() is almost enough but it doesn't check the scope.
+ int percentIndex = host.indexOf('%');
+ if (percentIndex > 0) {
+ String scope = host.substring(percentIndex, host.length() - 1);
+ checkPercentEncodedArg(scope, "scope", unreservedChars);
+ }
+ }
+ // IP-literal validation is complicated so we delegate it to Guava. We use this particular
+ // method of InetAddresses because it doesn't try to match interfaces on the local machine.
+ // (The validity of a URI should be the same no matter which machine does the parsing.)
+ // TODO(jdcormie): IPFuture
+ if (!InetAddresses.isUriInetAddress(host)) {
+ // Must be a "registered name".
+ checkPercentEncodedArg(host, "host", regNameChars);
+ }
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * Specifies the "port" component of the new URI, e.g. "8080".
+ *
+ *
The port can be any non-negative integer. A negative value represents "no port".
+ *
+ *
This field is optional.
+ *
+ * @param port the new "port" component, or -1 to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setPort(int port) {
+ this.port = port < 0 ? null : Integer.toString(port);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawPort(String port) {
+ if (port != null && !port.isEmpty()) {
+ try {
+ Integer.parseInt(port); // Result unused.
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid port", e);
+ }
+ }
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * Specifies the userinfo, host and port URI components all at once using a single string.
+ *
+ *
This setter is "raw" in the sense that special characters in userinfo and host must be
+ * passed in percent-encoded. See RFC 3986 3.2 for the set
+ * of characters allowed in each component of an authority.
+ *
+ *
There's no "cooked" method to set authority like for other URI components because
+ * authority is a *compound* URI component whose userinfo, host and port components are
+ * delimited with special characters '@' and ':'. But the first two of those components can
+ * themselves contain these delimiters so we need percent-encoding to parse them unambiguously.
+ *
+ * @param authority an RFC 3986 authority string that will be used to set userinfo, host and
+ * port, or null to clear all three of those components
+ */
+ @CanIgnoreReturnValue
+ public Builder setRawAuthority(@Nullable String authority) {
+ if (authority == null) {
+ setUserInfo(null);
+ setHost((String) null);
+ setPort(-1);
+ } else {
+ // UserInfo. Easy because '@' cannot appear unencoded inside userinfo or host.
+ int userInfoEnd = authority.indexOf('@');
+ if (userInfoEnd >= 0) {
+ setRawUserInfo(authority.substring(0, userInfoEnd));
+ } else {
+ setUserInfo(null);
+ }
+
+ // Host/Port.
+ int hostStart = userInfoEnd >= 0 ? userInfoEnd + 1 : 0;
+ int portStartColon = findPortStartColon(authority, hostStart);
+ if (portStartColon < 0) {
+ setRawHost(authority.substring(hostStart));
+ setPort(-1);
+ } else {
+ setRawHost(authority.substring(hostStart, portStartColon));
+ setRawPort(authority.substring(portStartColon + 1));
+ }
+ }
+ return this;
+ }
+
+ /** Builds a new instance of {@link Uri} as specified by the setters. */
+ public Uri build() {
+ checkState(scheme != null, "Missing required scheme.");
+ if (host == null) {
+ checkState(port == null, "Cannot set port without host.");
+ checkState(userInfo == null, "Cannot set userInfo without host.");
+ }
+ return new Uri(this);
+ }
+ }
+
+ /**
+ * Decodes a string of characters in the range [U+0000, U+007F] to bytes.
+ *
+ *
Each percent-encoded sequence (e.g. "%F0" or "%2a", as defined by RFC 3986 2.1) is decoded
+ * to the octet it encodes. Other characters are decoded to their code point's single byte value.
+ * A literal % character must be encoded as %25.
+ *
+ * @throws IllegalArgumentException if 's' contains characters out of range or invalid percent
+ * encoding sequences.
+ */
+ public static ByteBuffer percentDecode(CharSequence s) {
+ // This is large enough because each input character needs *at most* one byte of output.
+ ByteBuffer outBuf = ByteBuffer.allocate(s.length());
+ percentDecode(s, "input", null, outBuf);
+ outBuf.flip();
+ return outBuf;
+ }
+
+ private static void percentDecode(
+ CharSequence s, String what, BitSet allowedChars, ByteBuffer outBuf) {
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == '%') {
+ if (i + 2 >= s.length()) {
+ throw new IllegalArgumentException(
+ "Invalid percent-encoding at index " + i + " of " + what + ": " + s);
+ }
+ int h1 = Character.digit(s.charAt(i + 1), 16);
+ int h2 = Character.digit(s.charAt(i + 2), 16);
+ if (h1 == -1 || h2 == -1) {
+ throw new IllegalArgumentException(
+ "Invalid hex digit in " + what + " at index " + i + " of: " + s);
+ }
+ if (outBuf != null) {
+ outBuf.put((byte) (h1 << 4 | h2));
+ }
+ i += 2;
+ } else if (allowedChars == null || allowedChars.get(c)) {
+ if (outBuf != null) {
+ outBuf.put((byte) c);
+ }
+ } else {
+ throw new IllegalArgumentException("Invalid character in " + what + " at index " + i);
+ }
+ }
+ }
+
+ @Nullable
+ private static String percentDecodeAssumedUtf8(@Nullable String s) {
+ if (s == null || s.indexOf('%') == -1) {
+ return s;
+ }
+
+ ByteBuffer utf8Bytes = percentDecode(s);
+ try {
+ return StandardCharsets.UTF_8
+ .newDecoder()
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE)
+ .decode(utf8Bytes)
+ .toString();
+ } catch (CharacterCodingException e) {
+ throw new VerifyException(e); // Should not happen in REPLACE mode.
+ }
+ }
+
+ @Nullable
+ private static String percentEncode(String s, BitSet allowedCodePoints) {
+ if (s == null) {
+ return null;
+ }
+ CharsetEncoder encoder =
+ StandardCharsets.UTF_8
+ .newEncoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+ ByteBuffer utf8Bytes;
+ try {
+ utf8Bytes = encoder.encode(CharBuffer.wrap(s));
+ } catch (MalformedInputException e) {
+ throw new IllegalArgumentException("Malformed input", e); // Must be a broken surrogate pair.
+ } catch (CharacterCodingException e) {
+ throw new VerifyException(e); // Should not happen when encoding to UTF-8.
+ }
+
+ StringBuilder sb = new StringBuilder();
+ while (utf8Bytes.hasRemaining()) {
+ int b = 0xff & utf8Bytes.get();
+ if (allowedCodePoints.get(b)) {
+ sb.append((char) b);
+ } else {
+ sb.append('%');
+ sb.append(hexDigitsByVal[(b & 0xF0) >> 4]);
+ sb.append(hexDigitsByVal[b & 0x0F]);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static void checkPercentEncodedArg(String s, String what, BitSet allowedChars) {
+ percentDecode(s, what, allowedChars, null);
+ }
+
+ // See UriTest for how these were computed from the ABNF constants in RFC 3986.
+ static final BitSet digitChars = BitSet.valueOf(new long[] {0x3ff000000000000L});
+ static final BitSet alphaChars = BitSet.valueOf(new long[] {0L, 0x7fffffe07fffffeL});
+ // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+ static final BitSet schemeChars =
+ BitSet.valueOf(new long[] {0x3ff680000000000L, 0x7fffffe07fffffeL});
+ // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ static final BitSet unreservedChars =
+ BitSet.valueOf(new long[] {0x3ff600000000000L, 0x47fffffe87fffffeL});
+ // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+ static final BitSet genDelimsChars =
+ BitSet.valueOf(new long[] {0x8400800800000000L, 0x28000001L});
+ // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+ static final BitSet subDelimsChars = BitSet.valueOf(new long[] {0x28001fd200000000L});
+ // reserved = gen-delims / sub-delims
+ static final BitSet reservedChars = BitSet.valueOf(new long[] {0xac009fda00000000L, 0x28000001L});
+ // reg-name = *( unreserved / pct-encoded / sub-delims )
+ static final BitSet regNameChars =
+ BitSet.valueOf(new long[] {0x2bff7fd200000000L, 0x47fffffe87fffffeL});
+ // userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
+ static final BitSet userInfoChars =
+ BitSet.valueOf(new long[] {0x2fff7fd200000000L, 0x47fffffe87fffffeL});
+ // pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+ static final BitSet pChars =
+ BitSet.valueOf(new long[] {0x2fff7fd200000000L, 0x47fffffe87ffffffL});
+ static final BitSet pCharsAndSlash =
+ BitSet.valueOf(new long[] {0x2fffffd200000000L, 0x47fffffe87ffffffL});
+ // query = *( pchar / "/" / "?" )
+ static final BitSet queryChars =
+ BitSet.valueOf(new long[] {0xafffffd200000000L, 0x47fffffe87ffffffL});
+ // fragment = *( pchar / "/" / "?" )
+ static final BitSet fragmentChars = queryChars;
+
+ private static final char[] hexDigitsByVal = "0123456789ABCDEF".toCharArray();
+}
diff --git a/api/src/test/java/io/grpc/CallOptionsTest.java b/api/src/test/java/io/grpc/CallOptionsTest.java
index cc90a9799d7..65fb7ff3bf2 100644
--- a/api/src/test/java/io/grpc/CallOptionsTest.java
+++ b/api/src/test/java/io/grpc/CallOptionsTest.java
@@ -32,6 +32,7 @@
import com.google.common.base.Objects;
import io.grpc.ClientStreamTracer.StreamInfo;
import io.grpc.internal.SerializingExecutor;
+import java.time.Duration;
import java.util.concurrent.Executor;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -150,6 +151,15 @@ public void withDeadlineAfter() {
assertAbout(deadline()).that(actual).isWithin(10, MILLISECONDS).of(expected);
}
+ @Test
+ @IgnoreJRERequirement
+ public void withDeadlineAfterDuration() {
+ Deadline actual = CallOptions.DEFAULT.withDeadlineAfter(Duration.ofMinutes(1L)).getDeadline();
+ Deadline expected = Deadline.after(1, MINUTES);
+
+ assertAbout(deadline()).that(actual).isWithin(10, MILLISECONDS).of(expected);
+ }
+
@Test
public void toStringMatches_noDeadline_default() {
String actual = allSet
diff --git a/api/src/test/java/io/grpc/ConfiguratorRegistryTest.java b/api/src/test/java/io/grpc/ConfiguratorRegistryTest.java
index e231d13503a..457d5a36e77 100644
--- a/api/src/test/java/io/grpc/ConfiguratorRegistryTest.java
+++ b/api/src/test/java/io/grpc/ConfiguratorRegistryTest.java
@@ -85,14 +85,12 @@ public static final class StaticTestingClassLoaderGetBeforeSet implements Runnab
@Override
public void run() {
assertThat(ConfiguratorRegistry.getDefaultRegistry().getConfigurators()).isEmpty();
-
- try {
- ConfiguratorRegistry.getDefaultRegistry()
- .setConfigurators(Arrays.asList(new NoopConfigurator()));
- fail("should have failed for invoking set call after get is already called");
- } catch (IllegalStateException e) {
- assertThat(e).hasMessageThat().isEqualTo("Configurators are already set");
- }
+ NoopConfigurator noopConfigurator = new NoopConfigurator();
+ ConfiguratorRegistry.getDefaultRegistry()
+ .setConfigurators(Arrays.asList(noopConfigurator));
+ assertThat(ConfiguratorRegistry.getDefaultRegistry().getConfigurators())
+ .containsExactly(noopConfigurator);
+ assertThat(InternalConfiguratorRegistry.getConfiguratorsCallCountBeforeSet()).isEqualTo(1);
}
}
diff --git a/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java b/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java
new file mode 100644
index 00000000000..6620a7d413a
--- /dev/null
+++ b/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.testing.EqualsTester;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class HttpConnectProxiedSocketAddressTest {
+
+ private final InetSocketAddress proxyAddress =
+ new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);
+ private final InetSocketAddress targetAddress =
+ InetSocketAddress.createUnresolved("example.com", 443);
+
+ @Test
+ public void buildWithAllFields() {
+ Map headers = new HashMap<>();
+ headers.put("X-Custom-Header", "custom-value");
+ headers.put("Proxy-Authorization", "Bearer token");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .setUsername("user")
+ .setPassword("pass")
+ .build();
+
+ assertThat(address.getProxyAddress()).isEqualTo(proxyAddress);
+ assertThat(address.getTargetAddress()).isEqualTo(targetAddress);
+ assertThat(address.getHeaders()).hasSize(2);
+ assertThat(address.getHeaders()).containsEntry("X-Custom-Header", "custom-value");
+ assertThat(address.getHeaders()).containsEntry("Proxy-Authorization", "Bearer token");
+ assertThat(address.getUsername()).isEqualTo("user");
+ assertThat(address.getPassword()).isEqualTo("pass");
+ }
+
+ @Test
+ public void buildWithoutOptionalFields() {
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .build();
+
+ assertThat(address.getProxyAddress()).isEqualTo(proxyAddress);
+ assertThat(address.getTargetAddress()).isEqualTo(targetAddress);
+ assertThat(address.getHeaders()).isEmpty();
+ assertThat(address.getUsername()).isNull();
+ assertThat(address.getPassword()).isNull();
+ }
+
+ @Test
+ public void buildWithEmptyHeaders() {
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(Collections.emptyMap())
+ .build();
+
+ assertThat(address.getHeaders()).isEmpty();
+ }
+
+ @Test
+ public void headersAreImmutable() {
+ Map headers = new HashMap<>();
+ headers.put("key1", "value1");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .build();
+
+ headers.put("key2", "value2");
+
+ assertThat(address.getHeaders()).hasSize(1);
+ assertThat(address.getHeaders()).containsEntry("key1", "value1");
+ assertThat(address.getHeaders()).doesNotContainKey("key2");
+ }
+
+ @Test
+ public void returnedHeadersAreUnmodifiable() {
+ Map headers = new HashMap<>();
+ headers.put("key", "value");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .build();
+
+ assertThrows(UnsupportedOperationException.class,
+ () -> address.getHeaders().put("newKey", "newValue"));
+ }
+
+ @Test
+ public void nullHeadersThrowsException() {
+ assertThrows(NullPointerException.class,
+ () -> HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(null)
+ .build());
+ }
+
+ @Test
+ public void equalsAndHashCode() {
+ Map headers1 = new HashMap<>();
+ headers1.put("header", "value");
+
+ Map headers2 = new HashMap<>();
+ headers2.put("header", "value");
+
+ Map differentHeaders = new HashMap<>();
+ differentHeaders.put("different", "header");
+
+ new EqualsTester()
+ .addEqualityGroup(
+ HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers1)
+ .setUsername("user")
+ .setPassword("pass")
+ .build(),
+ HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers2)
+ .setUsername("user")
+ .setPassword("pass")
+ .build())
+ .addEqualityGroup(
+ HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(differentHeaders)
+ .setUsername("user")
+ .setPassword("pass")
+ .build())
+ .addEqualityGroup(
+ HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .build())
+ .testEquals();
+ }
+
+ @Test
+ public void toStringContainsHeaders() {
+ Map headers = new HashMap<>();
+ headers.put("X-Test", "test-value");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .setUsername("user")
+ .setPassword("secret")
+ .build();
+
+ String toString = address.toString();
+ assertThat(toString).contains("headers");
+ assertThat(toString).contains("X-Test");
+ assertThat(toString).contains("hasPassword=true");
+ assertThat(toString).doesNotContain("secret");
+ }
+
+ @Test
+ public void toStringWithoutPassword() {
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .build();
+
+ String toString = address.toString();
+ assertThat(toString).contains("hasPassword=false");
+ }
+
+ @Test
+ public void hashCodeDependsOnHeaders() {
+ Map headers1 = new HashMap<>();
+ headers1.put("header", "value1");
+
+ Map headers2 = new HashMap<>();
+ headers2.put("header", "value2");
+
+ HttpConnectProxiedSocketAddress address1 = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers1)
+ .build();
+
+ HttpConnectProxiedSocketAddress address2 = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers2)
+ .build();
+
+ assertNotEquals(address1.hashCode(), address2.hashCode());
+ }
+
+ @Test
+ public void multipleHeadersSupported() {
+ Map headers = new HashMap<>();
+ headers.put("X-Header-1", "value1");
+ headers.put("X-Header-2", "value2");
+ headers.put("X-Header-3", "value3");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .build();
+
+ assertThat(address.getHeaders()).hasSize(3);
+ assertThat(address.getHeaders()).containsEntry("X-Header-1", "value1");
+ assertThat(address.getHeaders()).containsEntry("X-Header-2", "value2");
+ assertThat(address.getHeaders()).containsEntry("X-Header-3", "value3");
+ }
+}
+
diff --git a/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java b/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java
index 5b348b7adab..690db6622e0 100644
--- a/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java
+++ b/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java
@@ -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);
@@ -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
diff --git a/api/src/test/java/io/grpc/LoadBalancerTest.java b/api/src/test/java/io/grpc/LoadBalancerTest.java
index 5e9e5cbe816..22fdc220081 100644
--- a/api/src/test/java/io/grpc/LoadBalancerTest.java
+++ b/api/src/test/java/io/grpc/LoadBalancerTest.java
@@ -64,6 +64,26 @@ public void pickResult_withSubchannelAndTracer() {
assertThat(result.isDrop()).isFalse();
}
+ @Test
+ public void pickResult_withSubchannelReplacement() {
+ PickResult result = PickResult.withSubchannel(subchannel, tracerFactory)
+ .copyWithSubchannel(subchannel2);
+ assertThat(result.getSubchannel()).isSameInstanceAs(subchannel2);
+ assertThat(result.getStatus()).isSameInstanceAs(Status.OK);
+ assertThat(result.getStreamTracerFactory()).isSameInstanceAs(tracerFactory);
+ assertThat(result.isDrop()).isFalse();
+ }
+
+ @Test
+ public void pickResult_withStreamTracerFactory() {
+ PickResult result = PickResult.withSubchannel(subchannel)
+ .copyWithStreamTracerFactory(tracerFactory);
+ assertThat(result.getSubchannel()).isSameInstanceAs(subchannel);
+ assertThat(result.getStatus()).isSameInstanceAs(Status.OK);
+ assertThat(result.getStreamTracerFactory()).isSameInstanceAs(tracerFactory);
+ assertThat(result.isDrop()).isFalse();
+ }
+
@Test
public void pickResult_withNoResult() {
PickResult result = PickResult.withNoResult();
diff --git a/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java b/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java
index 30de2477d77..2479e339791 100644
--- a/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java
+++ b/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java
@@ -20,17 +20,23 @@
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableSet;
+import io.grpc.FlagResetRule;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
/** Unit tests for {@link ManagedChannelRegistry}. */
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class ManagedChannelRegistryTest {
private String target = "testing123";
private ChannelCredentials creds = new ChannelCredentials() {
@@ -40,6 +46,20 @@ public ChannelCredentials withoutBearerTokens() {
}
};
+ @Rule public final FlagResetRule flagResetRule = new FlagResetRule();
+
+ @Parameters(name = "enableRfc3986UrisParam={0}")
+ public static Iterable