Skip to content

Commit 476e5a8

Browse files
committed
enable child channel plugins
1 parent bdbcaf1 commit 476e5a8

24 files changed

Lines changed: 509 additions & 22 deletions
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2025 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc;
18+
19+
import java.util.function.Consumer;
20+
21+
/**
22+
* A configurer for child channels created by gRPC's internal infrastructure.
23+
*
24+
* <p>This interface allows users to inject configuration (such as credentials, interceptors,
25+
* or flow control settings) into channels created automatically by gRPC for control plane
26+
* operations. Common use cases include:
27+
* <ul>
28+
* <li>xDS control plane connections</li>
29+
* <li>Load Balancing helper channels (OOB channels)</li>
30+
* </ul>
31+
*
32+
* <p><strong>Usage Example:</strong>
33+
* <pre>{@code
34+
* // 1. Define the configurer
35+
* ChildChannelConfigurer configurer = builder -> {
36+
* builder.intercept(new MyAuthInterceptor());
37+
* builder.maxInboundMessageSize(4 * 1024 * 1024);
38+
* };
39+
*
40+
* // 2. Apply to parent channel - automatically used for ALL child channels
41+
* ManagedChannel channel = ManagedChannelBuilder
42+
* .forTarget("xds:///my-service")
43+
* .childChannelConfigurer(configurer)
44+
* .build();
45+
* }</pre>
46+
*
47+
* <p>Implementations must be thread-safe as {@link #accept} may be invoked concurrently
48+
* by multiple internal components.
49+
*
50+
* @since 1.79.0
51+
*/
52+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
53+
@FunctionalInterface
54+
public interface ChildChannelConfigurer extends Consumer<ManagedChannelBuilder<?>> {
55+
56+
/**
57+
* Configures a builder for a new child channel.
58+
*
59+
* <p>This method is invoked synchronously during the creation of the child channel,
60+
* before {@link ManagedChannelBuilder#build()} is called.
61+
*
62+
* <p>Note: The provided {@code builder} is generic (`?`). Implementations should use
63+
* universal configuration methods (like {@code intercept()}, {@code userAgent()}) rather
64+
* than casting to specific implementation types.
65+
*
66+
* @param builder the mutable channel builder for the new child channel
67+
*/
68+
@Override
69+
void accept(ManagedChannelBuilder<?> builder);
70+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2025 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import java.util.logging.Level;
22+
import java.util.logging.Logger;
23+
24+
/**
25+
* Utilities for working with {@link ChildChannelConfigurer}.
26+
*
27+
* @since 1.79.0
28+
*/
29+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
30+
public final class ChildChannelConfigurers {
31+
private static final Logger logger = Logger.getLogger(ChildChannelConfigurers.class.getName());
32+
33+
// Singleton no-op instance to avoid object churn
34+
private static final ChildChannelConfigurer NO_OP = builder -> {
35+
};
36+
37+
private ChildChannelConfigurers() { // Prevent instantiation
38+
}
39+
40+
/**
41+
* Returns a configurer that does nothing.
42+
* Useful as a default value to avoid null checks in internal code.
43+
*/
44+
public static ChildChannelConfigurer noOp() {
45+
return NO_OP;
46+
}
47+
48+
/**
49+
* Returns a configurer that applies all the given configurers in sequence.
50+
*
51+
* <p>If any configurer in the chain throws an exception, the remaining ones are skipped
52+
* (unless wrapped in {@link #safe(ChildChannelConfigurer)}).
53+
*
54+
* @param configurers the configurers to apply in order. Null elements are ignored.
55+
*/
56+
public static ChildChannelConfigurer compose(ChildChannelConfigurer... configurers) {
57+
checkNotNull(configurers, "configurers");
58+
return builder -> {
59+
for (ChildChannelConfigurer configurer : configurers) {
60+
if (configurer != null) {
61+
configurer.accept(builder);
62+
}
63+
}
64+
};
65+
}
66+
67+
/**
68+
* Returns a configurer that applies the delegate but catches and logs any exceptions.
69+
*
70+
* <p>This prevents a buggy configurer (e.g., one that fails metric setup) from crashing
71+
* the critical path of channel creation.
72+
*
73+
* @param delegate the configurer to wrap.
74+
*/
75+
public static ChildChannelConfigurer safe(ChildChannelConfigurer delegate) {
76+
checkNotNull(delegate, "delegate");
77+
return builder -> {
78+
try {
79+
delegate.accept(builder);
80+
} catch (Exception e) {
81+
logger.log(Level.WARNING, "Failed to apply child channel configuration", e);
82+
}
83+
};
84+
}
85+
86+
/**
87+
* Returns a configurer that applies the delegate only if the given condition is true.
88+
*
89+
* <p>Useful for applying interceptors only in specific environments (e.g., Debug/Test).
90+
*
91+
* @param condition true to apply the delegate, false to do nothing.
92+
* @param delegate the configurer to apply if condition is true.
93+
*/
94+
public static ChildChannelConfigurer conditional(boolean condition,
95+
ChildChannelConfigurer delegate) {
96+
checkNotNull(delegate, "delegate");
97+
return condition ? delegate : NO_OP;
98+
}
99+
}

api/src/main/java/io/grpc/ForwardingChannelBuilder.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,18 @@ public T disableServiceConfigLookUp() {
242242
return thisT();
243243
}
244244

245+
@Override
246+
public T configureChannel(ManagedChannel parentChannel) {
247+
delegate().configureChannel(parentChannel);
248+
return thisT();
249+
}
250+
251+
@Override
252+
public T childChannelConfigurer(ChildChannelConfigurer childChannelConfigurer) {
253+
delegate().childChannelConfigurer(childChannelConfigurer);
254+
return thisT();
255+
}
256+
245257
/**
246258
* Returns the correctly typed version of the builder.
247259
*/

api/src/main/java/io/grpc/ForwardingChannelBuilder2.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,18 @@ public <X> T setNameResolverArg(NameResolver.Args.Key<X> key, X value) {
269269
return thisT();
270270
}
271271

272+
@Override
273+
public T configureChannel(ManagedChannel parentChannel) {
274+
delegate().configureChannel(parentChannel);
275+
return thisT();
276+
}
277+
278+
@Override
279+
public T childChannelConfigurer(ChildChannelConfigurer childChannelConfigurer) {
280+
delegate().childChannelConfigurer(childChannelConfigurer);
281+
return thisT();
282+
}
283+
272284
/**
273285
* Returns the {@link ManagedChannel} built by the delegate by default. Overriding method can
274286
* return different value.

api/src/main/java/io/grpc/ManagedChannel.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ public ConnectivityState getState(boolean requestConnection) {
8585
throw new UnsupportedOperationException("Not implemented");
8686
}
8787

88+
/**
89+
* Returns the configurer for child channels.
90+
*
91+
* <p>This method is intended for use by the internal gRPC infrastructure (specifically
92+
* load balancers and the channel builder) to propagate configuration to child channels.
93+
* Application code should not call this method.
94+
*
95+
* @return the configurer, or {@code null} if none is set.
96+
* @since 1.79.0
97+
*/
98+
@Internal
99+
public ChildChannelConfigurer getChildChannelConfigurer() {
100+
// Return null by default so we don't break existing custom ManagedChannel implementations
101+
// (like wrappers or mocks) that don't override this method.
102+
return null;
103+
}
104+
88105
/**
89106
* Registers a one-off callback that will be run if the connectivity state of the channel diverges
90107
* from the given {@code source}, which is typically what has just been returned by {@link

api/src/main/java/io/grpc/ManagedChannelBuilder.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,41 @@ public <X> T setNameResolverArg(NameResolver.Args.Key<X> key, X value) {
661661
throw new UnsupportedOperationException();
662662
}
663663

664+
/**
665+
* Configures this builder using settings derived from an existing parent channel.
666+
*
667+
* <p>This method is typically used by internal components (like LoadBalancers) when creating
668+
* child channels to ensure they inherit relevant configuration (like the
669+
* {@link ChildChannelConfigurer}) from the parent.
670+
*
671+
* <p>The specific settings copied are implementation dependent, but typically include
672+
* the child channel configurer and potentially user agents or offload executors.
673+
*
674+
* @param parentChannel the channel to inherit configuration from
675+
* @return this
676+
* @since 1.79.0
677+
*/
678+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
679+
public T configureChannel(ManagedChannel parentChannel) {
680+
throw new UnsupportedOperationException();
681+
}
682+
683+
/**
684+
* Sets a configurer that will be applied to all internal child channels created by this channel.
685+
*
686+
* <p>This allows injecting configuration (like credentials, interceptors, or flow control)
687+
* into auxiliary channels created by gRPC infrastructure, such as xDS control plane connections
688+
* or OOB load balancing channels.
689+
*
690+
* @param childChannelConfigurer the configurer to apply.
691+
* @return this
692+
* @since 1.79.0
693+
*/
694+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
695+
public T childChannelConfigurer(ChildChannelConfigurer childChannelConfigurer) {
696+
throw new UnsupportedOperationException("Not implemented");
697+
}
698+
664699
/**
665700
* Builds a channel using the given parameters.
666701
*

api/src/main/java/io/grpc/MetricRecorder.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@
2626
*/
2727
@Internal
2828
public interface MetricRecorder {
29+
30+
/**
31+
* Returns a {@link MetricRecorder} that performs no operations.
32+
* The returned instance ignores all calls and skips all validation checks.
33+
*/
34+
static MetricRecorder noOp() {
35+
return NoOpMetricRecorder.INSTANCE;
36+
}
37+
2938
/**
3039
* Adds a value for a double-precision counter metric instrument.
3140
*
@@ -176,4 +185,47 @@ interface Registration extends AutoCloseable {
176185
@Override
177186
void close();
178187
}
188+
189+
/**
190+
* No-Op implementation of MetricRecorder.
191+
* Overrides all default methods to skip validation checks for maximum performance.
192+
*/
193+
final class NoOpMetricRecorder implements MetricRecorder {
194+
private static final NoOpMetricRecorder INSTANCE = new NoOpMetricRecorder();
195+
196+
@Override
197+
public void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, double value,
198+
List<String> requiredLabelValues,
199+
List<String> optionalLabelValues) {
200+
}
201+
202+
@Override
203+
public void addLongCounter(LongCounterMetricInstrument metricInstrument, long value,
204+
List<String> requiredLabelValues, List<String> optionalLabelValues) {
205+
}
206+
207+
@Override
208+
public void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value,
209+
List<String> requiredLabelValues,
210+
List<String> optionalLabelValues) {
211+
}
212+
213+
@Override
214+
public void recordDoubleHistogram(DoubleHistogramMetricInstrument metricInstrument,
215+
double value, List<String> requiredLabelValues,
216+
List<String> optionalLabelValues) {
217+
}
218+
219+
@Override
220+
public void recordLongHistogram(LongHistogramMetricInstrument metricInstrument, long value,
221+
List<String> requiredLabelValues,
222+
List<String> optionalLabelValues) {
223+
}
224+
225+
@Override
226+
public Registration registerBatchCallback(BatchCallback callback,
227+
CallbackMetricInstrument... metricInstruments) {
228+
return () -> { };
229+
}
230+
}
179231
}

