diff --git a/api/src/main/java/io/grpc/ChildChannelConfigurer.java b/api/src/main/java/io/grpc/ChildChannelConfigurer.java
new file mode 100644
index 00000000000..36b67098797
--- /dev/null
+++ b/api/src/main/java/io/grpc/ChildChannelConfigurer.java
@@ -0,0 +1,70 @@
+/*
+ * 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.function.Consumer;
+
+/**
+ * A configurer for child channels created by gRPC's internal infrastructure.
+ *
+ *
This interface allows users to inject configuration (such as credentials, interceptors,
+ * or flow control settings) into channels created automatically by gRPC for control plane
+ * operations. Common use cases include:
+ *
+ * - xDS control plane connections
+ * - Load Balancing helper channels (OOB channels)
+ *
+ *
+ * Usage Example:
+ *
{@code
+ * // 1. Define the configurer
+ * ChildChannelConfigurer configurer = builder -> {
+ * builder.intercept(new MyAuthInterceptor());
+ * builder.maxInboundMessageSize(4 * 1024 * 1024);
+ * };
+ *
+ * // 2. Apply to parent channel - automatically used for ALL child channels
+ * ManagedChannel channel = ManagedChannelBuilder
+ * .forTarget("xds:///my-service")
+ * .childChannelConfigurer(configurer)
+ * .build();
+ * }
+ *
+ * Implementations must be thread-safe as {@link #accept} may be invoked concurrently
+ * by multiple internal components.
+ *
+ * @since 1.79.0
+ */
+@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
+@FunctionalInterface
+public interface ChildChannelConfigurer extends Consumer> {
+
+ /**
+ * Configures a builder for a new child channel.
+ *
+ * This method is invoked synchronously during the creation of the child channel,
+ * before {@link ManagedChannelBuilder#build()} is called.
+ *
+ *
Note: The provided {@code builder} is generic (`?`). Implementations should use
+ * universal configuration methods (like {@code intercept()}, {@code userAgent()}) rather
+ * than casting to specific implementation types.
+ *
+ * @param builder the mutable channel builder for the new child channel
+ */
+ @Override
+ void accept(ManagedChannelBuilder> builder);
+}
diff --git a/api/src/main/java/io/grpc/ChildChannelConfigurers.java b/api/src/main/java/io/grpc/ChildChannelConfigurers.java
new file mode 100644
index 00000000000..92508f5e71c
--- /dev/null
+++ b/api/src/main/java/io/grpc/ChildChannelConfigurers.java
@@ -0,0 +1,99 @@
+/*
+ * 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.checkNotNull;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Utilities for working with {@link ChildChannelConfigurer}.
+ *
+ * @since 1.79.0
+ */
+@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
+public final class ChildChannelConfigurers {
+ private static final Logger logger = Logger.getLogger(ChildChannelConfigurers.class.getName());
+
+ // Singleton no-op instance to avoid object churn
+ private static final ChildChannelConfigurer NO_OP = builder -> {
+ };
+
+ private ChildChannelConfigurers() { // Prevent instantiation
+ }
+
+ /**
+ * Returns a configurer that does nothing.
+ * Useful as a default value to avoid null checks in internal code.
+ */
+ public static ChildChannelConfigurer noOp() {
+ return NO_OP;
+ }
+
+ /**
+ * Returns a configurer that applies all the given configurers in sequence.
+ *
+ *
If any configurer in the chain throws an exception, the remaining ones are skipped
+ * (unless wrapped in {@link #safe(ChildChannelConfigurer)}).
+ *
+ * @param configurers the configurers to apply in order. Null elements are ignored.
+ */
+ public static ChildChannelConfigurer compose(ChildChannelConfigurer... configurers) {
+ checkNotNull(configurers, "configurers");
+ return builder -> {
+ for (ChildChannelConfigurer configurer : configurers) {
+ if (configurer != null) {
+ configurer.accept(builder);
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns a configurer that applies the delegate but catches and logs any exceptions.
+ *
+ *
This prevents a buggy configurer (e.g., one that fails metric setup) from crashing
+ * the critical path of channel creation.
+ *
+ * @param delegate the configurer to wrap.
+ */
+ public static ChildChannelConfigurer safe(ChildChannelConfigurer delegate) {
+ checkNotNull(delegate, "delegate");
+ return builder -> {
+ try {
+ delegate.accept(builder);
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Failed to apply child channel configuration", e);
+ }
+ };
+ }
+
+ /**
+ * Returns a configurer that applies the delegate only if the given condition is true.
+ *
+ *
Useful for applying interceptors only in specific environments (e.g., Debug/Test).
+ *
+ * @param condition true to apply the delegate, false to do nothing.
+ * @param delegate the configurer to apply if condition is true.
+ */
+ public static ChildChannelConfigurer conditional(boolean condition,
+ ChildChannelConfigurer delegate) {
+ checkNotNull(delegate, "delegate");
+ return condition ? delegate : NO_OP;
+ }
+}
diff --git a/api/src/main/java/io/grpc/ForwardingChannelBuilder.java b/api/src/main/java/io/grpc/ForwardingChannelBuilder.java
index 1202582421a..baedb55acfd 100644
--- a/api/src/main/java/io/grpc/ForwardingChannelBuilder.java
+++ b/api/src/main/java/io/grpc/ForwardingChannelBuilder.java
@@ -242,6 +242,18 @@ public T disableServiceConfigLookUp() {
return thisT();
}
+ @Override
+ public T configureChannel(ManagedChannel parentChannel) {
+ delegate().configureChannel(parentChannel);
+ return thisT();
+ }
+
+ @Override
+ public T childChannelConfigurer(ChildChannelConfigurer childChannelConfigurer) {
+ delegate().childChannelConfigurer(childChannelConfigurer);
+ return thisT();
+ }
+
/**
* Returns the correctly typed version of the builder.
*/
diff --git a/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java b/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java
index 78fe730d91a..7fc02ebd6a7 100644
--- a/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java
+++ b/api/src/main/java/io/grpc/ForwardingChannelBuilder2.java
@@ -269,6 +269,18 @@ public T setNameResolverArg(NameResolver.Args.Key key, X value) {
return thisT();
}
+ @Override
+ public T configureChannel(ManagedChannel parentChannel) {
+ delegate().configureChannel(parentChannel);
+ return thisT();
+ }
+
+ @Override
+ public T childChannelConfigurer(ChildChannelConfigurer childChannelConfigurer) {
+ delegate().childChannelConfigurer(childChannelConfigurer);
+ 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/ManagedChannel.java b/api/src/main/java/io/grpc/ManagedChannel.java
index 7875fdb57f2..7d6914c96b9 100644
--- a/api/src/main/java/io/grpc/ManagedChannel.java
+++ b/api/src/main/java/io/grpc/ManagedChannel.java
@@ -85,6 +85,23 @@ public ConnectivityState getState(boolean requestConnection) {
throw new UnsupportedOperationException("Not implemented");
}
+ /**
+ * Returns the configurer for child channels.
+ *
+ * This method is intended for use by the internal gRPC infrastructure (specifically
+ * load balancers and the channel builder) to propagate configuration to child channels.
+ * Application code should not call this method.
+ *
+ * @return the configurer, or {@code null} if none is set.
+ * @since 1.79.0
+ */
+ @Internal
+ public ChildChannelConfigurer getChildChannelConfigurer() {
+ // Return null by default so we don't break existing custom ManagedChannel implementations
+ // (like wrappers or mocks) that don't override this method.
+ return null;
+ }
+
/**
* Registers a one-off callback that will be run if the connectivity state of the channel diverges
* from the given {@code source}, which is typically what has just been returned by {@link
diff --git a/api/src/main/java/io/grpc/ManagedChannelBuilder.java b/api/src/main/java/io/grpc/ManagedChannelBuilder.java
index 3f370ab3003..6c4a18516c8 100644
--- a/api/src/main/java/io/grpc/ManagedChannelBuilder.java
+++ b/api/src/main/java/io/grpc/ManagedChannelBuilder.java
@@ -661,6 +661,41 @@ public T setNameResolverArg(NameResolver.Args.Key key, X value) {
throw new UnsupportedOperationException();
}
+ /**
+ * Configures this builder using settings derived from an existing parent channel.
+ *
+ * This method is typically used by internal components (like LoadBalancers) when creating
+ * child channels to ensure they inherit relevant configuration (like the
+ * {@link ChildChannelConfigurer}) from the parent.
+ *
+ *
The specific settings copied are implementation dependent, but typically include
+ * the child channel configurer and potentially user agents or offload executors.
+ *
+ * @param parentChannel the channel to inherit configuration from
+ * @return this
+ * @since 1.79.0
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
+ public T configureChannel(ManagedChannel parentChannel) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets a configurer that will be applied to all internal child channels created by this channel.
+ *
+ *
This allows injecting configuration (like credentials, interceptors, or flow control)
+ * into auxiliary channels created by gRPC infrastructure, such as xDS control plane connections
+ * or OOB load balancing channels.
+ *
+ * @param childChannelConfigurer the configurer to apply.
+ * @return this
+ * @since 1.79.0
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
+ public T childChannelConfigurer(ChildChannelConfigurer childChannelConfigurer) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
/**
* Builds a channel using the given parameters.
*
diff --git a/api/src/main/java/io/grpc/MetricRecorder.java b/api/src/main/java/io/grpc/MetricRecorder.java
index 897c28011cd..1f765ddc115 100644
--- a/api/src/main/java/io/grpc/MetricRecorder.java
+++ b/api/src/main/java/io/grpc/MetricRecorder.java
@@ -26,6 +26,15 @@
*/
@Internal
public interface MetricRecorder {
+
+ /**
+ * Returns a {@link MetricRecorder} that performs no operations.
+ * The returned instance ignores all calls and skips all validation checks.
+ */
+ static MetricRecorder noOp() {
+ return NoOpMetricRecorder.INSTANCE;
+ }
+
/**
* Adds a value for a double-precision counter metric instrument.
*
@@ -176,4 +185,47 @@ interface Registration extends AutoCloseable {
@Override
void close();
}
+
+ /**
+ * No-Op implementation of MetricRecorder.
+ * Overrides all default methods to skip validation checks for maximum performance.
+ */
+ final class NoOpMetricRecorder implements MetricRecorder {
+ private static final NoOpMetricRecorder INSTANCE = new NoOpMetricRecorder();
+
+ @Override
+ public void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, double value,
+ List requiredLabelValues,
+ List optionalLabelValues) {
+ }
+
+ @Override
+ public void addLongCounter(LongCounterMetricInstrument metricInstrument, long value,
+ List requiredLabelValues, List optionalLabelValues) {
+ }
+
+ @Override
+ public void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value,
+ List requiredLabelValues,
+ List optionalLabelValues) {
+ }
+
+ @Override
+ public void recordDoubleHistogram(DoubleHistogramMetricInstrument metricInstrument,
+ double value, List requiredLabelValues,
+ List optionalLabelValues) {
+ }
+
+ @Override
+ public void recordLongHistogram(LongHistogramMetricInstrument metricInstrument, long value,
+ List requiredLabelValues,
+ List optionalLabelValues) {
+ }
+
+ @Override
+ public Registration registerBatchCallback(BatchCallback callback,
+ CallbackMetricInstrument... metricInstruments) {
+ return () -> { };
+ }
+ }
}
diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java
index 0e8315e812c..d6e6c563823 100644
--- a/api/src/main/java/io/grpc/NameResolver.java
+++ b/api/src/main/java/io/grpc/NameResolver.java
@@ -323,6 +323,7 @@ public static final class Args {
@Nullable private final MetricRecorder metricRecorder;
@Nullable private final NameResolverRegistry nameResolverRegistry;
@Nullable private final IdentityHashMap, Object> customArgs;
+ @Nullable private final ManagedChannel parentChannel;
private Args(Builder builder) {
this.defaultPort = checkNotNull(builder.defaultPort, "defaultPort not set");
@@ -337,6 +338,7 @@ private Args(Builder builder) {
this.metricRecorder = builder.metricRecorder;
this.nameResolverRegistry = builder.nameResolverRegistry;
this.customArgs = cloneCustomArgs(builder.customArgs);
+ this.parentChannel = builder.parentChannel;
}
/**
@@ -435,6 +437,14 @@ public ChannelLogger getChannelLogger() {
return channelLogger;
}
+ /**
+ * Returns the parent {@link ManagedChannel} served by this NameResolver.
+ */
+ @Internal
+ public ManagedChannel getParentChannel() {
+ return parentChannel;
+ }
+
/**
* Returns the Executor on which this resolver should execute long-running or I/O bound work.
* Null if no Executor was set.
@@ -544,6 +554,7 @@ public static final class Builder {
private MetricRecorder metricRecorder;
private NameResolverRegistry nameResolverRegistry;
private IdentityHashMap, Object> customArgs;
+ private ManagedChannel parentChannel;
Builder() {
}
@@ -659,6 +670,16 @@ public Builder setNameResolverRegistry(NameResolverRegistry registry) {
return this;
}
+ /**
+ * See {@link Args#parentChannel}. This is an optional field.
+ *
+ * @since 1.79.0
+ */
+ public Builder setParentChannel(ManagedChannel parentChannel) {
+ this.parentChannel = parentChannel;
+ return this;
+ }
+
/**
* Builds an {@link Args}.
*
diff --git a/api/src/test/java/io/grpc/ChildChannelConfigurersTest.java b/api/src/test/java/io/grpc/ChildChannelConfigurersTest.java
new file mode 100644
index 00000000000..08ebcbc6e08
--- /dev/null
+++ b/api/src/test/java/io/grpc/ChildChannelConfigurersTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ChildChannelConfigurersTest {
+
+ @Test
+ public void noOp_doesNothing() {
+ ManagedChannelBuilder> builder = mock(ManagedChannelBuilder.class);
+ ChildChannelConfigurers.noOp().accept(builder);
+ verifyNoInteractions(builder);
+ }
+
+ @Test
+ public void compose_runsInOrder() {
+ ManagedChannelBuilder> builder = mock(ManagedChannelBuilder.class);
+ ChildChannelConfigurer configurer1 = b -> b.userAgent("agent1");
+ ChildChannelConfigurer configurer2 = b -> b.maxInboundMessageSize(123);
+
+ ChildChannelConfigurers.compose(configurer1, configurer2).accept(builder);
+
+ verify(builder).userAgent("agent1");
+ verify(builder).maxInboundMessageSize(123);
+ }
+
+ @Test
+ public void compose_ignoresNulls() {
+ ManagedChannelBuilder> builder = mock(ManagedChannelBuilder.class);
+ ChildChannelConfigurer configurer = b -> b.userAgent("agent1");
+
+ ChildChannelConfigurers.compose(null, configurer, null).accept(builder);
+
+ verify(builder).userAgent("agent1");
+ }
+}
\ No newline at end of file
diff --git a/api/src/test/java/io/grpc/NameResolverTest.java b/api/src/test/java/io/grpc/NameResolverTest.java
index 82abe5c7505..ead5c462286 100644
--- a/api/src/test/java/io/grpc/NameResolverTest.java
+++ b/api/src/test/java/io/grpc/NameResolverTest.java
@@ -105,6 +105,7 @@ public void args() {
}
private NameResolver.Args createArgs() {
+ ManagedChannel parent = mock(ManagedChannel.class);
return NameResolver.Args.newBuilder()
.setDefaultPort(defaultPort)
.setProxyDetector(proxyDetector)
@@ -116,9 +117,35 @@ private NameResolver.Args createArgs() {
.setOverrideAuthority(overrideAuthority)
.setMetricRecorder(metricRecorder)
.setArg(FOO_ARG_KEY, customArgValue)
+ .setParentChannel(parent)
.build();
}
+ @Test
+ public void args_parentChannel() {
+ ManagedChannel parent = mock(ManagedChannel.class);
+
+ // Create a real SynchronizationContext instead of mocking it
+ SynchronizationContext realSyncContext = new SynchronizationContext(
+ new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread t, Throwable e) {
+ throw new AssertionError(e);
+ }
+ });
+
+ NameResolver.Args args = NameResolver.Args.newBuilder()
+ .setDefaultPort(8080)
+ .setProxyDetector(mock(ProxyDetector.class))
+ .setSynchronizationContext(realSyncContext)
+ .setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class))
+ .setChannelLogger(mock(ChannelLogger.class))
+ .setParentChannel(parent)
+ .build();
+
+ assertThat(args.getParentChannel()).isSameInstanceAs(parent);
+ }
+
@Test
@SuppressWarnings("deprecation")
public void startOnOldListener_wrapperListener2UsedToStart() {
diff --git a/core/src/main/java/io/grpc/internal/ForwardingManagedChannel.java b/core/src/main/java/io/grpc/internal/ForwardingManagedChannel.java
index 7ef4ce42e97..ae09785258b 100644
--- a/core/src/main/java/io/grpc/internal/ForwardingManagedChannel.java
+++ b/core/src/main/java/io/grpc/internal/ForwardingManagedChannel.java
@@ -18,6 +18,7 @@
import com.google.common.base.MoreObjects;
import io.grpc.CallOptions;
+import io.grpc.ChildChannelConfigurer;
import io.grpc.ClientCall;
import io.grpc.ConnectivityState;
import io.grpc.ManagedChannel;
@@ -92,4 +93,9 @@ public void enterIdle() {
public String toString() {
return MoreObjects.toStringHelper(this).add("delegate", delegate).toString();
}
+
+ @Override
+ public ChildChannelConfigurer getChildChannelConfigurer() {
+ return delegate.getChildChannelConfigurer();
+ }
}
diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java
index 849e4b8e45c..2fa2ad8742a 100644
--- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java
+++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java
@@ -40,6 +40,8 @@
import io.grpc.ChannelCredentials;
import io.grpc.ChannelLogger;
import io.grpc.ChannelLogger.ChannelLogLevel;
+import io.grpc.ChildChannelConfigurer;
+import io.grpc.ChildChannelConfigurers;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ClientInterceptors;
@@ -154,6 +156,15 @@ public Result selectConfig(PickSubchannelArgs args) {
private static final LoadBalancer.PickDetailsConsumer NOOP_PICK_DETAILS_CONSUMER =
new LoadBalancer.PickDetailsConsumer() {};
+ /**
+ * Stores the user-provided configuration function for internal child channels.
+ *
+ * This is intended for use by gRPC internal components (NameResolvers, LoadBalancers)
+ * that are responsible for creating auxillary {@code ManagedChannel} instances.
+ * guaranteed to be not null (defaults to no-op).
+ */
+ private ChildChannelConfigurer childChannelConfigurer = ChildChannelConfigurers.noOp();
+
private final InternalLogId logId;
private final String target;
@Nullable
@@ -553,6 +564,9 @@ ClientStream newSubstream(
Supplier stopwatchSupplier,
List interceptors,
final TimeProvider timeProvider) {
+ if (builder.childChannelConfigurer != null) {
+ this.childChannelConfigurer = builder.childChannelConfigurer;
+ }
this.target = checkNotNull(builder.target, "target");
this.logId = InternalLogId.allocate("Channel", target);
this.timeProvider = checkNotNull(timeProvider, "timeProvider");
@@ -599,7 +613,8 @@ ClientStream newSubstream(
.setOffloadExecutor(this.offloadExecutorHolder)
.setOverrideAuthority(this.authorityOverride)
.setMetricRecorder(this.metricRecorder)
- .setNameResolverRegistry(builder.nameResolverRegistry);
+ .setNameResolverRegistry(builder.nameResolverRegistry)
+ .setParentChannel(this);
builder.copyAllNameResolverCustomArgsTo(nameResolverArgsBuilder);
this.nameResolverArgs = nameResolverArgsBuilder.build();
this.nameResolver = getNameResolver(
@@ -675,6 +690,19 @@ public CallTracer create() {
}
}
+ /**
+ * Retrieves the user-provided configuration function for internal child channels.
+ *
+ * This method is intended for use by gRPC internal components (NameResolvers, LoadBalancers)
+ * that are responsible for creating auxiliary {@code ManagedChannel} instances.
+ *
+ * @return the ChildChannelConfigurer, guaranteed to be not null (defaults to no-op).
+ */
+ @Override
+ public ChildChannelConfigurer getChildChannelConfigurer() {
+ return childChannelConfigurer;
+ }
+
@VisibleForTesting
static NameResolver getNameResolver(
URI targetUri, @Nullable final String overrideAuthority,
@@ -1467,7 +1495,11 @@ void onStateChange(InternalSubchannel is, ConnectivityStateInfo newState) {
subchannelLogger,
transportFilters,
target,
- lbHelper.getMetricRecorder());
+ /*
+ * TODO(AgraVator): we are breaking the metrics for the internal sub channels of
+ * OobChannels by passing in MetricRecorder.noOp(). Point this out in the release notes.
+ */
+ MetricRecorder.noOp());
oobChannelTracer.reportEvent(new ChannelTrace.Event.Builder()
.setDescription("Child Subchannel created")
.setSeverity(ChannelTrace.Event.Severity.CT_INFO)
@@ -1558,6 +1590,11 @@ protected ManagedChannelBuilder> delegate() {
ResolvingOobChannelBuilder builder = new ResolvingOobChannelBuilder();
+ // Note that we follow the global configurator pattern and try to fuse the configurations as
+ // soon as the builder gets created
+ ManagedChannel parentChannel = ManagedChannelImpl.this;
+ builder.configureChannel(parentChannel);
+
return builder
// TODO(zdapeng): executors should not outlive the parent channel.
.executor(executor)
diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java
index 1773c04388d..8cc588f2117 100644
--- a/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java
+++ b/core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java
@@ -29,6 +29,7 @@
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ChannelCredentials;
+import io.grpc.ChildChannelConfigurer;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ClientTransportFilter;
@@ -124,6 +125,8 @@ public static ManagedChannelBuilder> forTarget(String target) {
private static final Method GET_CLIENT_INTERCEPTOR_METHOD;
+ ChildChannelConfigurer childChannelConfigurer;
+
static {
Method getClientInterceptorMethod = null;
try {
@@ -714,6 +717,41 @@ protected ManagedChannelImplBuilder addMetricSink(MetricSink metricSink) {
return this;
}
+ /**
+ * Applies the configuration logic from the given parent channel to this builder.
+ *
+ *
This mechanism allows properties (like metrics, tracing, or interceptors) to propagate
+ * automatically from a parent channel to any child channels it creates
+ * (e.g., Subchannels or OOB channels).
+ *
+ * @param parentChannel the channel whose configuration logic should be applied to this builder.
+ */
+ @Override
+ public ManagedChannelImplBuilder configureChannel(ManagedChannel parentChannel) {
+ if (parentChannel != null) {
+ ChildChannelConfigurer childChannelConfigurer = parentChannel.getChildChannelConfigurer();
+ if (childChannelConfigurer != null) {
+ childChannelConfigurer.accept(this);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the configurer that will be stored in the channel built by this builder.
+ *
+ *
This configurer will subsequently be used to configure any descendants (children)
+ * created by that channel.
+ *
+ * @param childChannelConfigurer the configurer to store in the channel.
+ */
+ @Override
+ public ManagedChannelImplBuilder childChannelConfigurer(
+ ChildChannelConfigurer childChannelConfigurer) {
+ this.childChannelConfigurer = childChannelConfigurer;
+ return this;
+ }
+
@Override
public ManagedChannel build() {
ClientTransportFactory clientTransportFactory =
diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java
index 91a9f506bc8..8312e15d98d 100644
--- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java
+++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java
@@ -69,6 +69,7 @@
import io.grpc.Channel;
import io.grpc.ChannelCredentials;
import io.grpc.ChannelLogger;
+import io.grpc.ChildChannelConfigurer;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ClientInterceptors;
@@ -101,6 +102,7 @@
import io.grpc.LoadBalancerRegistry;
import io.grpc.LongCounterMetricInstrument;
import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.MethodDescriptor.MethodType;
@@ -302,9 +304,11 @@ public String getPolicyName() {
private boolean panicExpected;
@Captor
private ArgumentCaptor resolvedAddressCaptor;
-
private ArgumentCaptor streamListenerCaptor =
ArgumentCaptor.forClass(ClientStreamListener.class);
+ @Mock
+ private ChildChannelConfigurer mockChildChannelConfigurer;
+
private void createChannel(ClientInterceptor... interceptors) {
createChannel(false, interceptors);
@@ -5139,4 +5143,116 @@ private static ManagedChannelServiceConfig createManagedChannelServiceConfig(
return ManagedChannelServiceConfig
.fromServiceConfig(rawServiceConfig, true, 3, 4, policySelection);
}
+
+ @Test
+ public void getChildChannelConfigurer_returnsConfiguredValue() {
+ ManagedChannelImplBuilder builder = new ManagedChannelImplBuilder(TARGET,
+ new ClientTransportFactoryBuilder() {
+ @Override
+ public ClientTransportFactory buildClientTransportFactory() {
+ return mockTransportFactory;
+ }
+ },
+ new FixedPortProvider(DEFAULT_PORT));
+
+ when(mockTransportFactory.getSupportedSocketAddressTypes())
+ .thenReturn(Collections.singleton(InetSocketAddress.class));
+
+ ManagedChannel channel = builder
+ .nameResolverFactory(new FakeNameResolverFactory(
+ Collections.singletonList(URI.create(TARGET)),
+ Collections.emptyList(),
+ true,
+ null))
+ .childChannelConfigurer(mockChildChannelConfigurer)
+ .build();
+
+ assertThat(channel.getChildChannelConfigurer()).isSameInstanceAs(mockChildChannelConfigurer);
+ channel.shutdownNow();
+ }
+
+ @Test
+ public void configureChannel_propagatesConfigurerToNewBuilder_endToEnd() {
+ when(mockTransportFactory.getSupportedSocketAddressTypes())
+ .thenReturn(Collections.singleton(InetSocketAddress.class));
+
+ // 1. Setup Interceptor
+ final AtomicInteger interceptorCalls = new AtomicInteger(0);
+ final ClientInterceptor trackingInterceptor = new ClientInterceptor() {
+ @Override
+ public ClientCall interceptCall(
+ MethodDescriptor method, CallOptions callOptions, Channel next) {
+ interceptorCalls.incrementAndGet();
+ return next.newCall(method, callOptions);
+ }
+ };
+
+ // 2. Setup Configurer
+ ChildChannelConfigurer configurer = new ChildChannelConfigurer() {
+ @Override
+ public void accept(ManagedChannelBuilder> builder) {
+ builder.intercept(trackingInterceptor);
+ }
+ };
+
+ // 3. Create Parent Channel
+ ManagedChannelImplBuilder parentBuilder = new ManagedChannelImplBuilder("fake://parent-target",
+ new ClientTransportFactoryBuilder() {
+ @Override
+ public ClientTransportFactory buildClientTransportFactory() {
+ return mockTransportFactory;
+ }
+ },
+ new FixedPortProvider(DEFAULT_PORT));
+
+ ManagedChannel parentChannel = parentBuilder
+ // MATCH THE CONSTRUCTOR SIGNATURE: (List, List, boolean, Status)
+ .nameResolverFactory(new FakeNameResolverFactory(
+ Collections.singletonList(URI.create("fake://parent-target")),
+ Collections.emptyList(),
+ true,
+ null))
+ .childChannelConfigurer(configurer)
+ .build();
+
+ // 4. Create Child Channel Builder
+ ManagedChannelImplBuilder childBuilder = new ManagedChannelImplBuilder("fake://child-target",
+ new ClientTransportFactoryBuilder() {
+ @Override
+ public ClientTransportFactory buildClientTransportFactory() {
+ return mockTransportFactory;
+ }
+ },
+ new FixedPortProvider(DEFAULT_PORT));
+
+ childBuilder.configureChannel(parentChannel);
+
+ // Ensure child also has a resolver factory
+ childBuilder.nameResolverFactory(new FakeNameResolverFactory(
+ Collections.singletonList(URI.create("fake://child-target")),
+ Collections.emptyList(),
+ true,
+ null));
+
+ ManagedChannel childChannel = childBuilder.build();
+
+ // 5. Verification
+ ClientCall call = childChannel.newCall(
+ MethodDescriptor.newBuilder()
+ .setType(MethodDescriptor.MethodType.UNARY)
+ .setFullMethodName("service/method")
+ .setRequestMarshaller(new StringMarshaller())
+ .setResponseMarshaller(new IntegerMarshaller())
+ .build(),
+ CallOptions.DEFAULT);
+
+ call.start(new ClientCall.Listener() {
+ }, new Metadata());
+
+ assertThat(interceptorCalls.get())
+ .isEqualTo(1);
+
+ childChannel.shutdownNow();
+ parentChannel.shutdownNow();
+ }
}
diff --git a/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java b/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java
index 33bf9bb41e2..715b09a6cc4 100644
--- a/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java
+++ b/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java
@@ -16,6 +16,7 @@
package io.grpc.internal;
+import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
@@ -326,4 +327,25 @@ public void recordLongGaugeMismatchedOptionalLabelValues() {
callbackCaptor.getValue().run();
registration.close();
}
+
+ @Test
+ public void noOp_returnsSingleton() {
+ assertThat(MetricRecorder.noOp()).isSameInstanceAs(MetricRecorder.noOp());
+ }
+
+ @Test
+ public void noOp_methodsDoNotThrow() {
+ MetricRecorder recorder = MetricRecorder.noOp();
+
+ recorder.addDoubleCounter(null, 1.0,
+ null, null);
+ recorder.addLongCounter(null, 1,
+ null, null);
+ recorder.addLongUpDownCounter(null, 1,
+ null, null);
+ recorder.recordDoubleHistogram(null, 1.0,
+ null, null);
+ recorder.recordLongHistogram(null, 1,
+ null, null);
+ }
}
diff --git a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java
index 0da51bf47f7..54d600edee3 100644
--- a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java
+++ b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java
@@ -36,14 +36,16 @@
final class GrpcXdsTransportFactory implements XdsTransportFactory {
private final CallCredentials callCredentials;
+ private final ManagedChannel parentChannel;
- GrpcXdsTransportFactory(CallCredentials callCredentials) {
+ GrpcXdsTransportFactory(CallCredentials callCredentials, ManagedChannel parentChannel) {
this.callCredentials = callCredentials;
+ this.parentChannel = parentChannel;
}
@Override
public XdsTransport create(Bootstrapper.ServerInfo serverInfo) {
- return new GrpcXdsTransport(serverInfo, callCredentials);
+ return new GrpcXdsTransport(serverInfo, callCredentials, parentChannel);
}
@VisibleForTesting
@@ -75,6 +77,17 @@ public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials call
this.callCredentials = callCredentials;
}
+ public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, CallCredentials callCredentials,
+ ManagedChannel parentChannel) {
+ String target = serverInfo.target();
+ ChannelCredentials channelCredentials = (ChannelCredentials) serverInfo.implSpecificConfig();
+ this.channel = Grpc.newChannelBuilder(target, channelCredentials)
+ .keepAliveTime(5, TimeUnit.MINUTES)
+ .configureChannel(parentChannel)
+ .build();
+ this.callCredentials = callCredentials;
+ }
+
@VisibleForTesting
public GrpcXdsTransport(ManagedChannel channel, CallCredentials callCredentials) {
this.channel = checkNotNull(channel, "channel");
diff --git a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java
index cc5ff128274..1e3200eaef6 100644
--- a/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java
+++ b/xds/src/main/java/io/grpc/xds/InternalSharedXdsClientPoolProvider.java
@@ -86,7 +86,7 @@ public static XdsClientResult getOrCreate(
String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder,
CallCredentials transportCallCredentials) {
return new XdsClientResult(SharedXdsClientPoolProvider.getDefaultProvider()
- .getOrCreate(target, bootstrapInfo, metricRecorder, transportCallCredentials));
+ .getOrCreate(target, bootstrapInfo, metricRecorder, transportCallCredentials, null));
}
/**
diff --git a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java
index 45c379244af..a1705610300 100644
--- a/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java
+++ b/xds/src/main/java/io/grpc/xds/SharedXdsClientPoolProvider.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import io.grpc.CallCredentials;
+import io.grpc.ManagedChannel;
import io.grpc.MetricRecorder;
import io.grpc.internal.ExponentialBackoffPolicy;
import io.grpc.internal.GrpcUtil;
@@ -57,6 +58,10 @@ final class SharedXdsClientPoolProvider implements XdsClientPoolFactory {
@Nullable
private final Bootstrapper bootstrapper;
private final Object lock = new Object();
+ /*
+ The first one wins.
+ Anything with the same target string uses the client created for the first one.
+ */
private final Map> targetToXdsClientMap = new ConcurrentHashMap<>();
SharedXdsClientPoolProvider() {
@@ -88,20 +93,31 @@ public ObjectPool getOrCreate(
} else {
bootstrapInfo = GrpcBootstrapperImpl.defaultBootstrap();
}
- return getOrCreate(target, bootstrapInfo, metricRecorder, transportCallCredentials);
+ return getOrCreate(target, bootstrapInfo, metricRecorder, transportCallCredentials,
+ null);
}
@Override
public ObjectPool getOrCreate(
String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder) {
- return getOrCreate(target, bootstrapInfo, metricRecorder, null);
+ return getOrCreate(target, bootstrapInfo, metricRecorder, null,
+ null);
+ }
+
+ @Override
+ public ObjectPool getOrCreate(
+ String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder,
+ ManagedChannel parentChannel) {
+ return getOrCreate(target, bootstrapInfo, metricRecorder, null,
+ parentChannel);
}
public ObjectPool getOrCreate(
String target,
BootstrapInfo bootstrapInfo,
MetricRecorder metricRecorder,
- CallCredentials transportCallCredentials) {
+ CallCredentials transportCallCredentials,
+ ManagedChannel parentChannel) {
ObjectPool ref = targetToXdsClientMap.get(target);
if (ref == null) {
synchronized (lock) {
@@ -109,7 +125,7 @@ public ObjectPool getOrCreate(
if (ref == null) {
ref =
new RefCountedXdsClientObjectPool(
- bootstrapInfo, target, metricRecorder, transportCallCredentials);
+ bootstrapInfo, target, metricRecorder, transportCallCredentials, parentChannel);
targetToXdsClientMap.put(target, ref);
}
}
@@ -134,6 +150,7 @@ class RefCountedXdsClientObjectPool implements ObjectPool {
private final String target; // The target associated with the xDS client.
private final MetricRecorder metricRecorder;
private final CallCredentials transportCallCredentials;
+ private final ManagedChannel parentChannel;
private final Object lock = new Object();
@GuardedBy("lock")
private ScheduledExecutorService scheduler;
@@ -147,7 +164,7 @@ class RefCountedXdsClientObjectPool implements ObjectPool {
@VisibleForTesting
RefCountedXdsClientObjectPool(
BootstrapInfo bootstrapInfo, String target, MetricRecorder metricRecorder) {
- this(bootstrapInfo, target, metricRecorder, null);
+ this(bootstrapInfo, target, metricRecorder, null, null);
}
@VisibleForTesting
@@ -155,11 +172,13 @@ class RefCountedXdsClientObjectPool implements ObjectPool {
BootstrapInfo bootstrapInfo,
String target,
MetricRecorder metricRecorder,
- CallCredentials transportCallCredentials) {
+ CallCredentials transportCallCredentials,
+ ManagedChannel parentChannel) {
this.bootstrapInfo = checkNotNull(bootstrapInfo, "bootstrapInfo");
this.target = target;
this.metricRecorder = checkNotNull(metricRecorder, "metricRecorder");
this.transportCallCredentials = transportCallCredentials;
+ this.parentChannel = parentChannel;
}
@Override
@@ -172,7 +191,7 @@ public XdsClient getObject() {
scheduler = SharedResourceHolder.get(GrpcUtil.TIMER_SERVICE);
metricReporter = new XdsClientMetricReporterImpl(metricRecorder, target);
GrpcXdsTransportFactory xdsTransportFactory =
- new GrpcXdsTransportFactory(transportCallCredentials);
+ new GrpcXdsTransportFactory(transportCallCredentials, parentChannel);
xdsClient =
new XdsClientImpl(
xdsTransportFactory,
diff --git a/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java b/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java
index 6df8d566a7a..4fbd5d7df30 100644
--- a/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java
+++ b/xds/src/main/java/io/grpc/xds/XdsClientPoolFactory.java
@@ -16,6 +16,7 @@
package io.grpc.xds;
+import io.grpc.ManagedChannel;
import io.grpc.MetricRecorder;
import io.grpc.internal.ObjectPool;
import io.grpc.xds.client.Bootstrapper.BootstrapInfo;
@@ -30,5 +31,9 @@ interface XdsClientPoolFactory {
ObjectPool getOrCreate(
String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder);
+ ObjectPool getOrCreate(
+ String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder,
+ ManagedChannel parentChannel);
+
List getTargets();
}
diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java
index 196d51fb5a6..abc4ff09371 100644
--- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java
+++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java
@@ -39,6 +39,7 @@
import io.grpc.InternalConfigSelector;
import io.grpc.InternalLogId;
import io.grpc.LoadBalancer.PickSubchannelArgs;
+import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.MetricRecorder;
@@ -182,7 +183,8 @@ final class XdsNameResolver extends NameResolver {
} else {
checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory");
this.xdsClientPool = new BootstrappingXdsClientPool(
- xdsClientPoolFactory, target, bootstrapOverride, metricRecorder);
+ xdsClientPoolFactory, target, bootstrapOverride, metricRecorder,
+ nameResolverArgs.getParentChannel());
}
this.random = checkNotNull(random, "random");
this.filterRegistry = checkNotNull(filterRegistry, "filterRegistry");
@@ -1054,16 +1056,19 @@ private static final class BootstrappingXdsClientPool implements XdsClientPool {
private final @Nullable Map bootstrapOverride;
private final @Nullable MetricRecorder metricRecorder;
private ObjectPool xdsClientPool;
+ private ManagedChannel parentChannel;
BootstrappingXdsClientPool(
XdsClientPoolFactory xdsClientPoolFactory,
String target,
@Nullable Map bootstrapOverride,
- @Nullable MetricRecorder metricRecorder) {
+ @Nullable MetricRecorder metricRecorder,
+ @Nullable ManagedChannel parentChannel) {
this.xdsClientPoolFactory = checkNotNull(xdsClientPoolFactory, "xdsClientPoolFactory");
this.target = checkNotNull(target, "target");
this.bootstrapOverride = bootstrapOverride;
this.metricRecorder = metricRecorder;
+ this.parentChannel = parentChannel;
}
@Override
@@ -1076,7 +1081,7 @@ public XdsClient getObject() throws XdsInitializationException {
bootstrapInfo = new GrpcBootstrapperImpl().bootstrap(bootstrapOverride);
}
this.xdsClientPool =
- xdsClientPoolFactory.getOrCreate(target, bootstrapInfo, metricRecorder);
+ xdsClientPoolFactory.getOrCreate(target, bootstrapInfo, metricRecorder, parentChannel);
}
return xdsClientPool.getObject();
}
diff --git a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java
index e8bd7461736..ff5edaded00 100644
--- a/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java
+++ b/xds/src/test/java/io/grpc/xds/CsdsServiceTest.java
@@ -39,6 +39,7 @@
import io.envoyproxy.envoy.type.matcher.v3.NodeMatcher;
import io.grpc.Deadline;
import io.grpc.InsecureChannelCredentials;
+import io.grpc.ManagedChannel;
import io.grpc.MetricRecorder;
import io.grpc.Status;
import io.grpc.Status.Code;
@@ -517,5 +518,12 @@ public ObjectPool getOrCreate(
String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder) {
throw new UnsupportedOperationException("Should not be called");
}
+
+ @Override
+ public ObjectPool getOrCreate(
+ String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder,
+ ManagedChannel parentChannel) {
+ throw new UnsupportedOperationException("Should not be called");
+ }
}
}
diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
index 887923b169b..caad48a54e9 100644
--- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
+++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java
@@ -5068,7 +5068,7 @@ public void serverFailureMetricReport_forRetryAndBackoff() {
private XdsClientImpl createXdsClient(String serverUri) {
BootstrapInfo bootstrapInfo = buildBootStrap(serverUri);
return new XdsClientImpl(
- new GrpcXdsTransportFactory(null),
+ new GrpcXdsTransportFactory(null, null),
bootstrapInfo,
fakeClock.getScheduledExecutorService(),
backoffPolicyProvider,
diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java
index 66e0d4b3198..93b3be3c9ba 100644
--- a/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java
+++ b/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java
@@ -92,7 +92,7 @@ public void onCompleted() {
@Test
public void callApis() throws Exception {
XdsTransportFactory.XdsTransport xdsTransport =
- new GrpcXdsTransportFactory(null)
+ new GrpcXdsTransportFactory(null, null)
.create(
Bootstrapper.ServerInfo.create(
"localhost:" + server.getPort(), InsecureChannelCredentials.create()));
diff --git a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java
index 9bdf86132b6..bfc7db19750 100644
--- a/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java
+++ b/xds/src/test/java/io/grpc/xds/LoadReportClientTest.java
@@ -181,7 +181,7 @@ public void cancelled(Context context) {
lrsClient =
new LoadReportClient(
loadStatsManager,
- new GrpcXdsTransportFactory(null).createForTest(channel),
+ new GrpcXdsTransportFactory(null, null).createForTest(channel),
NODE,
syncContext,
fakeClock.getScheduledExecutorService(),
diff --git a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java
index 29b149f166f..9f091a79ff9 100644
--- a/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java
+++ b/xds/src/test/java/io/grpc/xds/SharedXdsClientPoolProviderTest.java
@@ -207,7 +207,7 @@ public void xdsClient_usesCallCredentials() throws Exception {
// Create xDS client that uses the CallCredentials on the transport
ObjectPool xdsClientPool =
- provider.getOrCreate("target", bootstrapInfo, metricRecorder, sampleCreds);
+ provider.getOrCreate("target", bootstrapInfo, metricRecorder, sampleCreds, null);
XdsClient xdsClient = xdsClientPool.getObject();
xdsClient.watchXdsResource(
XdsListenerResource.getInstance(), "someLDSresource", ldsResourceWatcher);
diff --git a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java
index 27ee8d22825..4d5e7d09ad4 100644
--- a/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java
+++ b/xds/src/test/java/io/grpc/xds/XdsClientFallbackTest.java
@@ -484,7 +484,7 @@ public void fallbackFromBadUrlToGoodOne() {
XdsClientImpl client =
CommonBootstrapperTestUtils.createXdsClient(
Arrays.asList(garbageUri, validUri),
- new GrpcXdsTransportFactory(null),
+ new GrpcXdsTransportFactory(null, null),
fakeClock,
new ExponentialBackoffPolicy.Provider(),
MessagePrinter.INSTANCE,
@@ -509,7 +509,7 @@ public void testGoodUrlFollowedByBadUrl() {
XdsClientImpl client =
CommonBootstrapperTestUtils.createXdsClient(
Arrays.asList(validUri, garbageUri),
- new GrpcXdsTransportFactory(null),
+ new GrpcXdsTransportFactory(null, null),
fakeClock,
new ExponentialBackoffPolicy.Provider(),
MessagePrinter.INSTANCE,
@@ -536,7 +536,7 @@ public void testTwoBadUrl() {
XdsClientImpl client =
CommonBootstrapperTestUtils.createXdsClient(
Arrays.asList(garbageUri1, garbageUri2),
- new GrpcXdsTransportFactory(null),
+ new GrpcXdsTransportFactory(null, null),
fakeClock,
new ExponentialBackoffPolicy.Provider(),
MessagePrinter.INSTANCE,
diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java
index 45a96ee172f..95c4cde279a 100644
--- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java
+++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java
@@ -57,6 +57,7 @@
import io.grpc.InternalConfigSelector.Result;
import io.grpc.LoadBalancer.PickDetailsConsumer;
import io.grpc.LoadBalancer.PickSubchannelArgs;
+import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.MethodDescriptor.MethodType;
@@ -2493,6 +2494,24 @@ public XdsClient returnObject(Object object) {
};
}
+ @Override
+ public ObjectPool getOrCreate(
+ String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder,
+ ManagedChannel parentChannel) {
+ targets.add(target);
+ return new ObjectPool() {
+ @Override
+ public XdsClient getObject() {
+ return xdsClient;
+ }
+
+ @Override
+ public XdsClient returnObject(Object object) {
+ return null;
+ }
+ };
+ }
+
@Override
public List getTargets() {
if (targets.isEmpty()) {
diff --git a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java
index 386793299d8..bc300a4ef68 100644
--- a/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java
+++ b/xds/src/test/java/io/grpc/xds/XdsServerTestHelper.java
@@ -23,6 +23,7 @@
import com.google.common.util.concurrent.SettableFuture;
import io.envoyproxy.envoy.config.core.v3.SocketAddress.Protocol;
import io.grpc.InsecureChannelCredentials;
+import io.grpc.ManagedChannel;
import io.grpc.MetricRecorder;
import io.grpc.Status;
import io.grpc.StatusOr;
@@ -182,6 +183,25 @@ public XdsClient returnObject(Object object) {
};
}
+ @Override
+ public ObjectPool getOrCreate(
+ String target, BootstrapInfo bootstrapInfo, MetricRecorder metricRecorder,
+ ManagedChannel parentChannel) {
+ this.savedBootstrapInfo = bootstrapInfo;
+ return new ObjectPool() {
+ @Override
+ public XdsClient getObject() {
+ return xdsClient;
+ }
+
+ @Override
+ public XdsClient returnObject(Object object) {
+ xdsClient.shutdown();
+ return null;
+ }
+ };
+ }
+
@Override
public List getTargets() {
return Collections.singletonList("fake-target");