2929 */
3030package com .google .api .gax .grpc ;
3131
32+ import static com .google .api .gax .grpc .testing .FakeServiceGrpc .METHOD_RECOGNIZE ;
3233import static com .google .api .gax .grpc .testing .FakeServiceGrpc .METHOD_SERVER_STREAMING_RECOGNIZE ;
3334import static com .google .common .truth .Truth .assertThat ;
3435
36+ import com .google .api .core .ApiFuture ;
3537import com .google .api .gax .grpc .testing .FakeChannelFactory ;
3638import com .google .api .gax .grpc .testing .FakeMethodDescriptor ;
37- import com .google .api .gax .grpc .testing .FakeServiceGrpc ;
3839import com .google .api .gax .rpc .ClientContext ;
3940import com .google .api .gax .rpc .ResponseObserver ;
4041import com .google .api .gax .rpc .ServerStreamingCallSettings ;
4142import com .google .api .gax .rpc .ServerStreamingCallable ;
4243import com .google .api .gax .rpc .StreamController ;
44+ import com .google .api .gax .rpc .UnaryCallSettings ;
45+ import com .google .api .gax .rpc .UnaryCallable ;
4346import com .google .common .base .Preconditions ;
4447import com .google .common .collect .ImmutableList ;
4548import com .google .common .collect .Lists ;
6366import java .util .concurrent .ScheduledFuture ;
6467import java .util .concurrent .TimeUnit ;
6568import java .util .concurrent .atomic .AtomicInteger ;
69+ import java .util .logging .Handler ;
70+ import java .util .logging .LogRecord ;
71+ import java .util .stream .Collectors ;
6672import org .junit .After ;
6773import org .junit .Assert ;
6874import org .junit .Test ;
@@ -117,7 +123,7 @@ public void testRoundRobin() throws IOException {
117123
118124 private void verifyTargetChannel (
119125 ChannelPool pool , List <ManagedChannel > channels , ManagedChannel targetChannel ) {
120- MethodDescriptor <Color , Money > methodDescriptor = FakeServiceGrpc . METHOD_RECOGNIZE ;
126+ MethodDescriptor <Color , Money > methodDescriptor = METHOD_RECOGNIZE ;
121127 CallOptions callOptions = CallOptions .DEFAULT ;
122128 @ SuppressWarnings ("unchecked" )
123129 ClientCall <Color , Money > expectedClientCall = Mockito .mock (ClientCall .class );
@@ -143,7 +149,7 @@ public void ensureEvenDistribution() throws InterruptedException, IOException {
143149 final ManagedChannel [] channels = new ManagedChannel [numChannels ];
144150 final AtomicInteger [] counts = new AtomicInteger [numChannels ];
145151
146- final MethodDescriptor <Color , Money > methodDescriptor = FakeServiceGrpc . METHOD_RECOGNIZE ;
152+ final MethodDescriptor <Color , Money > methodDescriptor = METHOD_RECOGNIZE ;
147153 final CallOptions callOptions = CallOptions .DEFAULT ;
148154 @ SuppressWarnings ("unchecked" )
149155 final ClientCall <Color , Money > clientCall = Mockito .mock (ClientCall .class );
@@ -472,23 +478,21 @@ public void channelCountShouldNotChangeWhenOutstandingRpcsAreWithinLimits() thro
472478 // Start the minimum number of
473479 for (int i = 0 ; i < 2 ; i ++) {
474480 ClientCalls .futureUnaryCall (
475- pool .newCall (FakeServiceGrpc .METHOD_RECOGNIZE , CallOptions .DEFAULT ),
476- Color .getDefaultInstance ());
481+ pool .newCall (METHOD_RECOGNIZE , CallOptions .DEFAULT ), Color .getDefaultInstance ());
477482 }
478483 pool .resize ();
479484 assertThat (pool .entries .get ()).hasSize (2 );
480485
481486 // Add enough RPCs to be just at the brink of expansion
482487 for (int i = startedCalls .size (); i < 4 ; i ++) {
483488 ClientCalls .futureUnaryCall (
484- pool .newCall (FakeServiceGrpc .METHOD_RECOGNIZE , CallOptions .DEFAULT ),
485- Color .getDefaultInstance ());
489+ pool .newCall (METHOD_RECOGNIZE , CallOptions .DEFAULT ), Color .getDefaultInstance ());
486490 }
487491 pool .resize ();
488492 assertThat (pool .entries .get ()).hasSize (2 );
489493
490494 // Add another RPC to push expansion
491- pool .newCall (FakeServiceGrpc . METHOD_RECOGNIZE , CallOptions .DEFAULT );
495+ pool .newCall (METHOD_RECOGNIZE , CallOptions .DEFAULT );
492496 pool .resize ();
493497 assertThat (pool .entries .get ()).hasSize (4 ); // += ChannelPool::MAX_RESIZE_DELTA
494498 assertThat (startedCalls ).hasSize (5 );
@@ -593,8 +597,7 @@ public void removedActiveChannelsAreShutdown() throws Exception {
593597 // Start 2 RPCs
594598 for (int i = 0 ; i < 2 ; i ++) {
595599 ClientCalls .futureUnaryCall (
596- pool .newCall (FakeServiceGrpc .METHOD_RECOGNIZE , CallOptions .DEFAULT ),
597- Color .getDefaultInstance ());
600+ pool .newCall (METHOD_RECOGNIZE , CallOptions .DEFAULT ), Color .getDefaultInstance ());
598601 }
599602 // Complete the first one
600603 @ SuppressWarnings ("unchecked" )
@@ -663,4 +666,74 @@ public void onComplete() {}
663666 assertThat (e .getCause ()).isInstanceOf (CancellationException .class );
664667 assertThat (e .getMessage ()).isEqualTo ("Call is already cancelled" );
665668 }
669+
670+ @ Test
671+ public void testDoubleRelease () throws Exception {
672+ FakeLogHandler logHandler = new FakeLogHandler ();
673+ ChannelPool .LOG .addHandler (logHandler );
674+
675+ try {
676+ // Create a fake channel pool thats backed by mock channels that simply record invocations
677+ ClientCall mockClientCall = Mockito .mock (ClientCall .class );
678+ ManagedChannel fakeChannel = Mockito .mock (ManagedChannel .class );
679+ Mockito .when (fakeChannel .newCall (Mockito .any (), Mockito .any ())).thenReturn (mockClientCall );
680+ ChannelPoolSettings channelPoolSettings = ChannelPoolSettings .staticallySized (1 );
681+ ChannelFactory factory = new FakeChannelFactory (ImmutableList .of (fakeChannel ));
682+
683+ pool = ChannelPool .create (channelPoolSettings , factory );
684+
685+ // Construct a fake callable to use the channel pool
686+ ClientContext context =
687+ ClientContext .newBuilder ()
688+ .setTransportChannel (GrpcTransportChannel .create (pool ))
689+ .setDefaultCallContext (GrpcCallContext .of (pool , CallOptions .DEFAULT ))
690+ .build ();
691+
692+ UnaryCallSettings <Color , Money > settings =
693+ UnaryCallSettings .<Color , Money >newUnaryCallSettingsBuilder ().build ();
694+ UnaryCallable <Color , Money > callable =
695+ GrpcCallableFactory .createUnaryCallable (
696+ GrpcCallSettings .create (METHOD_RECOGNIZE ), settings , context );
697+
698+ // Start the RPC
699+ ApiFuture <Money > rpcFuture =
700+ callable .futureCall (Color .getDefaultInstance (), context .getDefaultCallContext ());
701+
702+ // Get the server side listener and intentionally close it twice
703+ ArgumentCaptor <ClientCall .Listener <?>> clientCallListenerCaptor =
704+ ArgumentCaptor .forClass (ClientCall .Listener .class );
705+ Mockito .verify (mockClientCall ).start (clientCallListenerCaptor .capture (), Mockito .any ());
706+ clientCallListenerCaptor .getValue ().onClose (Status .INTERNAL , new Metadata ());
707+ clientCallListenerCaptor .getValue ().onClose (Status .UNKNOWN , new Metadata ());
708+
709+ // Ensure that the channel pool properly logged the double call and kept the refCount correct
710+ assertThat (logHandler .getAllMessages ())
711+ .contains (
712+ "Call is being closed more than once. Please make sure that onClose() is not being manually called." );
713+ assertThat (pool .entries .get ()).hasSize (1 );
714+ ChannelPool .Entry entry = pool .entries .get ().get (0 );
715+ assertThat (entry .outstandingRpcs .get ()).isEqualTo (0 );
716+ } finally {
717+ ChannelPool .LOG .removeHandler (logHandler );
718+ }
719+ }
720+
721+ private static class FakeLogHandler extends Handler {
722+ List <LogRecord > records = new ArrayList <>();
723+
724+ @ Override
725+ public void publish (LogRecord record ) {
726+ records .add (record );
727+ }
728+
729+ @ Override
730+ public void flush () {}
731+
732+ @ Override
733+ public void close () throws SecurityException {}
734+
735+ List <String > getAllMessages () {
736+ return records .stream ().map (LogRecord ::getMessage ).collect (Collectors .toList ());
737+ }
738+ }
666739}
0 commit comments