api/src/main/java/io/grpc/NameResolver.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ public static final class Args {
323323
@Nullable private final MetricRecorder metricRecorder;
324324
@Nullable private final NameResolverRegistry nameResolverRegistry;
325325
@Nullable private final IdentityHashMap<Key<?>, Object> customArgs;
326+
@Nullable private final ManagedChannel parentChannel;
326327

327328
private Args(Builder builder) {
328329
this.defaultPort = checkNotNull(builder.defaultPort, "defaultPort not set");
@@ -337,6 +338,7 @@ private Args(Builder builder) {
337338
this.metricRecorder = builder.metricRecorder;
338339
this.nameResolverRegistry = builder.nameResolverRegistry;
339340
this.customArgs = cloneCustomArgs(builder.customArgs);
341+
this.parentChannel = builder.parentChannel;
340342
}
341343

342344
/**
@@ -435,6 +437,14 @@ public ChannelLogger getChannelLogger() {
435437
return channelLogger;
436438
}
437439

440+
/**
441+
* Returns the parent {@link ManagedChannel} served by this NameResolver.
442+
*/
443+
@Internal
444+
public ManagedChannel getParentChannel() {
445+
return parentChannel;
446+
}
447+
438448
/**
439449
* Returns the Executor on which this resolver should execute long-running or I/O bound work.
440450
* Null if no Executor was set.
@@ -544,6 +554,7 @@ public static final class Builder {
544554
private MetricRecorder metricRecorder;
545555
private NameResolverRegistry nameResolverRegistry;
546556
private IdentityHashMap<Key<?>, Object> customArgs;
557+
private ManagedChannel parentChannel;
547558

548559
Builder() {
549560
}
@@ -659,6 +670,16 @@ public Builder setNameResolverRegistry(NameResolverRegistry registry) {
659670
return this;
660671
}
661672

673+
/**
674+
* See {@link Args#parentChannel}. This is an optional field.
675+
*
676+
* @since 1.79.0
677+
*/
678+
public Builder setParentChannel(ManagedChannel parentChannel) {
679+
this.parentChannel = parentChannel;
680+
return this;
681+
}
682+
662683
/**
663684
* Builds an {@link Args}.
664685
*

0 commit comments

Comments
 (0)