Skip to content
73 changes: 73 additions & 0 deletions api/src/main/java/io/grpc/ChannelConfigurer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc;



/**
* A configurer for child channels created by gRPC's internal infrastructure.
*
* <p>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:
* <ul>
* <li>xDS control plane connections</li>
* <li>Load Balancing helper channels (OOB channels)</li>
* </ul>
*
* <p><strong>Usage Example:</strong>
* <pre>{@code
* // 1. Define the configurer
* ChannelConfigurer configurer = builder -> {
* 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();
* }</pre>
*
* <p>Implementations must be thread-safe as the configure methods may be invoked concurrently
* by multiple internal components.
*
* @since 1.81.0
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
public interface ChannelConfigurer {

/**
* Configures a builder for a new child channel.
*
* <p>This method is invoked synchronously during the creation of the child channel,
* before {@link ManagedChannelBuilder#build()} is called.
*
* @param builder the mutable channel builder for the new child channel
*/
default void configureChannelBuilder(ManagedChannelBuilder<?> builder) {}

/**
* Configures a builder for a new child server.
*
* <p>This method is invoked synchronously during the creation of the child server,
* before {@link ServerBuilder#build()} is called.
*
* @param builder the mutable server builder for the new child server
*/
default void configureServerBuilder(ServerBuilder<?> builder) {}
}
7 changes: 7 additions & 0 deletions api/src/main/java/io/grpc/ForwardingChannelBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ public T disableServiceConfigLookUp() {
return thisT();
}


@Override
public T childChannelConfigurer(ChannelConfigurer channelConfigurer) {
delegate().childChannelConfigurer(channelConfigurer);
return thisT();
}

/**
* Returns the correctly typed version of the builder.
*/
Expand Down
9 changes: 8 additions & 1 deletion api/src/main/java/io/grpc/ForwardingChannelBuilder2.java
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public T disableServiceConfigLookUp() {
}

