/*
* 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;
import com.google.common.collect.Lists;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.ListenableFuture;
import com.spotify.folsom.client.NoopMetrics;
import com.spotify.folsom.client.Utils;
import com.thimbleware.jmemcached.Cache;
import com.thimbleware.jmemcached.CacheElement;
import com.thimbleware.jmemcached.Key;
import com.thimbleware.jmemcached.LocalCacheElement;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import java.util.List;
import java.util.concurrent.ExecutionException;
import static io.netty.util.CharsetUtil.UTF_8;
import static org.hamcrest.Matchers.is;
import static org.jboss.netty.buffer.ChannelBuffers.copiedBuffer;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class RecoveryTest {
private static final int MAX_OUTSTANDING_REQUESTS = 10;
private static final int TIMEOUT_MILLIS = 200;
@Mock Cache<CacheElement> cache;
private EmbeddedServer server;
private MemcacheClient<String> client;
@Before
public void setUp() throws Exception {
server = new EmbeddedServer(true, cache);
int port = server.getPort();
final MemcacheClientBuilder<String> builder = MemcacheClientBuilder.newStringClient()
.withAddress(HostAndPort.fromParts("127.0.0.1", port))
.withConnections(1)
.withMaxOutstandingRequests(MAX_OUTSTANDING_REQUESTS)
.withMetrics(NoopMetrics.INSTANCE)
.withRetry(false)
.withReplyExecutor(Utils.SAME_THREAD_EXECUTOR)
.withRequestTimeoutMillis(TIMEOUT_MILLIS);
client = builder.connectBinary();
ConnectFuture.connectFuture(client).get();
}
@After
public void tearDown() throws Exception {
if (client != null) {
client.shutdown();
}
if (server != null) {
server.stop();
}
}
@Test
public void testOverloadAndTimeoutRecovery() throws Exception {
// Have memcached block indefinitely on all GET requests
final GetAnswer answer = new GetAnswer();
when(cache.get(any(Key[].class))).then(answer);
// Overload the client
final int overload = 10;
final List<ListenableFuture<String>> overloadFutures = Lists.newArrayList();
for (int i = 0; i < MAX_OUTSTANDING_REQUESTS + overload; i++) {
overloadFutures.add(client.get("foo"));
}
int timeout = 0;
int overloaded = 0;
for (final ListenableFuture<String> f : overloadFutures) {
try {
f.get();
} catch (ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof MemcacheOverloadedException) {
overloaded++;
} else if (cause instanceof MemcacheClosedException &&
cause.getMessage().contains("Timeout")) {
timeout++;
}
}
}
// Verify that the expected number of requests failed with MemcacheOverloadedException
assertThat(overloaded, is(overload));
// Verify that the rest failed with TimeoutException
assertThat(timeout, is(MAX_OUTSTANDING_REQUESTS));
// Wait for the client to reconnect
ConnectFuture.connectFuture(client).get();
// Have memcached reply to all GET requests
answer.set(elements("foo", "bar"));
// Verify that the client recovers and successfully processes requests
final List<ListenableFuture<String>> recoveryFutures = Lists.newArrayList();
for (int i = 0; i < MAX_OUTSTANDING_REQUESTS; i++) {
recoveryFutures.add(client.get("foo"));
}
for (final ListenableFuture<String> f : recoveryFutures) {
f.get();
}
}
private CacheElement[] elements(final String... keyValues) {
final CacheElement[] elements = new CacheElement[keyValues.length / 2];
for (int i = 0; i < elements.length; i++) {
elements[i] = element(keyValues[i * 2], keyValues[i * 2 + 1]);
}
return elements;
}
private LocalCacheElement element(final String key1, final String value1) {
final LocalCacheElement e = new LocalCacheElement(new Key(copiedBuffer(key1, UTF_8)));
e.setData(copiedBuffer(value1, UTF_8));
return e;
}
private static class GetAnswer extends AbstractFuture<CacheElement[]>
implements Answer<CacheElement[]> {
@Override
public boolean set(final CacheElement[] value) {
return super.set(value);
}
@Override
public CacheElement[] answer(final InvocationOnMock invocationOnMock) throws Throwable {
return get();
}
}
}