From 7c45aac5efa86584d0dc3264de0efd5be69fc612 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 20 May 2025 07:19:41 +0000 Subject: [PATCH 1/5] Add Docker fiels for xds example server and client. --- examples/example-xds/xds-client.Dockerfile | 47 ++++++++++++++++++++++ examples/example-xds/xds-server.Dockerfile | 47 ++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 examples/example-xds/xds-client.Dockerfile create mode 100644 examples/example-xds/xds-server.Dockerfile diff --git a/examples/example-xds/xds-client.Dockerfile b/examples/example-xds/xds-client.Dockerfile new file mode 100644 index 00000000000..0f34d219177 --- /dev/null +++ b/examples/example-xds/xds-client.Dockerfile @@ -0,0 +1,47 @@ +# Copyright 2024 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. + +# +# Stage 1: Build XDS client +# + +FROM eclipse-temurin:11-jdk AS build + +WORKDIR /grpc-java/examples +COPY . . + +RUN cd example-xds && ../gradlew installDist -PskipCodegen=true -PskipAndroid=true + +# +# Stage 2: +# +# - Copy only the necessary files to reduce Docker image size. +# - Have an ENTRYPOINT script which will launch the XDS client +# with the given parameters. +# + +FROM eclipse-temurin:11-jre + +WORKDIR /grpc-java/ +COPY --from=build /grpc-java/examples/example-xds/build/install/example-xds/. . + +# Intentionally after the COPY to force the update on each build. +# Update Ubuntu system packages: +RUN apt-get update \ + && apt-get -y upgrade \ + && apt-get -y autoremove \ + && rm -rf /var/lib/apt/lists/* + +# Client +ENTRYPOINT ["bin/xds-hello-world-client"] diff --git a/examples/example-xds/xds-server.Dockerfile b/examples/example-xds/xds-server.Dockerfile new file mode 100644 index 00000000000..542fb0263af --- /dev/null +++ b/examples/example-xds/xds-server.Dockerfile @@ -0,0 +1,47 @@ +# Copyright 2024 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. + +# +# Stage 1: Build XDS server +# + +FROM eclipse-temurin:11-jdk AS build + +WORKDIR /grpc-java/examples +COPY . . + +RUN cd example-xds && ../gradlew installDist -PskipCodegen=true -PskipAndroid=true + +# +# Stage 2: +# +# - Copy only the necessary files to reduce Docker image size. +# - Have an ENTRYPOINT script which will launch the XDS server +# with the given parameters. +# + +FROM eclipse-temurin:11-jre + +WORKDIR /grpc-java/ +COPY --from=build /grpc-java/examples/example-xds/build/install/example-xds/. . + +# Intentionally after the COPY to force the update on each build. +# Update Ubuntu system packages: +RUN apt-get update \ + && apt-get -y upgrade \ + && apt-get -y autoremove \ + && rm -rf /var/lib/apt/lists/* + +# Server +ENTRYPOINT ["bin/xds-hello-world-server"] From 2a8230abafb53097684630732e541c2e93a88d92 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 27 Mar 2026 09:36:00 +0000 Subject: [PATCH 2/5] Call executor safeguard for interceptor --- .../java/io/grpc/internal/CallExecutors.java | 52 +++++++++++++++++++ .../java/io/grpc/internal/ClientCallImpl.java | 9 +--- .../io/grpc/internal/ManagedChannelImpl.java | 18 +++++-- .../io/grpc/internal/SubchannelChannel.java | 7 ++- 4 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 core/src/main/java/io/grpc/internal/CallExecutors.java diff --git a/core/src/main/java/io/grpc/internal/CallExecutors.java b/core/src/main/java/io/grpc/internal/CallExecutors.java new file mode 100644 index 00000000000..9aa7a02f664 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/CallExecutors.java @@ -0,0 +1,52 @@ +/* + * 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.internal; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import java.util.concurrent.Executor; + +/** + * Common utilities for GRPC call executors. + */ +final class CallExecutors { + + private CallExecutors() {} + + /** + * Wraps an executor with safeguarding (serialization) if not already safeguarded. + */ + static Executor safeguard(Executor executor) { + if (executor instanceof SerializingExecutor + || executor instanceof SerializeReentrantCallsDirectExecutor) { + return executor; + } + if (executor == directExecutor()) { + return new SerializeReentrantCallsDirectExecutor(); + } + return new SerializingExecutor(executor); + } + + /** + * Returns true if the executor is safeguarded (e.g. a {@link SerializingExecutor} or + * {@link SerializeReentrantCallsDirectExecutor}). + */ + static boolean isSafeguarded(Executor executor) { + return executor instanceof SerializingExecutor + || executor instanceof SerializeReentrantCallsDirectExecutor; + } +} diff --git a/core/src/main/java/io/grpc/internal/ClientCallImpl.java b/core/src/main/java/io/grpc/internal/ClientCallImpl.java index 4b24b1eae3d..966d43f9ef0 100644 --- a/core/src/main/java/io/grpc/internal/ClientCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ClientCallImpl.java @@ -107,13 +107,8 @@ final class ClientCallImpl extends ClientCall { // If we know that the executor is a direct executor, we don't need to wrap it with a // SerializingExecutor. This is purely for performance reasons. // See https://github.com/grpc/grpc-java/issues/368 - if (executor == directExecutor()) { - this.callExecutor = new SerializeReentrantCallsDirectExecutor(); - callExecutorIsDirect = true; - } else { - this.callExecutor = new SerializingExecutor(executor); - callExecutorIsDirect = false; - } + this.callExecutor = CallExecutors.safeguard(executor); + callExecutorIsDirect = (this.callExecutor instanceof SerializeReentrantCallsDirectExecutor); this.channelCallsTracer = channelCallsTracer; // Propagate the context from the thread which initiated the call to all callbacks. this.context = Context.current(); diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index e423220e3ad..0197dd1177e 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -808,6 +808,13 @@ public boolean isTerminated() { @Override public ClientCall newCall(MethodDescriptor method, CallOptions callOptions) { + Executor executor = callOptions.getExecutor(); + if (executor == null) { + executor = this.executor; + } + // All calls on the channel should have a safeguarded executor in CallOptions before + // calling interceptors. + callOptions = callOptions.withExecutor(CallExecutors.safeguard(executor)); return interceptorChannel.newCall(method, callOptions); } @@ -821,7 +828,7 @@ private Executor getCallExecutor(CallOptions callOptions) { if (executor == null) { executor = this.executor; } - return executor; + return CallExecutors.safeguard(executor); } private class RealChannel extends Channel { @@ -1084,9 +1091,12 @@ static final class ConfigSelectingClientCall this.configSelector = configSelector; this.channel = channel; this.method = method; - this.callExecutor = - callOptions.getExecutor() == null ? channelExecutor : callOptions.getExecutor(); - this.callOptions = callOptions.withExecutor(callExecutor); + Executor executor = callOptions.getExecutor(); + if (executor == null) { + executor = channelExecutor; + } + this.callExecutor = CallExecutors.safeguard(executor); + this.callOptions = callOptions.withExecutor(this.callExecutor); this.context = Context.current(); } diff --git a/core/src/main/java/io/grpc/internal/SubchannelChannel.java b/core/src/main/java/io/grpc/internal/SubchannelChannel.java index ced4272afe3..777e903d8d7 100644 --- a/core/src/main/java/io/grpc/internal/SubchannelChannel.java +++ b/core/src/main/java/io/grpc/internal/SubchannelChannel.java @@ -85,8 +85,11 @@ public ClientStream newStream(MethodDescriptor method, @Override public ClientCall newCall( MethodDescriptor methodDescriptor, CallOptions callOptions) { - final Executor effectiveExecutor = - callOptions.getExecutor() == null ? executor : callOptions.getExecutor(); + Executor callExecutor = callOptions.getExecutor(); + if (callExecutor == null) { + callExecutor = this.executor; + } + final Executor effectiveExecutor = CallExecutors.safeguard(callExecutor); if (callOptions.isWaitForReady()) { return new ClientCall() { @Override From f07bcaef6bee3d6bad83e953ec9abdba6d718f84 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Fri, 27 Mar 2026 12:04:41 +0000 Subject: [PATCH 3/5] Remove trash --- .../java/io/grpc/internal/CallExecutors.java | 3 ++ .../java/io/grpc/internal/ClientCallImpl.java | 3 -- examples/example-xds/xds-client.Dockerfile | 47 ------------------- examples/example-xds/xds-server.Dockerfile | 47 ------------------- 4 files changed, 3 insertions(+), 97 deletions(-) delete mode 100644 examples/example-xds/xds-client.Dockerfile delete mode 100644 examples/example-xds/xds-server.Dockerfile diff --git a/core/src/main/java/io/grpc/internal/CallExecutors.java b/core/src/main/java/io/grpc/internal/CallExecutors.java index 9aa7a02f664..75760338191 100644 --- a/core/src/main/java/io/grpc/internal/CallExecutors.java +++ b/core/src/main/java/io/grpc/internal/CallExecutors.java @@ -31,6 +31,9 @@ private CallExecutors() {} * Wraps an executor with safeguarding (serialization) if not already safeguarded. */ static Executor safeguard(Executor executor) { + // If we know that the executor is a direct executor, we don't need to wrap it with a + // SerializingExecutor. This is purely for performance reasons. + // See https://github.com/grpc/grpc-java/issues/368 if (executor instanceof SerializingExecutor || executor instanceof SerializeReentrantCallsDirectExecutor) { return executor; diff --git a/core/src/main/java/io/grpc/internal/ClientCallImpl.java b/core/src/main/java/io/grpc/internal/ClientCallImpl.java index 966d43f9ef0..3debcae6403 100644 --- a/core/src/main/java/io/grpc/internal/ClientCallImpl.java +++ b/core/src/main/java/io/grpc/internal/ClientCallImpl.java @@ -104,9 +104,6 @@ final class ClientCallImpl extends ClientCall { this.method = method; // TODO(carl-mastrangelo): consider moving this construction to ManagedChannelImpl. this.tag = PerfMark.createTag(method.getFullMethodName(), System.identityHashCode(this)); - // If we know that the executor is a direct executor, we don't need to wrap it with a - // SerializingExecutor. This is purely for performance reasons. - // See https://github.com/grpc/grpc-java/issues/368 this.callExecutor = CallExecutors.safeguard(executor); callExecutorIsDirect = (this.callExecutor instanceof SerializeReentrantCallsDirectExecutor); this.channelCallsTracer = channelCallsTracer; diff --git a/examples/example-xds/xds-client.Dockerfile b/examples/example-xds/xds-client.Dockerfile deleted file mode 100644 index 0f34d219177..00000000000 --- a/examples/example-xds/xds-client.Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2024 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. - -# -# Stage 1: Build XDS client -# - -FROM eclipse-temurin:11-jdk AS build - -WORKDIR /grpc-java/examples -COPY . . - -RUN cd example-xds && ../gradlew installDist -PskipCodegen=true -PskipAndroid=true - -# -# Stage 2: -# -# - Copy only the necessary files to reduce Docker image size. -# - Have an ENTRYPOINT script which will launch the XDS client -# with the given parameters. -# - -FROM eclipse-temurin:11-jre - -WORKDIR /grpc-java/ -COPY --from=build /grpc-java/examples/example-xds/build/install/example-xds/. . - -# Intentionally after the COPY to force the update on each build. -# Update Ubuntu system packages: -RUN apt-get update \ - && apt-get -y upgrade \ - && apt-get -y autoremove \ - && rm -rf /var/lib/apt/lists/* - -# Client -ENTRYPOINT ["bin/xds-hello-world-client"] diff --git a/examples/example-xds/xds-server.Dockerfile b/examples/example-xds/xds-server.Dockerfile deleted file mode 100644 index 542fb0263af..00000000000 --- a/examples/example-xds/xds-server.Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2024 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. - -# -# Stage 1: Build XDS server -# - -FROM eclipse-temurin:11-jdk AS build - -WORKDIR /grpc-java/examples -COPY . . - -RUN cd example-xds && ../gradlew installDist -PskipCodegen=true -PskipAndroid=true - -# -# Stage 2: -# -# - Copy only the necessary files to reduce Docker image size. -# - Have an ENTRYPOINT script which will launch the XDS server -# with the given parameters. -# - -FROM eclipse-temurin:11-jre - -WORKDIR /grpc-java/ -COPY --from=build /grpc-java/examples/example-xds/build/install/example-xds/. . - -# Intentionally after the COPY to force the update on each build. -# Update Ubuntu system packages: -RUN apt-get update \ - && apt-get -y upgrade \ - && apt-get -y autoremove \ - && rm -rf /var/lib/apt/lists/* - -# Server -ENTRYPOINT ["bin/xds-hello-world-server"] From c1c952f01037faf1a4bc5d5da76f9a3f181e780a Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 30 Mar 2026 04:44:18 +0000 Subject: [PATCH 4/5] Fix unit test failure. --- .../src/main/java/io/grpc/internal/ManagedChannelImpl.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 0197dd1177e..0cb1e01cc65 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -808,6 +808,13 @@ public boolean isTerminated() { @Override public ClientCall newCall(MethodDescriptor method, CallOptions callOptions) { + // If we have no interceptors, we don't need to populate the executor in CallOptions + // yet. This avoids mutating CallOptions unnecessarily and breaking tests that + // expect exact instance equality. The executor will still be safeguarded when + // creating the actual ClientCallImpl. + if (interceptorChannel == realChannel) { + return realChannel.newCall(method, callOptions); + } Executor executor = callOptions.getExecutor(); if (executor == null) { executor = this.executor; From 594c4dc0a09146f80434c49282a805f27722f76c Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 30 Mar 2026 06:35:25 +0000 Subject: [PATCH 5/5] Unit test. --- .../java/io/grpc/internal/CallExecutors.java | 9 --- .../io/grpc/internal/CallExecutorsTest.java | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 core/src/test/java/io/grpc/internal/CallExecutorsTest.java diff --git a/core/src/main/java/io/grpc/internal/CallExecutors.java b/core/src/main/java/io/grpc/internal/CallExecutors.java index 75760338191..9a5493e4b01 100644 --- a/core/src/main/java/io/grpc/internal/CallExecutors.java +++ b/core/src/main/java/io/grpc/internal/CallExecutors.java @@ -43,13 +43,4 @@ static Executor safeguard(Executor executor) { } return new SerializingExecutor(executor); } - - /** - * Returns true if the executor is safeguarded (e.g. a {@link SerializingExecutor} or - * {@link SerializeReentrantCallsDirectExecutor}). - */ - static boolean isSafeguarded(Executor executor) { - return executor instanceof SerializingExecutor - || executor instanceof SerializeReentrantCallsDirectExecutor; - } } diff --git a/core/src/test/java/io/grpc/internal/CallExecutorsTest.java b/core/src/test/java/io/grpc/internal/CallExecutorsTest.java new file mode 100644 index 00000000000..ed26577c2e2 --- /dev/null +++ b/core/src/test/java/io/grpc/internal/CallExecutorsTest.java @@ -0,0 +1,68 @@ +/* + * 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.internal; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.Executor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CallExecutorsTest { + + @Test + public void safeguard_alreadySerializing_returnsSameInstance() { + Executor raw = command -> command.run(); + SerializingExecutor serializing = new SerializingExecutor(raw); + assertSame(serializing, CallExecutors.safeguard(serializing)); + } + + @Test + public void safeguard_alreadySerializeReentrantCallsDirect_returnsSameInstance() { + SerializeReentrantCallsDirectExecutor direct = new SerializeReentrantCallsDirectExecutor(); + assertSame(direct, CallExecutors.safeguard(direct)); + } + + @Test + public void safeguard_directExecutor_returnsSerializeReentrantCallsDirect() { + Executor safeguarded = CallExecutors.safeguard(directExecutor()); + assertTrue(safeguarded instanceof SerializeReentrantCallsDirectExecutor); + } + + @Test + public void safeguard_otherExecutor_returnsSerializing() { + Executor raw = command -> command.run(); + Executor safeguarded = CallExecutors.safeguard(raw); + assertTrue(safeguarded instanceof SerializingExecutor); + } + + @Test + public void safeguard_idempotent() { + Executor raw = command -> command.run(); + Executor first = CallExecutors.safeguard(raw); + Executor second = CallExecutors.safeguard(first); + assertSame(first, second); + + Executor firstDirect = CallExecutors.safeguard(directExecutor()); + Executor secondDirect = CallExecutors.safeguard(firstDirect); + assertSame(firstDirect, secondDirect); + } +}