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: + *

+ * + *

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");