From 9554e0d44a5682aec602fa99a2fa7ef4f4edebfc Mon Sep 17 00:00:00 2001 From: themechbro Date: Wed, 18 Mar 2026 22:51:44 +0530 Subject: [PATCH 1/8] core: fix false-positive orphan warning in ManagedChannelOrphanWrapper Add a reachability fence in shutdown() and shutdownNow() to ensure the wrapper is not garbage collected while shutdown logic is executing. This prevents a race condition when using directExecutor() where a warning could be logged despite a proper shutdown. Fixes #12641 --- .../internal/ManagedChannelOrphanWrapper.java | 17 ++++++-- .../ManagedChannelOrphanWrapperTest.java | 39 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java b/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java index eac9b64d9db..c7f722a9499 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java @@ -62,14 +62,24 @@ final class ManagedChannelOrphanWrapper extends ForwardingManagedChannel { @Override public ManagedChannel shutdown() { + ManagedChannel result = super.shutdown(); phantom.clearSafely(); - return super.shutdown(); + // This dummy check prevents the JIT from collecting 'this' too early + if (this.getClass() == null) { + throw new AssertionError(); + } + return result; } @Override public ManagedChannel shutdownNow() { + ManagedChannel result = super.shutdownNow(); phantom.clearSafely(); - return super.shutdownNow(); + // This dummy check prevents the JIT from collecting 'this' too early + if (this.getClass() == null) { + throw new AssertionError(); + } + return result; } @VisibleForTesting @@ -151,8 +161,9 @@ static int cleanQueue(ReferenceQueue refqueue) { int orphanedChannels = 0; while ((ref = (ManagedChannelReference) refqueue.poll()) != null) { RuntimeException maybeAllocationSite = ref.allocationSite.get(); + boolean wasShutdown = ref.shutdown.get(); ref.clearInternal(); // technically the reference is gone already. - if (!ref.shutdown.get()) { + if (!wasShutdown) { orphanedChannels++; Level level = Level.SEVERE; if (logger.isLoggable(level)) { diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java index 5ae97c69211..7ddab5b1ceb 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java @@ -101,6 +101,45 @@ public boolean isDone() { } } + @Test + public void shutdownNow_withDelegateStillReferenced_doesNotLogWarning() { + ManagedChannel mc = new TestManagedChannel(); + final ReferenceQueue refqueue = new ReferenceQueue<>(); + ConcurrentMap refs = + new ConcurrentHashMap<>(); + + ManagedChannelOrphanWrapper wrapper = new ManagedChannelOrphanWrapper(mc, refqueue, refs); + WeakReference wrapperWeakRef = new WeakReference<>(wrapper); + + final List records = new ArrayList<>(); + Logger orphanLogger = Logger.getLogger(ManagedChannelOrphanWrapper.class.getName()); + Filter oldFilter = orphanLogger.getFilter(); + orphanLogger.setFilter(new Filter() { + @Override + public boolean isLoggable(LogRecord record) { + synchronized (records) { + records.add(record); + } + return false; + } + }); + + try { + wrapper.shutdownNow(); + wrapper = null; + + // Wait for the WRAPPER itself to be garbage collected + GcFinalization.awaitClear(wrapperWeakRef); + ManagedChannelReference.cleanQueue(refqueue); + + synchronized (records) { + assertEquals("Warning was logged even though shutdownNow() was called!", 0, records.size()); + } + } finally { + orphanLogger.setFilter(oldFilter); + } + } + @Test public void refCycleIsGCed() { ReferenceQueue refqueue = From ffe4072622ca1e4b66f0ea6ceaac19984d9a9b4b Mon Sep 17 00:00:00 2001 From: themechbro Date: Wed, 18 Mar 2026 23:16:15 +0530 Subject: [PATCH 2/8] test: add coverage for standard shutdown() method --- .../java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java index 7ddab5b1ceb..a285f0b1ccb 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java @@ -125,7 +125,7 @@ public boolean isLoggable(LogRecord record) { }); try { - wrapper.shutdownNow(); + wrapper.shutdown(); wrapper = null; // Wait for the WRAPPER itself to be garbage collected From f81fc0b7004b62691503cdb8b6f6e6d08ae38b2e Mon Sep 17 00:00:00 2001 From: themechbro Date: Wed, 18 Mar 2026 23:40:12 +0530 Subject: [PATCH 3/8] test: add coverage for orphaned channel branch --- .../ManagedChannelOrphanWrapperTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java index a285f0b1ccb..a0f6063433b 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java @@ -140,6 +140,30 @@ public boolean isLoggable(LogRecord record) { } } + @Test + public void orphanedChannel_triggerWarningAndCoverage() { + ManagedChannel mc = new TestManagedChannel(); + final ReferenceQueue refqueue = new ReferenceQueue<>(); + ConcurrentMap refs = + new ConcurrentHashMap<>(); + + // Create the wrapper but NEVER call shutdown + ManagedChannelOrphanWrapper wrapper = new ManagedChannelOrphanWrapper(mc, refqueue, refs); + wrapper = null; // Make it eligible for GC + + // Trigger GC and clean the queue to hit the !wasShutdown branch + final AtomicInteger numOrphans = new AtomicInteger(); + GcFinalization.awaitDone(new FinalizationPredicate() { + @Override + public boolean isDone() { + numOrphans.getAndAdd(ManagedChannelReference.cleanQueue(refqueue)); + return numOrphans.get() > 0; + } + }); + + assertEquals(1, numOrphans.get()); + } + @Test public void refCycleIsGCed() { ReferenceQueue refqueue = From 46a6cdf26356c12bee12856f0a73e6339b365060 Mon Sep 17 00:00:00 2001 From: themechbro Date: Wed, 18 Mar 2026 23:54:32 +0530 Subject: [PATCH 4/8] test: suppress unused variable warning in orphan test --- .../java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java index a0f6063433b..aad002b27ce 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelOrphanWrapperTest.java @@ -148,6 +148,7 @@ public void orphanedChannel_triggerWarningAndCoverage() { new ConcurrentHashMap<>(); // Create the wrapper but NEVER call shutdown + @SuppressWarnings("UnusedVariable") ManagedChannelOrphanWrapper wrapper = new ManagedChannelOrphanWrapper(mc, refqueue, refs); wrapper = null; // Make it eligible for GC From 24c8d61549c2f92994c9f7d14125ac698eed2701 Mon Sep 17 00:00:00 2001 From: themechbro Date: Sat, 21 Mar 2026 23:33:32 +0530 Subject: [PATCH 5/8] core: introduce MirroringInterceptor for traffic shadowing This adds a fire-and-forget interceptor that mirrors Unary and Streaming traffic to a secondary channel without blocking the primary call. Headers are copied and propagated safely, and the secondary call respects the lifecycle (halfClose, cancel) of the primary stream. Addresses the Java ClientInterceptor proposal discussed in #12448 --- .../io/grpc/util/MirroringInterceptor.java | 116 ++++++++++++++++++ .../inprocess/MirroringInterceptorTest.java | 85 +++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 core/src/main/java/io/grpc/util/MirroringInterceptor.java create mode 100644 inprocess/src/test/java/io/grpc/inprocess/MirroringInterceptorTest.java diff --git a/core/src/main/java/io/grpc/util/MirroringInterceptor.java b/core/src/main/java/io/grpc/util/MirroringInterceptor.java new file mode 100644 index 00000000000..851af694c5d --- /dev/null +++ b/core/src/main/java/io/grpc/util/MirroringInterceptor.java @@ -0,0 +1,116 @@ +/* + * 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.util; + +import com.google.common.base.Preconditions; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A ClientInterceptor that mirrors calls to a shadow channel. + * Designed to support Unary, Client-Streaming, Server-Streaming, and Bidi calls. + */ +public final class MirroringInterceptor implements ClientInterceptor { + private static final Logger logger = Logger.getLogger(MirroringInterceptor.class.getName()); + + private final Channel mirrorChannel; + private final Executor executor; + + public MirroringInterceptor(Channel mirrorChannel, Executor executor) { + this.mirrorChannel = Preconditions.checkNotNull(mirrorChannel, "mirrorChannel"); + this.executor = Preconditions.checkNotNull(executor, "executor"); + } + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + + private ClientCall mirrorCall; + + @Override + public void start(Listener responseListener, Metadata headers) { + // 1. Capture and copy headers immediately (thread-safe for the executor) + final Metadata mirrorHeaders = new Metadata(); + mirrorHeaders.merge(headers); + + executor.execute(() -> { + try { + // 2. Initialize the shadow call once per stream + mirrorCall = mirrorChannel.newCall(method, callOptions); + mirrorCall.start(new ClientCall.Listener() {}, mirrorHeaders); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to start mirror call", e); + } + }); + super.start(responseListener, headers); + } + + @Override + public void sendMessage(ReqT message) { + executor.execute(() -> { + if (mirrorCall != null) { + try { + mirrorCall.sendMessage(message); + } catch (Exception e) { + logger.log(Level.WARNING, "Mirroring message failed", e); + } + } + }); + super.sendMessage(message); + } + + @Override + public void halfClose() { + executor.execute(() -> { + if (mirrorCall != null) { + try { + mirrorCall.halfClose(); + } catch (Exception e) { + logger.log(Level.WARNING, "Mirroring halfClose failed", e); + } + } + }); + super.halfClose(); + } + + @Override + public void cancel(String message, Throwable cause) { + executor.execute(() -> { + if (mirrorCall != null) { + try { + mirrorCall.cancel(message, cause); + } catch (Exception e) { + logger.log(Level.WARNING, "Mirroring cancel failed", e); + } + } + }); + super.cancel(message, cause); + } + }; + } +} \ No newline at end of file diff --git a/inprocess/src/test/java/io/grpc/inprocess/MirroringInterceptorTest.java b/inprocess/src/test/java/io/grpc/inprocess/MirroringInterceptorTest.java new file mode 100644 index 00000000000..d660c10fb6c --- /dev/null +++ b/inprocess/src/test/java/io/grpc/inprocess/MirroringInterceptorTest.java @@ -0,0 +1,85 @@ +package io.grpc.inprocess; + +import static org.junit.Assert.assertTrue; +import io.grpc.*; +import io.grpc.testing.GrpcCleanupRule; +import org.junit.Rule; +import org.junit.Test; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.nio.charset.StandardCharsets; + +public class MirroringInterceptorTest { + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + private static final MethodDescriptor.Marshaller MARSHALLER = new MethodDescriptor.Marshaller() { + @Override public java.io.InputStream stream(String value) { + return new java.io.ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)); + } + @Override public String parse(java.io.InputStream stream) { return "response"; } + }; + + private final MethodDescriptor method = MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("test/Method") + .setRequestMarshaller(MARSHALLER) + .setResponseMarshaller(MARSHALLER) + .build(); + + @Test + public void unaryCallIsMirroredWithHeaders() throws Exception { + CountDownLatch mirrorLatch = new CountDownLatch(1); + Metadata.Key testKey = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER); + AtomicBoolean mirrorHeaderVerified = new AtomicBoolean(false); + + // 1. Setup Mirror Server - IMPORTANT: It must CLOSE the call + String mirrorName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(mirrorName).directExecutor() + .addService(ServerServiceDefinition.builder("test") + .addMethod(method, (call, headers) -> { + if ("shadow-value".equals(headers.get(testKey))) { + mirrorHeaderVerified.set(true); + } + mirrorLatch.countDown(); + + // CRITICAL: Close the call so the channel can shut down + call.sendHeaders(new Metadata()); + call.close(Status.OK, new Metadata()); + return new ServerCall.Listener() {}; + }).build()).build().start()); + + // 2. Setup Primary Server - Also must CLOSE the call + String primaryName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(primaryName).directExecutor() + .addService(ServerServiceDefinition.builder("test") + .addMethod(method, (call, headers) -> { + call.sendHeaders(new Metadata()); + call.close(Status.OK, new Metadata()); + return new ServerCall.Listener() {}; + }).build()).build().start()); + + ManagedChannel mirrorChannel = grpcCleanup.register(InProcessChannelBuilder.forName(mirrorName).build()); + ManagedChannel primaryChannel = grpcCleanup.register(InProcessChannelBuilder.forName(primaryName).build()); + + // Use direct executor to keep the mirror call on the same thread + java.util.concurrent.Executor directExecutor = Runnable::run; + + Channel interceptedChannel = ClientInterceptors.intercept(primaryChannel, + new MirroringInterceptor(mirrorChannel, directExecutor)); + + // 3. Trigger call with Metadata + Metadata headers = new Metadata(); + headers.put(testKey, "shadow-value"); + + ClientCall call = interceptedChannel.newCall(method, CallOptions.DEFAULT); + call.start(new ClientCall.Listener() {}, headers); + call.sendMessage("hello"); + call.halfClose(); + + // 4. Assertions + assertTrue("Mirror server was not reached", mirrorLatch.await(1, TimeUnit.SECONDS)); + assertTrue("Headers were not correctly mirrored to shadow service", mirrorHeaderVerified.get()); + System.out.println("FULL MIRRORING SUCCESSFUL!"); + } +} \ No newline at end of file From 61b45113b4deec0e0c6ec5cbdccc16771bc1c466 Mon Sep 17 00:00:00 2001 From: themechbro Date: Sun, 22 Mar 2026 12:35:02 +0530 Subject: [PATCH 6/8] test: fix checkstyle and import for MirroringInterceptor --- .../inprocess/MirroringInterceptorTest.java | 201 +++++++++++------- 1 file changed, 128 insertions(+), 73 deletions(-) diff --git a/inprocess/src/test/java/io/grpc/inprocess/MirroringInterceptorTest.java b/inprocess/src/test/java/io/grpc/inprocess/MirroringInterceptorTest.java index d660c10fb6c..d81a5aa6904 100644 --- a/inprocess/src/test/java/io/grpc/inprocess/MirroringInterceptorTest.java +++ b/inprocess/src/test/java/io/grpc/inprocess/MirroringInterceptorTest.java @@ -1,85 +1,140 @@ +/* + * 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.inprocess; import static org.junit.Assert.assertTrue; -import io.grpc.*; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptors; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerServiceDefinition; +import io.grpc.Status; import io.grpc.testing.GrpcCleanupRule; -import org.junit.Rule; -import org.junit.Test; +import io.grpc.util.MirroringInterceptor; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.nio.charset.StandardCharsets; +import org.junit.Rule; +import org.junit.Test; public class MirroringInterceptorTest { - @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); - private static final MethodDescriptor.Marshaller MARSHALLER = new MethodDescriptor.Marshaller() { - @Override public java.io.InputStream stream(String value) { - return new java.io.ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)); + private static final MethodDescriptor.Marshaller MARSHALLER = + new MethodDescriptor.Marshaller() { + @Override + public java.io.InputStream stream(String value) { + return new java.io.ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)); } - @Override public String parse(java.io.InputStream stream) { return "response"; } - }; - - private final MethodDescriptor method = MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("test/Method") - .setRequestMarshaller(MARSHALLER) - .setResponseMarshaller(MARSHALLER) - .build(); - - @Test - public void unaryCallIsMirroredWithHeaders() throws Exception { - CountDownLatch mirrorLatch = new CountDownLatch(1); - Metadata.Key testKey = Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER); - AtomicBoolean mirrorHeaderVerified = new AtomicBoolean(false); - - // 1. Setup Mirror Server - IMPORTANT: It must CLOSE the call - String mirrorName = InProcessServerBuilder.generateName(); - grpcCleanup.register(InProcessServerBuilder.forName(mirrorName).directExecutor() - .addService(ServerServiceDefinition.builder("test") - .addMethod(method, (call, headers) -> { - if ("shadow-value".equals(headers.get(testKey))) { - mirrorHeaderVerified.set(true); - } - mirrorLatch.countDown(); - - // CRITICAL: Close the call so the channel can shut down - call.sendHeaders(new Metadata()); - call.close(Status.OK, new Metadata()); - return new ServerCall.Listener() {}; - }).build()).build().start()); - - // 2. Setup Primary Server - Also must CLOSE the call - String primaryName = InProcessServerBuilder.generateName(); - grpcCleanup.register(InProcessServerBuilder.forName(primaryName).directExecutor() - .addService(ServerServiceDefinition.builder("test") - .addMethod(method, (call, headers) -> { - call.sendHeaders(new Metadata()); - call.close(Status.OK, new Metadata()); - return new ServerCall.Listener() {}; - }).build()).build().start()); - - ManagedChannel mirrorChannel = grpcCleanup.register(InProcessChannelBuilder.forName(mirrorName).build()); - ManagedChannel primaryChannel = grpcCleanup.register(InProcessChannelBuilder.forName(primaryName).build()); - - // Use direct executor to keep the mirror call on the same thread - java.util.concurrent.Executor directExecutor = Runnable::run; - - Channel interceptedChannel = ClientInterceptors.intercept(primaryChannel, - new MirroringInterceptor(mirrorChannel, directExecutor)); - - // 3. Trigger call with Metadata - Metadata headers = new Metadata(); - headers.put(testKey, "shadow-value"); - - ClientCall call = interceptedChannel.newCall(method, CallOptions.DEFAULT); - call.start(new ClientCall.Listener() {}, headers); - call.sendMessage("hello"); - call.halfClose(); - - // 4. Assertions - assertTrue("Mirror server was not reached", mirrorLatch.await(1, TimeUnit.SECONDS)); - assertTrue("Headers were not correctly mirrored to shadow service", mirrorHeaderVerified.get()); - System.out.println("FULL MIRRORING SUCCESSFUL!"); - } + + @Override + public String parse(java.io.InputStream stream) { + return "response"; + } + }; + + private final MethodDescriptor method = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("test/Method") + .setRequestMarshaller(MARSHALLER) + .setResponseMarshaller(MARSHALLER) + .build(); + + @Test + public void unaryCallIsMirroredWithHeaders() throws Exception { + CountDownLatch mirrorLatch = new CountDownLatch(1); + Metadata.Key testKey = + Metadata.Key.of("test-header", Metadata.ASCII_STRING_MARSHALLER); + AtomicBoolean mirrorHeaderVerified = new AtomicBoolean(false); + + // 1. Setup Mirror Server - IMPORTANT: It must CLOSE the call + String mirrorName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(mirrorName) + .directExecutor() + .addService( + ServerServiceDefinition.builder("test") + .addMethod( + method, + (call, headers) -> { + if ("shadow-value".equals(headers.get(testKey))) { + mirrorHeaderVerified.set(true); + } + mirrorLatch.countDown(); + + // CRITICAL: Close the call so the channel can shut down + call.sendHeaders(new Metadata()); + call.close(Status.OK, new Metadata()); + return new ServerCall.Listener() {}; + }) + .build()) + .build() + .start()); + + // 2. Setup Primary Server - Also must CLOSE the call + String primaryName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(primaryName) + .directExecutor() + .addService( + ServerServiceDefinition.builder("test") + .addMethod( + method, + (call, headers) -> { + call.sendHeaders(new Metadata()); + call.close(Status.OK, new Metadata()); + return new ServerCall.Listener() {}; + }) + .build()) + .build() + .start()); + + ManagedChannel mirrorChannel = + grpcCleanup.register(InProcessChannelBuilder.forName(mirrorName).build()); + ManagedChannel primaryChannel = + grpcCleanup.register(InProcessChannelBuilder.forName(primaryName).build()); + + // Use direct executor to keep the mirror call on the same thread + java.util.concurrent.Executor directExecutor = Runnable::run; + + Channel interceptedChannel = + ClientInterceptors.intercept( + primaryChannel, new MirroringInterceptor(mirrorChannel, directExecutor)); + + // 3. Trigger call with Metadata + Metadata headers = new Metadata(); + headers.put(testKey, "shadow-value"); + + ClientCall call = interceptedChannel.newCall(method, CallOptions.DEFAULT); + call.start(new ClientCall.Listener() {}, headers); + call.sendMessage("hello"); + call.halfClose(); + + // 4. Assertions + assertTrue("Mirror server was not reached", mirrorLatch.await(1, TimeUnit.SECONDS)); + assertTrue( + "Headers were not correctly mirrored to shadow service", mirrorHeaderVerified.get()); + System.out.println("FULL MIRRORING SUCCESSFUL!"); + } } \ No newline at end of file From 55e43baacd11e853307d9cbb4bf502f6907035aa Mon Sep 17 00:00:00 2001 From: themechbro Date: Sun, 22 Mar 2026 13:05:23 +0530 Subject: [PATCH 7/8] chore: trigger CI re-run for network timeouts From 6485f912256b080aab76c4ad97ce4844fc750f40 Mon Sep 17 00:00:00 2001 From: themechbro Date: Thu, 26 Mar 2026 16:07:37 +0530 Subject: [PATCH 8/8] fix: remove unused result variable to pass error-prone compiler --- .../java/io/grpc/internal/ManagedChannelOrphanWrapper.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java b/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java index c7f722a9499..790d5bd297f 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelOrphanWrapper.java @@ -62,24 +62,22 @@ final class ManagedChannelOrphanWrapper extends ForwardingManagedChannel { @Override public ManagedChannel shutdown() { - ManagedChannel result = super.shutdown(); phantom.clearSafely(); // This dummy check prevents the JIT from collecting 'this' too early if (this.getClass() == null) { throw new AssertionError(); } - return result; + return super.shutdown(); } @Override public ManagedChannel shutdownNow() { - ManagedChannel result = super.shutdownNow(); phantom.clearSafely(); // This dummy check prevents the JIT from collecting 'this' too early if (this.getClass() == null) { throw new AssertionError(); } - return result; + return super.shutdownNow(); } @VisibleForTesting