Skip to content

Commit 89aef90

Browse files
authored
enable child channel plugins (grpc#12578)
Implements grpc/proposal#529
1 parent fd971d1 commit 89aef90

34 files changed

Lines changed: 1214 additions & 33 deletions
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2026 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+
20+
21+
/**
22+
* A configurator 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 configurator
35+
* ChannelConfigurator configurator = builder -> {
36+
* builder.maxInboundMessageSize(4 * 1024 * 1024);
37+
* };
38+
*
39+
* // 2. Apply to parent channel - automatically used for ALL child channels
40+
* ManagedChannel channel = ManagedChannelBuilder
41+
* .forTarget("xds:///my-service")
42+
* .childChannelConfigurator(configurator)
43+
* .build();
44+
* }</pre>
45+
*
46+
* <p>Implementations must be thread-safe as the configure methods may be invoked concurrently
47+
* by multiple internal components.
48+
*
49+
* @since 1.83.0
50+
*/
51+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
52+
@FunctionalInterface
53+
public interface ChannelConfigurator {
54+
55+
/**
56+
* Configures a builder for a new child channel.
57+
*
58+
* <p>This method is invoked synchronously during the creation of the child channel,
59+
* before {@link ManagedChannelBuilder#build()} is called.
60+
*
61+
* <p><strong>Note:</strong> Implementations must only apply configurations to the
62+
* provided builder and must NOT call {@code builder.build()} themselves.
63+
*
64+
* <p><strong>Note:</strong> The provided {@code builder} is generic ({@code ?}). Implementations
65+
* should use universal configuration methods (like {@code intercept()}, {@code userAgent()})
66+
* on the builder rather than casting it to specific implementation types.
67+
*
68+
* @param builder the mutable channel builder for the new child channel
69+
*/
70+
void configureChannelBuilder(ManagedChannelBuilder<?> builder);
71+
}

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

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

245+
246+
@Override
247+
public T childChannelConfigurator(ChannelConfigurator channelConfigurator) {
248+
delegate().childChannelConfigurator(channelConfigurator);
249+
return thisT();
250+
}
251+
245252
/**
246253
* Returns the correctly typed version of the builder.
247254
*/

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

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

272+
273+
@Override
274+
public T childChannelConfigurator(ChannelConfigurator channelConfigurator) {
275+
delegate().childChannelConfigurator(channelConfigurator);
276+
return thisT();
277+
}
278+
272279
/**
273280
* Returns the {@link ManagedChannel} built by the delegate by default. Overriding method can
274281
* return different value.

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,23 @@ public <X> T setNameResolverArg(NameResolver.Args.Key<X> key, X value) {
666666
throw new UnsupportedOperationException();
667667
}
668668

669+
670+
/**
671+
* Sets a configurator that will be applied to all internal child channels created by this
672+
* channel.
673+
*
674+
* <p>This allows injecting universal configuration (like interceptors)
675+
* into auxiliary channels created by gRPC infrastructure, such as xDS control plane connections.
676+
*
677+
* @param channelConfigurator the configurator to apply.
678+
* @return this
679+
* @since 1.83.0
680+
*/
681+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
682+
public T childChannelConfigurator(ChannelConfigurator channelConfigurator) {
683+
throw new UnsupportedOperationException("Not implemented");
684+
}
685+
669686
/**
670687
* Builds a channel using the given parameters.
671688
*

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ public static final class Args {
358358
private final MetricRecorder metricRecorder;
359359
@Nullable private final NameResolverRegistry nameResolverRegistry;
360360
@Nullable private final IdentityHashMap<Key<?>, Object> customArgs;
361+
private final ChannelConfigurator channelConfigurator;
361362

362363
private Args(Builder builder) {
363364
this.defaultPort = checkNotNull(builder.defaultPort, "defaultPort not set");
@@ -373,6 +374,7 @@ private Args(Builder builder) {
373374
: new MetricRecorder() {};
374375
this.nameResolverRegistry = builder.nameResolverRegistry;
375376
this.customArgs = cloneCustomArgs(builder.customArgs);
377+
this.channelConfigurator = builder.channelConfigurator;
376378
}
377379

378380
/**
@@ -471,6 +473,16 @@ public ChannelLogger getChannelLogger() {
471473
return channelLogger;
472474
}
473475

476+
/**
477+
* Returns the configurator for child channels.
478+
*
479+
* @since 1.83.0
480+
*/
481+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
482+
public ChannelConfigurator getChildChannelConfigurator() {
483+
return channelConfigurator;
484+
}
485+
474486
/**
475487
* Returns the Executor on which this resolver should execute long-running or I/O bound work.
476488
* Null if no Executor was set.
@@ -549,6 +561,7 @@ public Builder toBuilder() {
549561
builder.setOverrideAuthority(overrideAuthority);
550562
builder.setMetricRecorder(metricRecorder);
551563
builder.setNameResolverRegistry(nameResolverRegistry);
564+
builder.setChildChannelConfigurator(channelConfigurator);
552565
builder.customArgs = cloneCustomArgs(customArgs);
553566
return builder;
554567
}
@@ -579,6 +592,7 @@ public static final class Builder {
579592
private MetricRecorder metricRecorder;
580593
private NameResolverRegistry nameResolverRegistry;
581594
private IdentityHashMap<Key<?>, Object> customArgs;
595+
private ChannelConfigurator channelConfigurator = builder -> { };
582596

583597
Builder() {
584598
}
@@ -694,6 +708,17 @@ public Builder setNameResolverRegistry(NameResolverRegistry registry) {
694708
return this;
695709
}
696710

711+
/**
712+
* See {@link Args#getChildChannelConfigurator()}. This is an optional field.
713+
*
714+
* @since 1.83.0
715+
*/
716+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
717+
public Builder setChildChannelConfigurator(ChannelConfigurator channelConfigurator) {
718+
this.channelConfigurator = checkNotNull(channelConfigurator, "channelConfigurator");
719+
return this;
720+
}
721+
697722
/**
698723
* Builds an {@link Args}.
699724
*

api/src/test/java/io/grpc/NameResolverTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.mockito.Mockito.verify;
2323

2424
import com.google.common.base.Objects;
25+
import io.grpc.ChannelConfigurator;
2526
import io.grpc.NameResolver.ConfigOrError;
2627
import io.grpc.NameResolver.Listener2;
2728
import io.grpc.NameResolver.ResolutionResult;
@@ -72,6 +73,8 @@ public class NameResolverTest {
7273
private final int customArgValue = 42;
7374
@Mock NameResolver.Listener mockListener;
7475

76+
private final ChannelConfigurator channelConfigurator = builder -> { };
77+
7578
@Test
7679
public void args() {
7780
NameResolver.Args args = createArgs();
@@ -84,6 +87,7 @@ public void args() {
8487
assertThat(args.getOffloadExecutor()).isSameInstanceAs(executor);
8588
assertThat(args.getOverrideAuthority()).isSameInstanceAs(overrideAuthority);
8689
assertThat(args.getMetricRecorder()).isSameInstanceAs(metricRecorder);
90+
assertThat(args.getChildChannelConfigurator()).isSameInstanceAs(channelConfigurator);
8791
assertThat(args.getArg(FOO_ARG_KEY)).isEqualTo(customArgValue);
8892
assertThat(args.getArg(BAR_ARG_KEY)).isNull();
8993

@@ -97,6 +101,7 @@ public void args() {
97101
assertThat(args2.getOffloadExecutor()).isSameInstanceAs(executor);
98102
assertThat(args2.getOverrideAuthority()).isSameInstanceAs(overrideAuthority);
99103
assertThat(args.getMetricRecorder()).isSameInstanceAs(metricRecorder);
104+
assertThat(args2.getChildChannelConfigurator()).isSameInstanceAs(channelConfigurator);
100105
assertThat(args.getArg(FOO_ARG_KEY)).isEqualTo(customArgValue);
101106
assertThat(args.getArg(BAR_ARG_KEY)).isNull();
102107

@@ -115,10 +120,47 @@ private NameResolver.Args createArgs() {
115120
.setOffloadExecutor(executor)
116121
.setOverrideAuthority(overrideAuthority)
117122
.setMetricRecorder(metricRecorder)
123+
.setChildChannelConfigurator(channelConfigurator)
118124
.setArg(FOO_ARG_KEY, customArgValue)
119125
.build();
120126
}
121127

128+
@Test
129+
public void args_childChannelConfigurator() {
130+
final ManagedChannelBuilder<?>[] capturedBuilder = new ManagedChannelBuilder<?>[1];
131+
ChannelConfigurator channelConfigurator = new ChannelConfigurator() {
132+
@Override
133+
public void configureChannelBuilder(ManagedChannelBuilder<?> builder) {
134+
capturedBuilder[0] = builder;
135+
}
136+
};
137+
138+
SynchronizationContext realSyncContext = new SynchronizationContext(
139+
new Thread.UncaughtExceptionHandler() {
140+
@Override
141+
public void uncaughtException(Thread t, Throwable e) {
142+
throw new AssertionError(e);
143+
}
144+
});
145+
146+
NameResolver.Args args = NameResolver.Args.newBuilder()
147+
.setDefaultPort(8080)
148+
.setProxyDetector(mock(ProxyDetector.class))
149+
.setSynchronizationContext(realSyncContext)
150+
.setServiceConfigParser(mock(NameResolver.ServiceConfigParser.class))
151+
.setChannelLogger(mock(ChannelLogger.class))
152+
.setChildChannelConfigurator(channelConfigurator)
153+
.build();
154+
155+
ChannelConfigurator configurator = args.getChildChannelConfigurator();
156+
assertThat(configurator).isSameInstanceAs(channelConfigurator);
157+
158+
// Validate configurator accepts builders
159+
ManagedChannelBuilder<?> mockBuilder = mock(ManagedChannelBuilder.class);
160+
configurator.configureChannelBuilder(mockBuilder);
161+
assertThat(capturedBuilder[0]).isSameInstanceAs(mockBuilder);
162+
}
163+
122164
@Test
123165
@SuppressWarnings("deprecation")
124166
public void startOnOldListener_wrapperListener2UsedToStart() {

core/src/main/java/io/grpc/internal/ManagedChannelImpl.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.grpc.CallCredentials;
3838
import io.grpc.CallOptions;
3939
import io.grpc.Channel;
40+
import io.grpc.ChannelConfigurator;
4041
import io.grpc.ChannelCredentials;
4142
import io.grpc.ChannelLogger;
4243
import io.grpc.ChannelLogger.ChannelLogLevel;
@@ -155,6 +156,14 @@ public Result selectConfig(PickSubchannelArgs args) {
155156
private static final LoadBalancer.PickDetailsConsumer NOOP_PICK_DETAILS_CONSUMER =
156157
new LoadBalancer.PickDetailsConsumer() {};
157158

159+
/**
160+
* Retrieves the user-provided configuration function for internal child channels.
161+
*
162+
* <p>This is intended for use by gRPC internal components
163+
* that are responsible for creating auxiliary {@code ManagedChannel} instances.
164+
*/
165+
private final ChannelConfigurator channelConfigurator;
166+
158167
private final InternalLogId logId;
159168
private final String target;
160169
@Nullable
@@ -545,6 +554,8 @@ ClientStream newSubstream(
545554
Supplier<Stopwatch> stopwatchSupplier,
546555
List<ClientInterceptor> interceptors,
547556
final TimeProvider timeProvider) {
557+
this.channelConfigurator = checkNotNull(builder.channelConfigurator,
558+
"channelConfigurator");
548559
this.target = checkNotNull(builder.target, "target");
549560
this.logId = InternalLogId.allocate("Channel", target);
550561
this.timeProvider = checkNotNull(timeProvider, "timeProvider");
@@ -589,7 +600,8 @@ ClientStream newSubstream(
589600
.setOffloadExecutor(this.offloadExecutorHolder)
590601
.setOverrideAuthority(this.authorityOverride)
591602
.setMetricRecorder(this.metricRecorder)
592-
.setNameResolverRegistry(builder.nameResolverRegistry);
603+
.setNameResolverRegistry(builder.nameResolverRegistry)
604+
.setChildChannelConfigurator(this.channelConfigurator);
593605
builder.copyAllNameResolverCustomArgsTo(nameResolverArgsBuilder);
594606
this.nameResolverArgs = nameResolverArgsBuilder.build();
595607
this.nameResolver = getNameResolver(
@@ -1489,6 +1501,11 @@ protected ManagedChannelBuilder<?> delegate() {
14891501

14901502
ResolvingOobChannelBuilder builder = new ResolvingOobChannelBuilder();
14911503

1504+
// Note that we follow the global configurator pattern and try to fuse the configurations as
1505+
// soon as the builder gets created
1506+
channelConfigurator.configureChannelBuilder(builder);
1507+
builder.childChannelConfigurator(channelConfigurator);
1508+
14921509
return builder
14931510
// TODO(zdapeng): executors should not outlive the parent channel.
14941511
.executor(executor)

core/src/main/java/io/grpc/internal/ManagedChannelImplBuilder.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.grpc.CallCredentials;
3030
import io.grpc.CallOptions;
3131
import io.grpc.Channel;
32+
import io.grpc.ChannelConfigurator;
3233
import io.grpc.ChannelCredentials;
3334
import io.grpc.ClientCall;
3435
import io.grpc.ClientInterceptor;
@@ -149,6 +150,8 @@ public static ManagedChannelBuilder<?> forTarget(String target) {
149150
}
150151

151152

153+
ChannelConfigurator channelConfigurator = builder -> { };
154+
152155
ObjectPool<? extends Executor> executorPool = DEFAULT_EXECUTOR_POOL;
153156

154157
ObjectPool<? extends Executor> offloadExecutorPool = DEFAULT_EXECUTOR_POOL;
@@ -756,6 +759,14 @@ protected ManagedChannelImplBuilder addMetricSink(MetricSink metricSink) {
756759
return this;
757760
}
758761

762+
@Override
763+
public ManagedChannelImplBuilder childChannelConfigurator(
764+
ChannelConfigurator channelConfigurator) {
765+
this.channelConfigurator = checkNotNull(channelConfigurator,
766+
"childChannelConfigurator");
767+
return this;
768+
}
769+
759770
@Override
760771
public ManagedChannel build() {
761772
ClientTransportFactory clientTransportFactory =

0 commit comments

Comments
 (0)