@Override
protected T addMetricSink(MetricSink metricSink) {
public T addMetricSink(MetricSink metricSink) {
delegate().addMetricSink(metricSink);
return thisT();
}
Expand All @@ -269,6 +269,13 @@ public <X> T setNameResolverArg(NameResolver.Args.Key<X> key, X value) {
return thisT();
}


@Override
public T childChannelConfigurer(ChannelConfigurer channelConfigurer) {
delegate().childChannelConfigurer(channelConfigurer);
return thisT();
}

/**
* Returns the {@link ManagedChannel} built by the delegate by default. Overriding method can
* return different value.
Expand Down
6 changes: 6 additions & 0 deletions api/src/main/java/io/grpc/ForwardingServerBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ public T setBinaryLog(BinaryLog binaryLog) {
return thisT();
}

@Override
public T childChannelConfigurer(ChannelConfigurer channelConfigurer) {
delegate().childChannelConfigurer(channelConfigurer);
return thisT();
}

/**
* Returns the {@link Server} built by the delegate by default. Overriding method can return
* different value.
Expand Down
23 changes: 18 additions & 5 deletions api/src/main/java/io/grpc/ManagedChannelBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,7 @@ protected T interceptWithTarget(InterceptorFactory factory) {
throw new UnsupportedOperationException();
}

/** Internal-only. */
@Internal
protected interface InterceptorFactory {
public interface InterceptorFactory {
ClientInterceptor newInterceptor(String target);
}

Expand Down Expand Up @@ -638,8 +636,7 @@ public T disableServiceConfigLookUp() {
* @return this
* @since 1.64.0
*/
@Internal
protected T addMetricSink(MetricSink metricSink) {
public T addMetricSink(MetricSink metricSink) {
throw new UnsupportedOperationException();
}

Expand All @@ -661,6 +658,22 @@ public <X> T setNameResolverArg(NameResolver.Args.Key<X> key, X value) {
throw new UnsupportedOperationException();
}


/**
* Sets a configurer that will be applied to all internal child channels created by this channel.
*
* <p>This allows injecting configuration (like credentials, interceptors, or flow control)
* into auxiliary channels created by gRPC infrastructure, such as xDS control plane connections.
*
* @param channelConfigurer the configurer to apply.
* @return this
* @since 1.81.0
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
public T childChannelConfigurer(ChannelConfigurer channelConfigurer) {
throw new UnsupportedOperationException("Not implemented");
}

/**
* Builds a channel using the given parameters.
*
Expand Down
52 changes: 52 additions & 0 deletions api/src/main/java/io/grpc/MetricRecorder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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<String> requiredLabelValues,
List<String> optionalLabelValues) {
}

@Override
public void addLongCounter(LongCounterMetricInstrument metricInstrument, long value,
List<String> requiredLabelValues, List<String> optionalLabelValues) {
}

@Override
public void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value,
List<String> requiredLabelValues,
List<String> optionalLabelValues) {
}

@Override
public void recordDoubleHistogram(DoubleHistogramMetricInstrument metricInstrument,
double value, List<String> requiredLabelValues,
List<String> optionalLabelValues) {
}

@Override
public void recordLongHistogram(LongHistogramMetricInstrument metricInstrument, long value,
List<String> requiredLabelValues,
List<String> optionalLabelValues) {
}

@Override
public Registration registerBatchCallback(BatchCallback callback,
CallbackMetricInstrument... metricInstruments) {
return () -> { };
}
}
}
24 changes: 24 additions & 0 deletions api/src/main/java/io/grpc/NameResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ public static final class Args {
@Nullable private final MetricRecorder metricRecorder;
@Nullable private final NameResolverRegistry nameResolverRegistry;
@Nullable private final IdentityHashMap<Key<?>, Object> customArgs;
@Nullable private final ChannelConfigurer channelConfigurer;

private Args(Builder builder) {
this.defaultPort = checkNotNull(builder.defaultPort, "defaultPort not set");
Expand All @@ -372,6 +373,7 @@ private Args(Builder builder) {
this.metricRecorder = builder.metricRecorder;
this.nameResolverRegistry = builder.nameResolverRegistry;
this.customArgs = cloneCustomArgs(builder.customArgs);
this.channelConfigurer = builder.channelConfigurer;
}

/**
Expand Down Expand Up @@ -470,6 +472,17 @@ public ChannelLogger getChannelLogger() {
return channelLogger;
}

/**
* Returns the configurer for child channels.
*
* @since 1.81.0
*/
@Nullable
@Internal
public ChannelConfigurer getChildChannelConfigurer() {
return channelConfigurer;
}

/**
* Returns the Executor on which this resolver should execute long-running or I/O bound work.
* Null if no Executor was set.
Expand Down Expand Up @@ -579,6 +592,7 @@ public static final class Builder {
private MetricRecorder metricRecorder;
private NameResolverRegistry nameResolverRegistry;
private IdentityHashMap<Key<?>, Object> customArgs;
private ChannelConfigurer channelConfigurer = new ChannelConfigurer() {};

Builder() {
}
Expand Down Expand Up @@ -694,6 +708,16 @@ public Builder setNameResolverRegistry(NameResolverRegistry registry) {
return this;
}

/**
* See {@link Args#getChildChannelConfigurer()}. This is an optional field.
*
* @since 1.81.0
*/
public Builder setChildChannelConfigurer(ChannelConfigurer channelConfigurer) {
this.channelConfigurer = channelConfigurer;
return this;
}

/**
* Builds an {@link Args}.
*
Expand Down
1 change: 1 addition & 0 deletions api/src/main/java/io/grpc/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,5 @@ public List<ServerServiceDefinition> getMutableServices() {
* @since 1.0.0
*/
public abstract void awaitTermination() throws InterruptedException;

}
18 changes: 18 additions & 0 deletions api/src/main/java/io/grpc/ServerBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,24 @@ public T setBinaryLog(BinaryLog binaryLog) {
throw new UnsupportedOperationException();
}


/**
* Sets a configurer that will be applied to all internal child channels created by this server.
*
* <p>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 channelConfigurer the configurer to apply.
* @return this
* @since 1.81.0
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12574")
public T childChannelConfigurer(ChannelConfigurer channelConfigurer) {
throw new UnsupportedOperationException("Not implemented");
}


/**
* Builds a server using the given parameters.
*
Expand Down
41 changes: 41 additions & 0 deletions api/src/test/java/io/grpc/ChannelConfigurerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc;

import static org.mockito.Mockito.mock;
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 ChannelConfigurerTest {

@Test
public void defaultMethods_doNothing() {
ChannelConfigurer configurer = new ChannelConfigurer() {};

ManagedChannelBuilder<?> mockChannelBuilder = mock(ManagedChannelBuilder.class);
configurer.configureChannelBuilder(mockChannelBuilder);
verifyNoInteractions(mockChannelBuilder);

ServerBuilder<?> mockServerBuilder = mock(ServerBuilder.class);
configurer.configureServerBuilder(mockServerBuilder);
verifyNoInteractions(mockServerBuilder);
}
}
Loading
Loading