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+
117package io .grpc .inprocess ;
218
319import static org .junit .Assert .assertTrue ;
4- import io .grpc .*;
20+
21+ import io .grpc .CallOptions ;
22+ import io .grpc .Channel ;
23+ import io .grpc .ClientCall ;
24+ import io .grpc .ClientInterceptors ;
25+ import io .grpc .ManagedChannel ;
26+ import io .grpc .Metadata ;
27+ import io .grpc .MethodDescriptor ;
28+ import io .grpc .ServerCall ;
29+ import io .grpc .ServerServiceDefinition ;
30+ import io .grpc .Status ;
531import io .grpc .testing .GrpcCleanupRule ;
6- import org . junit . Rule ;
7- import org . junit . Test ;
32+ import io . grpc . util . MirroringInterceptor ;
33+ import java . nio . charset . StandardCharsets ;
834import java .util .concurrent .CountDownLatch ;
935import java .util .concurrent .TimeUnit ;
1036import java .util .concurrent .atomic .AtomicBoolean ;
11- import java .nio .charset .StandardCharsets ;
37+ import org .junit .Rule ;
38+ import org .junit .Test ;
1239
1340public class MirroringInterceptorTest {
14- @ Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule ();
41+ @ Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule ();
1542
16- private static final MethodDescriptor .Marshaller <String > MARSHALLER = new MethodDescriptor .Marshaller <String >() {
17- @ Override public java .io .InputStream stream (String value ) {
18- return new java .io .ByteArrayInputStream (value .getBytes (StandardCharsets .UTF_8 ));
43+ private static final MethodDescriptor .Marshaller <String > MARSHALLER =
44+ new MethodDescriptor .Marshaller <String >() {
45+ @ Override
46+ public java .io .InputStream stream (String value ) {
47+ return new java .io .ByteArrayInputStream (value .getBytes (StandardCharsets .UTF_8 ));
1948 }
20- @ Override public String parse (java .io .InputStream stream ) { return "response" ; }
21- };
22-
23- private final MethodDescriptor <String , String > method = MethodDescriptor .<String , String >newBuilder ()
24- .setType (MethodDescriptor .MethodType .UNARY )
25- .setFullMethodName ("test/Method" )
26- .setRequestMarshaller (MARSHALLER )
27- .setResponseMarshaller (MARSHALLER )
28- .build ();
29-
30- @ Test
31- public void unaryCallIsMirroredWithHeaders () throws Exception {
32- CountDownLatch mirrorLatch = new CountDownLatch (1 );
33- Metadata .Key <String > testKey = Metadata .Key .of ("test-header" , Metadata .ASCII_STRING_MARSHALLER );
34- AtomicBoolean mirrorHeaderVerified = new AtomicBoolean (false );
35-
36- // 1. Setup Mirror Server - IMPORTANT: It must CLOSE the call
37- String mirrorName = InProcessServerBuilder .generateName ();
38- grpcCleanup .register (InProcessServerBuilder .forName (mirrorName ).directExecutor ()
39- .addService (ServerServiceDefinition .builder ("test" )
40- .addMethod (method , (call , headers ) -> {
41- if ("shadow-value" .equals (headers .get (testKey ))) {
42- mirrorHeaderVerified .set (true );
43- }
44- mirrorLatch .countDown ();
45-
46- // CRITICAL: Close the call so the channel can shut down
47- call .sendHeaders (new Metadata ());
48- call .close (Status .OK , new Metadata ());
49- return new ServerCall .Listener <String >() {};
50- }).build ()).build ().start ());
51-
52- // 2. Setup Primary Server - Also must CLOSE the call
53- String primaryName = InProcessServerBuilder .generateName ();
54- grpcCleanup .register (InProcessServerBuilder .forName (primaryName ).directExecutor ()
55- .addService (ServerServiceDefinition .builder ("test" )
56- .addMethod (method , (call , headers ) -> {
57- call .sendHeaders (new Metadata ());
58- call .close (Status .OK , new Metadata ());
59- return new ServerCall .Listener <String >() {};
60- }).build ()).build ().start ());
61-
62- ManagedChannel mirrorChannel = grpcCleanup .register (InProcessChannelBuilder .forName (mirrorName ).build ());
63- ManagedChannel primaryChannel = grpcCleanup .register (InProcessChannelBuilder .forName (primaryName ).build ());
64-
65- // Use direct executor to keep the mirror call on the same thread
66- java .util .concurrent .Executor directExecutor = Runnable ::run ;
67-
68- Channel interceptedChannel = ClientInterceptors .intercept (primaryChannel ,
69- new MirroringInterceptor (mirrorChannel , directExecutor ));
70-
71- // 3. Trigger call with Metadata
72- Metadata headers = new Metadata ();
73- headers .put (testKey , "shadow-value" );
74-
75- ClientCall <String , String > call = interceptedChannel .newCall (method , CallOptions .DEFAULT );
76- call .start (new ClientCall .Listener <String >() {}, headers );
77- call .sendMessage ("hello" );
78- call .halfClose ();
79-
80- // 4. Assertions
81- assertTrue ("Mirror server was not reached" , mirrorLatch .await (1 , TimeUnit .SECONDS ));
82- assertTrue ("Headers were not correctly mirrored to shadow service" , mirrorHeaderVerified .get ());
83- System .out .println ("FULL MIRRORING SUCCESSFUL!" );
84- }
49+
50+ @ Override
51+ public String parse (java .io .InputStream stream ) {
52+ return "response" ;
53+ }
54+ };
55+
56+ private final MethodDescriptor <String , String > method =
57+ MethodDescriptor .<String , String >newBuilder ()
58+ .setType (MethodDescriptor .MethodType .UNARY )
59+ .setFullMethodName ("test/Method" )
60+ .setRequestMarshaller (MARSHALLER )
61+ .setResponseMarshaller (MARSHALLER )
62+ .build ();
63+
64+ @ Test
65+ public void unaryCallIsMirroredWithHeaders () throws Exception {
66+ CountDownLatch mirrorLatch = new CountDownLatch (1 );
67+ Metadata .Key <String > testKey =
68+ Metadata .Key .of ("test-header" , Metadata .ASCII_STRING_MARSHALLER );
69+ AtomicBoolean mirrorHeaderVerified = new AtomicBoolean (false );
70+
71+ // 1. Setup Mirror Server - IMPORTANT: It must CLOSE the call
72+ String mirrorName = InProcessServerBuilder .generateName ();
73+ grpcCleanup .register (
74+ InProcessServerBuilder .forName (mirrorName )
75+ .directExecutor ()
76+ .addService (
77+ ServerServiceDefinition .builder ("test" )
78+ .addMethod (
79+ method ,
80+ (call , headers ) -> {
81+ if ("shadow-value" .equals (headers .get (testKey ))) {
82+ mirrorHeaderVerified .set (true );
83+ }
84+ mirrorLatch .countDown ();
85+
86+ // CRITICAL: Close the call so the channel can shut down
87+ call .sendHeaders (new Metadata ());
88+ call .close (Status .OK , new Metadata ());
89+ return new ServerCall .Listener <String >() {};
90+ })
91+ .build ())
92+ .build ()
93+ .start ());
94+
95+ // 2. Setup Primary Server - Also must CLOSE the call
96+ String primaryName = InProcessServerBuilder .generateName ();
97+ grpcCleanup .register (
98+ InProcessServerBuilder .forName (primaryName )
99+ .directExecutor ()
100+ .addService (
101+ ServerServiceDefinition .builder ("test" )
102+ .addMethod (
103+ method ,
104+ (call , headers ) -> {
105+ call .sendHeaders (new Metadata ());
106+ call .close (Status .OK , new Metadata ());
107+ return new ServerCall .Listener <String >() {};
108+ })
109+ .build ())
110+ .build ()
111+ .start ());
112+
113+ ManagedChannel mirrorChannel =
114+ grpcCleanup .register (InProcessChannelBuilder .forName (mirrorName ).build ());
115+ ManagedChannel primaryChannel =
116+ grpcCleanup .register (InProcessChannelBuilder .forName (primaryName ).build ());
117+
118+ // Use direct executor to keep the mirror call on the same thread
119+ java .util .concurrent .Executor directExecutor = Runnable ::run ;
120+
121+ Channel interceptedChannel =
122+ ClientInterceptors .intercept (
123+ primaryChannel , new MirroringInterceptor (mirrorChannel , directExecutor ));
124+
125+ // 3. Trigger call with Metadata
126+ Metadata headers = new Metadata ();
127+ headers .put (testKey , "shadow-value" );
128+
129+ ClientCall <String , String > call = interceptedChannel .newCall (method , CallOptions .DEFAULT );
130+ call .start (new ClientCall .Listener <String >() {}, headers );
131+ call .sendMessage ("hello" );
132+ call .halfClose ();
133+
134+ // 4. Assertions
135+ assertTrue ("Mirror server was not reached" , mirrorLatch .await (1 , TimeUnit .SECONDS ));
136+ assertTrue (
137+ "Headers were not correctly mirrored to shadow service" , mirrorHeaderVerified .get ());
138+ System .out .println ("FULL MIRRORING SUCCESSFUL!" );
139+ }
85140}
0 commit comments