/*
* Copyright (c) 2014-2015 Spotify AB
*
* 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 com.spotify.folsom.reconnect;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.spotify.folsom.AbstractRawMemcacheClient;
import com.spotify.folsom.BackoffFunction;
import com.spotify.folsom.ConnectFuture;
import com.spotify.folsom.ConnectionChangeListener;
import com.spotify.folsom.RawMemcacheClient;
import com.spotify.folsom.client.Request;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class ReconnectingClientTest {
private final ScheduledExecutorService scheduledExecutorService =
mock(ScheduledExecutorService.class);
@Before
public void setUp() throws Exception {
when(scheduledExecutorService
.schedule(Mockito.<Runnable>any(), anyLong(), Matchers.<TimeUnit>any()))
.thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
Runnable runnable = (Runnable) invocationOnMock.getArguments()[0];
runnable.run();
return null;
}
});
}
@Test
public void testInitialConnect() throws Exception {
FakeClient delegate = new FakeClient(true, false);
BackoffFunction backoffFunction = mock(BackoffFunction.class);
when(backoffFunction.getBackoffTimeMillis(0)).thenReturn(0L);
when(backoffFunction.getBackoffTimeMillis(1)).thenReturn(123L);
ReconnectingClient.Connector connector = mock(ReconnectingClient.Connector.class);
when(connector.connect())
.thenReturn(Futures.<RawMemcacheClient>immediateFailedFuture(new RuntimeException()))
.thenReturn(Futures.<RawMemcacheClient>immediateFailedFuture(new RuntimeException()))
.thenReturn(Futures.<RawMemcacheClient>immediateFuture(delegate));
ReconnectingClient client = new ReconnectingClient(
backoffFunction, scheduledExecutorService,
connector, HostAndPort.fromString("localhost:123"));
verify(connector, times(3)).connect();
verify(scheduledExecutorService, times(1))
.schedule(Mockito.<Runnable>any(), eq(0L), eq(TimeUnit.MILLISECONDS));
verify(scheduledExecutorService, times(1))
.schedule(Mockito.<Runnable>any(), eq(123L), eq(TimeUnit.MILLISECONDS));
verify(backoffFunction, times(1)).getBackoffTimeMillis(0);
verify(backoffFunction, times(1)).getBackoffTimeMillis(1);
verifyNoMoreInteractions(connector, scheduledExecutorService, backoffFunction);
assertTrue(client.isConnected());
}
@Test
public void testLostConnectionRetry() throws Exception {
FakeClient delegate1 = new FakeClient(true, true);
FakeClient delegate2 = new FakeClient(true, false);
BackoffFunction backoffFunction = mock(BackoffFunction.class);
when(backoffFunction.getBackoffTimeMillis(0)).thenReturn(0L);
when(backoffFunction.getBackoffTimeMillis(1)).thenReturn(123L);
ReconnectingClient.Connector connector = mock(ReconnectingClient.Connector.class);
when(connector.connect())
.thenReturn(Futures.<RawMemcacheClient>immediateFuture(delegate1))
.thenReturn(Futures.<RawMemcacheClient>immediateFailedFuture(new RuntimeException()))
.thenReturn(Futures.<RawMemcacheClient>immediateFailedFuture(new RuntimeException()))
.thenReturn(Futures.<RawMemcacheClient>immediateFuture(delegate2));
ReconnectingClient client = new ReconnectingClient(
backoffFunction, scheduledExecutorService,
connector, HostAndPort.fromString("localhost:123"));
verify(connector, times(4)).connect();
verify(scheduledExecutorService, times(1))
.schedule(Mockito.<Runnable>any(), eq(0L), eq(TimeUnit.MILLISECONDS));
verify(scheduledExecutorService, times(1))
.schedule(Mockito.<Runnable>any(), eq(123L), eq(TimeUnit.MILLISECONDS));
verify(backoffFunction, times(1)).getBackoffTimeMillis(0);
verify(backoffFunction, times(1)).getBackoffTimeMillis(1);
verifyNoMoreInteractions(connector, scheduledExecutorService, backoffFunction);
assertTrue(client.isConnected());
}
@Test
public void testShutdown() throws Exception {
FakeClient delegate = new FakeClient(true, false);
BackoffFunction backoffFunction = mock(BackoffFunction.class);
when(backoffFunction.getBackoffTimeMillis(0)).thenReturn(0L);
when(backoffFunction.getBackoffTimeMillis(1)).thenReturn(123L);
ReconnectingClient.Connector connector = mock(ReconnectingClient.Connector.class);
when(connector.connect())
.thenReturn(Futures.<RawMemcacheClient>immediateFuture(delegate));
ReconnectingClient client = new ReconnectingClient(
backoffFunction, scheduledExecutorService,
connector, HostAndPort.fromString("localhost:123"));
assertTrue(client.isConnected());
client.shutdown();
ConnectFuture.disconnectFuture(client).get();
assertFalse(client.isConnected());
}
private static class FakeClient extends AbstractRawMemcacheClient {
private boolean connected;
private boolean disconnectImmediately;
private FakeClient(boolean connected, boolean disconnectImmediately) {
this.connected = connected;
this.disconnectImmediately = disconnectImmediately;
}
@Override
public <T> ListenableFuture<T> send(Request<T> request) {
throw new RuntimeException("Not implemented");
}
@Override
public void shutdown() {
connected = false;
notifyConnectionChange();
}
@Override
public boolean isConnected() {
return connected;
}
@Override
public int numTotalConnections() {
return 0;
}
@Override
public int numActiveConnections() {
return 0;
}
@Override
public void registerForConnectionChanges(ConnectionChangeListener listener) {
super.registerForConnectionChanges(listener);
if (disconnectImmediately) {
connected = false;
notifyConnectionChange();
}
}
}
}