/* * Copyright 2016, Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package io.grpc.internal; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import io.grpc.Status; import io.grpc.internal.KeepAliveManager.ClientKeepAlivePinger; import io.grpc.internal.KeepAliveManager.KeepAlivePinger; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @RunWith(JUnit4.class) public final class KeepAliveManagerTest { private final FakeTicker ticker = new FakeTicker(); private KeepAliveManager keepAliveManager; @Mock private KeepAlivePinger keepAlivePinger; @Mock private ConnectionClientTransport transport; @Mock private ScheduledExecutorService scheduler; @Captor private ArgumentCaptor<Status> statusCaptor; static class FakeTicker extends KeepAliveManager.Ticker { long time; @Override public long read() { return time; } } @Before public void setUp() { MockitoAnnotations.initMocks(this); keepAliveManager = new KeepAliveManager(keepAlivePinger, scheduler, ticker, 1000, 2000, false); } @Test public void sendKeepAlivePings() { ticker.time = 1; // Transport becomes active. We should schedule keepalive pings. keepAliveManager.onTransportActive(); ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class); ArgumentCaptor<Runnable> sendPingCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(1)).schedule(sendPingCaptor.capture(), delayCaptor.capture(), isA(TimeUnit.class)); Runnable sendPing = sendPingCaptor.getValue(); Long delay = delayCaptor.getValue(); assertEquals(1000 - 1, delay.longValue()); ScheduledFuture<?> shutdownFuture = mock(ScheduledFuture.class); doReturn(shutdownFuture) .when(scheduler).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Mannually running the Runnable will send the ping. Shutdown task should be scheduled. ticker.time = 1000; sendPing.run(); verify(keepAlivePinger).ping(); verify(scheduler, times(2)).schedule(isA(Runnable.class), delayCaptor.capture(), isA(TimeUnit.class)); delay = delayCaptor.getValue(); // Keepalive timeout is 2000. assertEquals(2000, delay.longValue()); // Ping succeeds. Reschedule another ping. ticker.time = 1100; keepAliveManager.onDataReceived(); verify(scheduler, times(3)).schedule(isA(Runnable.class), delayCaptor.capture(), isA(TimeUnit.class)); // Shutdown task has been cancelled. verify(shutdownFuture).cancel(isA(Boolean.class)); delay = delayCaptor.getValue(); // Next ping should be exactly 1000 nanoseconds later. assertEquals(1000, delay.longValue()); } @Test public void keepAlivePingDelayedByIncomingData() { // Transport becomes active. We should schedule keepalive pings. keepAliveManager.onTransportActive(); ArgumentCaptor<Runnable> sendPingCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(1)).schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable sendPing = sendPingCaptor.getValue(); // We receive some data. We may need to delay the ping. ticker.time = 1500; keepAliveManager.onDataReceived(); ticker.time = 1600; sendPing.run(); // We didn't send the ping. verify(transport, times(0)).ping(isA(ClientTransport.PingCallback.class), isA(Executor.class)); // Instead we reschedule. ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class); verify(scheduler, times(2)).schedule(isA(Runnable.class), delayCaptor.capture(), isA(TimeUnit.class)); Long delay = delayCaptor.getValue(); assertEquals(1500 + 1000 - 1600, delay.longValue()); } @Test public void clientKeepAlivePinger_pingTimeout() { keepAlivePinger = new ClientKeepAlivePinger(transport); keepAlivePinger.onPingTimeout(); verify(transport).shutdownNow(statusCaptor.capture()); Status status = statusCaptor.getValue(); assertThat(status.getCode()).isEqualTo(Status.Code.UNAVAILABLE); assertThat(status.getDescription()).isEqualTo( "Keepalive failed. The connection is likely gone"); } @Test public void clientKeepAlivePinger_pingFailure() { keepAlivePinger = new ClientKeepAlivePinger(transport); keepAlivePinger.ping(); ArgumentCaptor<ClientTransport.PingCallback> pingCallbackCaptor = ArgumentCaptor.forClass(ClientTransport.PingCallback.class); verify(transport).ping(pingCallbackCaptor.capture(), isA(Executor.class)); ClientTransport.PingCallback pingCallback = pingCallbackCaptor.getValue(); pingCallback.onFailure(new Throwable()); verify(transport).shutdownNow(statusCaptor.capture()); Status status = statusCaptor.getValue(); assertThat(status.getCode()).isEqualTo(Status.Code.UNAVAILABLE); assertThat(status.getDescription()).isEqualTo( "Keepalive failed. The connection is likely gone"); } @Test public void onTransportTerminationCancelsShutdownFuture() { // Transport becomes active. We should schedule keepalive pings. keepAliveManager.onTransportActive(); ArgumentCaptor<Runnable> sendPingCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(1)) .schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable sendPing = sendPingCaptor.getValue(); ScheduledFuture<?> shutdownFuture = mock(ScheduledFuture.class); doReturn(shutdownFuture) .when(scheduler).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Mannually running the Runnable will send the ping. Shutdown task should be scheduled. ticker.time = 1000; sendPing.run(); keepAliveManager.onTransportTermination(); // Shutdown task has been cancelled. verify(shutdownFuture).cancel(isA(Boolean.class)); } @Test public void keepAlivePingTimesOut() { // Transport becomes active. We should schedule keepalive pings. keepAliveManager.onTransportActive(); ArgumentCaptor<Runnable> sendPingCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(1)).schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable sendPing = sendPingCaptor.getValue(); ScheduledFuture<?> shutdownFuture = mock(ScheduledFuture.class); doReturn(shutdownFuture) .when(scheduler).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Mannually running the Runnable will send the ping. Shutdown task should be scheduled. ticker.time = 1000; sendPing.run(); verify(keepAlivePinger).ping(); ArgumentCaptor<Runnable> shutdownCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(2)).schedule(shutdownCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable shutdown = shutdownCaptor.getValue(); // We do not receive the ping response. Shutdown runnable runs. // TODO(zdapeng): use FakeClock.ScheduledExecutorService ticker.time = 3000; shutdown.run(); verify(keepAlivePinger).onPingTimeout(); // We receive the ping response too late. keepAliveManager.onDataReceived(); // No more ping should be scheduled. verify(scheduler, times(2)).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); } @Test public void transportGoesIdle() { // Transport becomes active. We should schedule keepalive pings. keepAliveManager.onTransportActive(); ArgumentCaptor<Runnable> sendPingCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(1)).schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable sendPing = sendPingCaptor.getValue(); // Transport becomes idle. Nothing should happen when ping runnable runs. keepAliveManager.onTransportIdle(); sendPing.run(); // Ping was not sent. verify(transport, times(0)).ping(isA(ClientTransport.PingCallback.class), isA(Executor.class)); // No new ping got scheduled. verify(scheduler, times(1)).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); } @Test public void transportGoesIdle_doesntCauseIdleWhenEnabled() { keepAliveManager.onTransportTermination(); keepAliveManager = new KeepAliveManager(keepAlivePinger, scheduler, ticker, 1000, 2000, true); keepAliveManager.onTransportStarted(); // Keepalive scheduling should have started immediately. ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler).schedule(runnableCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable sendPing = runnableCaptor.getValue(); keepAliveManager.onTransportActive(); // Transport becomes idle. Should not impact the sending of the ping. keepAliveManager.onTransportIdle(); sendPing.run(); // Ping was sent. verify(keepAlivePinger).ping(); // Shutdown is scheduled. verify(scheduler, times(2)).schedule(runnableCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); // Shutdown is triggered. runnableCaptor.getValue().run(); verify(keepAlivePinger).onPingTimeout(); } @Test public void transportGoesIdleAfterPingSent() { // Transport becomes active. We should schedule keepalive pings. keepAliveManager.onTransportActive(); ArgumentCaptor<Runnable> sendPingCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(1)).schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable sendPing = sendPingCaptor.getValue(); ScheduledFuture<?> shutdownFuture = mock(ScheduledFuture.class); doReturn(shutdownFuture) .when(scheduler).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Mannually running the Runnable will send the ping. Shutdown task should be scheduled. // TODO(zdapeng): user FakeClock.ScheduledExecutorService ticker.time = 1000; sendPing.run(); verify(keepAlivePinger).ping(); verify(scheduler, times(2)).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Transport becomes idle. No more ping should be scheduled after we receive a ping response. keepAliveManager.onTransportIdle(); ticker.time = 1100; keepAliveManager.onDataReceived(); verify(scheduler, times(2)).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Shutdown task has been cancelled. verify(shutdownFuture).cancel(isA(Boolean.class)); // Transport becomes active again. Another ping is scheduled. keepAliveManager.onTransportActive(); verify(scheduler, times(3)).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); } @Test public void transportShutsdownAfterPingScheduled() { ScheduledFuture<?> pingFuture = mock(ScheduledFuture.class); doReturn(pingFuture) .when(scheduler).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Ping will be scheduled. keepAliveManager.onTransportActive(); verify(scheduler, times(1)).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Transport is shutting down. keepAliveManager.onTransportTermination(); // Ping future should have been cancelled. verify(pingFuture).cancel(isA(Boolean.class)); } @Test public void transportShutsdownAfterPingSent() { keepAliveManager.onTransportActive(); ArgumentCaptor<Runnable> sendPingCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(1)).schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable sendPing = sendPingCaptor.getValue(); ScheduledFuture<?> shutdownFuture = mock(ScheduledFuture.class); doReturn(shutdownFuture) .when(scheduler).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Mannually running the Runnable will send the ping. Shutdown task should be scheduled. ticker.time = 1000; sendPing.run(); verify(keepAlivePinger).ping(); verify(scheduler, times(2)).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Transport is shutting down. keepAliveManager.onTransportTermination(); // Shutdown task has been cancelled. verify(shutdownFuture).cancel(isA(Boolean.class)); } @Test public void pingSentThenIdleThenActiveThenAck() { keepAliveManager.onTransportActive(); ArgumentCaptor<Runnable> sendPingCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, times(1)) .schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); Runnable sendPing = sendPingCaptor.getValue(); ScheduledFuture<?> shutdownFuture = mock(ScheduledFuture.class); // ping scheduled doReturn(shutdownFuture) .when(scheduler).schedule(isA(Runnable.class), isA(Long.class), isA(TimeUnit.class)); // Mannually running the Runnable will send the ping. ticker.time = 1000; sendPing.run(); // shutdown scheduled verify(scheduler, times(2)) .schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); verify(keepAlivePinger).ping(); keepAliveManager.onTransportIdle(); keepAliveManager.onTransportActive(); keepAliveManager.onDataReceived(); // another ping scheduled verify(scheduler, times(3)) .schedule(sendPingCaptor.capture(), isA(Long.class), isA(TimeUnit.class)); } }