/*
* 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.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.spotify.folsom.client.NoopMetrics;
import com.spotify.folsom.client.Utils;
import junit.framework.AssertionFailedError;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public class IntegrationTest {
private static final HostAndPort DEFAULT_SERVER_ADDRESS
= HostAndPort.fromParts("127.0.0.1", 11211);
private static EmbeddedServer asciiEmbeddedServer;
private static EmbeddedServer binaryEmbeddedServer;
private static HostAndPort getServerAddress() {
String addressOverride = System.getProperty("memcached-address");
if (addressOverride != null) {
return HostAndPort.fromString(addressOverride).withDefaultPort(11211);
} else {
return DEFAULT_SERVER_ADDRESS;
}
}
private static boolean memcachedRunning() {
try (Socket socket = new Socket()) {
HostAndPort serverAddress = getServerAddress();
socket.connect(new InetSocketAddress(serverAddress.getHostText(), serverAddress.getPort()));
return true;
} catch (ConnectException e) {
System.err.println("memcached not running, disabling test");
return false;
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
@Parameterized.Parameters(name = "{0}-{1}")
public static Collection<Object[]> data() throws Exception {
ArrayList<Object[]> res = Lists.newArrayList();
res.add(new Object[]{"embedded", "ascii"});
res.add(new Object[]{"embedded", "binary"});
if (memcachedRunning()) {
res.add(new Object[]{"integration", "ascii"});
res.add(new Object[]{"integration", "binary"});
}
return res;
}
@Parameterized.Parameter(0)
public String type;
@Parameterized.Parameter(1)
public String protocol;
private MemcacheClient<String> client;
private AsciiMemcacheClient<String> asciiClient;
private BinaryMemcacheClient<String> binaryClient;
@BeforeClass
public static void setUpClass() throws Exception {
asciiEmbeddedServer = new EmbeddedServer(false);
binaryEmbeddedServer = new EmbeddedServer(true);
}
@AfterClass
public static void tearDownClass() throws Exception {
binaryEmbeddedServer.stop();
asciiEmbeddedServer.stop();
}
@Before
public void setUp() throws Exception {
boolean ascii;
if (protocol.equals("ascii")) {
ascii = true;
} else if (protocol.equals("binary")) {
ascii = false;
} else {
throw new IllegalArgumentException(protocol);
}
HostAndPort integrationServer = getServerAddress();
int embeddedPort = ascii ? asciiEmbeddedServer.getPort() : binaryEmbeddedServer.getPort();
String address = isEmbedded() ? "127.0.0.1" : integrationServer.getHostText();
int port = isEmbedded() ? embeddedPort : integrationServer.getPort();
MemcacheClientBuilder<String> builder = MemcacheClientBuilder.newStringClient()
.withAddress(HostAndPort.fromParts(address, port))
.withConnections(1)
.withMaxOutstandingRequests(1000)
.withMetrics(NoopMetrics.INSTANCE)
.withRetry(false)
.withReplyExecutor(Utils.SAME_THREAD_EXECUTOR)
.withRequestTimeoutMillis(100);
if (ascii) {
asciiClient = builder.connectAscii();
binaryClient = null;
client = asciiClient;
} else {
binaryClient = builder.connectBinary();
asciiClient = null;
client = binaryClient;
}
ConnectFuture.connectFuture(client).get();
System.out.printf("Using client: %s protocol: %s and port: %d\n", client, protocol, port);
cleanup();
}
private void cleanup() throws ExecutionException, InterruptedException {
for (String key : ALL_KEYS) {
client.delete(key).get();
}
}
@After
public void tearDown() throws Exception {
cleanup();
client.shutdown();
ConnectFuture.disconnectFuture(client).get();
}
protected static final String KEY1 = "folsomtest:key1";
protected static final String KEY2 = "folsomtest:key2";
protected static final String KEY3 = "folsomtest:key3";
protected static final String KEY4 = "folsomtest:keywithÅÄÖ";
protected static final String VALUE1 = "val1";
protected static final String VALUE2 = "val2";
protected static final String NUMERIC_VALUE = "123";
public static final List<String> ALL_KEYS = ImmutableList.of(KEY1, KEY2, KEY3, KEY4);
protected static final int TTL = Integer.MAX_VALUE;
@Test
public void testSetGet() throws Exception {
client.set(KEY1, VALUE1, TTL).get();
assertEquals(VALUE1, client.get(KEY1).get());
}
@Test
public void testSetGetWithUTF8() throws Exception {
client.set(KEY4, VALUE1, TTL).get();
assertEquals(VALUE1, client.get(KEY4).get());
}
@Test
public void testLargeSet() throws Exception {
String value = createValue(100000);
client.set(KEY1, value, TTL).get();
assertEquals(value, client.get(KEY1).get());
}
@Test
public void testAppend() throws Exception {
client.set(KEY1, VALUE1, TTL).get();
client.append(KEY1, VALUE2).get();
assertEquals(VALUE1 + VALUE2, client.get(KEY1).get());
}
@Test
public void testAppendMissing() throws Throwable {
checkStatus(client.append(KEY1, VALUE2), MemcacheStatus.ITEM_NOT_STORED);
}
@Test
public void testPrependMissing() throws Throwable {
checkStatus(client.prepend(KEY1, VALUE2), MemcacheStatus.ITEM_NOT_STORED);
}
@Test
public void testAppendCas() throws Exception {
assumeBinary();
binaryClient.set(KEY1, VALUE1, TTL).get();
final long cas = binaryClient.casGet(KEY1).get().getCas();
binaryClient.append(KEY1, VALUE2, cas).get();
assertEquals(VALUE1 + VALUE2, binaryClient.get(KEY1).get());
}
@Test
public void testAppendCasIncorrect() throws Throwable {
assumeBinary();
assumeNotEmbedded("CAS is broken on embedded server");
binaryClient.set(KEY1, VALUE1, TTL).get();
final long cas = binaryClient.casGet(KEY1).get().getCas();
checkStatus(binaryClient.append(KEY1, VALUE2, cas + 666), MemcacheStatus.KEY_EXISTS);
}
@Test
public void testPrependCasIncorrect() throws Throwable {
assumeBinary();
assumeNotEmbedded("CAS is broken on embedded server");
binaryClient.set(KEY1, VALUE1, TTL).get();
final long cas = binaryClient.casGet(KEY1).get().getCas();
checkStatus(binaryClient.prepend(KEY1, VALUE2, cas + 666), MemcacheStatus.KEY_EXISTS);
}
@Test
public void testPrependCas() throws Exception {
assumeBinary();
binaryClient.set(KEY1, VALUE1, TTL).get();
final long cas = binaryClient.casGet(KEY1).get().getCas();
binaryClient.prepend(KEY1, VALUE2, cas).get();
assertEquals(VALUE2 + VALUE1, binaryClient.get(KEY1).get());
}
@Test
public void testPrepend() throws Exception {
client.set(KEY1, VALUE1, TTL).get();
client.prepend(KEY1, VALUE2).get();
assertEquals(VALUE2 + VALUE1, client.get(KEY1).get());
}
@Test
public void testSetDeleteGet() throws Throwable {
assumeBinary();
assumeNotEmbedded("returns \"\" instead of NOT_FOUND on embedded server");
client.set(KEY1, VALUE1, TTL).get();
client.delete(KEY1).get();
assertGetKeyNotFound(client.get(KEY1));
}
@Test
public void testAddGet() throws Exception {
client.add(KEY1, VALUE1, TTL).get();
assertEquals(VALUE1, client.get(KEY1).get());
}
@Test
public void testAddKeyExists() throws Throwable {
client.set(KEY1, VALUE1, TTL).get();
checkStatus(client.add(KEY1, VALUE1, TTL), MemcacheStatus.ITEM_NOT_STORED);
}
@Test
public void testReplaceGet() throws Exception {
client.set(KEY1, VALUE1, TTL).get();
client.replace(KEY1, VALUE2, TTL).get();
assertEquals(VALUE2, client.get(KEY1).get());
}
@Test
public void testGetKeyNotExist() throws Throwable {
assumeBinary();
assumeNotEmbedded("returns \"\" instead of NOT_FOUND on embedded server");
assertGetKeyNotFound(client.get(KEY1));
}
@Test
public void testPartitionMultiget() throws Exception {
final List<String> keys = Lists.newArrayList();
for (int i = 0; i < 500; i++) {
keys.add("key-" + i);
}
final List<ListenableFuture<MemcacheStatus>> futures = Lists.newArrayList();
try {
for (final String key : keys) {
futures.add(client.set(key, key, TTL));
}
Futures.allAsList(futures).get();
final ListenableFuture<List<String>> resultsFuture = client.get(keys);
final List<String> results = resultsFuture.get();
assertEquals(keys, results);
} finally {
futures.clear();
for (final String key : keys) {
futures.add(client.delete(key));
}
Futures.allAsList(futures).get();
}
}
@Test(expected = IllegalArgumentException.class)
public void testTooLongKey() throws Exception {
client.get(String.format("key-%300d", 1));
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidKey() throws Exception {
client.get("key with spaces");
}
@Test
public void testMultiGetAllKeysExistsIterable() throws Throwable {
client.set(KEY1, VALUE1, TTL).get();
client.set(KEY2, VALUE2, TTL).get();
assertEquals(asList(VALUE1, VALUE2), client.get(asList(KEY1, KEY2)).get());
}
@Test
public void testMultiGetWithCas() throws Throwable {
client.set(KEY1, VALUE1, TTL).get();
client.set(KEY2, VALUE2, TTL).get();
long cas1 = client.casGet(KEY1).get().getCas();
long cas2 = client.casGet(KEY2).get().getCas();
List<GetResult<String>> expected = asList(
GetResult.success(VALUE1, cas1),
GetResult.success(VALUE2, cas2));
assertEquals(expected, client.casGet(asList(KEY1, KEY2)).get());
}
@Test
public void testMultiGetKeyMissing() throws Throwable {
assumeBinary();
assumeNotEmbedded("returns \"\" instead of NOT_FOUND on embedded server");
client.set(KEY1, VALUE1, TTL).get();
client.set(KEY2, VALUE2, TTL).get();
assertEquals(asList(VALUE1, null, VALUE2), client.get(Arrays.asList(KEY1, KEY3, KEY2)).get());
}
@Test
public void testDeleteKeyNotExist() throws Throwable {
checkKeyNotFound(client.delete(KEY1));
}
@Test
public void testIncr() throws Throwable {
assumeBinary();
assumeNotEmbedded("INCR/DECR gives NPE on embedded server");
assertEquals(new Long(3), binaryClient.incr(KEY1, 2, 3, TTL).get());
assertEquals(new Long(5), binaryClient.incr(KEY1, 2, 7, TTL).get());
}
private boolean isEmbedded() {
return type.equals("embedded");
}
private boolean isAscii() {
return protocol.equals("ascii");
}
private boolean isBinary() {
return protocol.equals("binary");
}
@Test
public void testDecr() throws Throwable {
assumeBinary();
assumeNotEmbedded("INCR/DECR gives NPE on embedded server");
assertEquals(new Long(3), binaryClient.decr(KEY1, 2, 3, TTL).get());
assertEquals(new Long(1), binaryClient.decr(KEY1, 2, 5, TTL).get());
}
@Test
public void testDecrBelowZero() throws Throwable {
assumeBinary();
assumeNotEmbedded("INCR/DECR gives NPE on embedded server");
assertEquals(new Long(3), binaryClient.decr(KEY1, 2, 3, TTL).get());
// should not go below 0
assertEquals(new Long(0), binaryClient.decr(KEY1, 4, 5, TTL).get());
}
@Test
public void testIncrDefaults() throws Throwable {
assumeBinary();
assumeNotEmbedded("INCR/DECR gives NPE on embedded server");
// Ascii client behaves differently for this use case
assertEquals(new Long(0), binaryClient.incr(KEY1, 2, 0, 0).get());
// memcached will intermittently cause this test to fail due to the value not being set
// correctly when incr with initial=0 is used, this works around that
binaryClient.set(KEY1, "0", TTL).get();
assertEquals(new Long(2), binaryClient.incr(KEY1, 2, 0, 0).get());
}
@Test
public void testIncrDefaultsAscii() throws Throwable {
assumeAscii();
// Ascii client behaves differently for this use case
assertEquals(null, asciiClient.incr(KEY1, 2).get());
// memcached will intermittently cause this test to fail due to the value not being set
// correctly when incr with initial=0 is used, this works around that
asciiClient.set(KEY1, "0", TTL).get();
assertEquals(new Long(2), asciiClient.incr(KEY1, 2).get());
}
@Test
public void testIncrAscii() throws Throwable {
assumeAscii();
asciiClient.set(KEY1, NUMERIC_VALUE, TTL).get();
assertEquals(new Long(125), asciiClient.incr(KEY1, 2).get());
}
@Test
public void testDecrAscii() throws Throwable {
assumeAscii();
asciiClient.set(KEY1, NUMERIC_VALUE, TTL).get();
assertEquals(new Long(121), asciiClient.decr(KEY1, 2).get());
}
@Test
public void testCas() throws Throwable {
assumeBinary();
assumeNotEmbedded("CAS is broken on embedded server");
client.set(KEY1, VALUE1, TTL).get();
final GetResult<String> getResult1 = client.casGet(KEY1).get();
assertNotNull(getResult1.getCas());
assertEquals(VALUE1, getResult1.getValue());
client.set(KEY1, VALUE2, TTL, getResult1.getCas()).get();
final long newCas = client.casGet(KEY1).get().getCas();
assertNotEquals(0, newCas);
final GetResult<String> getResult2 = client.casGet(KEY1).get();
assertEquals(newCas, getResult2.getCas());
assertEquals(VALUE2, getResult2.getValue());
}
@Test
public void testCasNotMatchingSet() throws Throwable {
assumeBinary();
assumeNotEmbedded("CAS is broken on embedded server");
client.set(KEY1, VALUE1, TTL).get();
checkStatus(client.set(KEY1, VALUE2, TTL, 666), MemcacheStatus.KEY_EXISTS);
}
@Test
public void testTouchNotFound() throws Throwable {
assumeNotEmbedded("touch is broken on embedded server");
MemcacheStatus result = client.touch(KEY1, TTL).get();
assertEquals(MemcacheStatus.KEY_NOT_FOUND, result);
}
@Test
public void testTouchFound() throws Throwable {
assumeNotEmbedded("touch is broken on embedded server");
client.set(KEY1, VALUE1, TTL).get();
MemcacheStatus result = client.touch(KEY1, TTL).get();
assertEquals(MemcacheStatus.OK, result);
}
@Test
public void testTouchAndGetFound() throws Throwable {
assumeBinary();
assumeNotEmbedded("touch is broken on embedded server");
client.set(KEY1, VALUE1, TTL).get();
assertEquals(VALUE1, binaryClient.getAndTouch(KEY1, TTL).get());
}
@Test
public void testTouchAndGetNotFound() throws Throwable {
assumeBinary();
assumeNotEmbedded("touch is broken on embedded server");
assertNull(binaryClient.getAndTouch(KEY1, TTL).get());
}
@Test
public void testCasTouchAndGetFound() throws Throwable {
assumeBinary();
assumeNotEmbedded("touch is broken on embedded server");
client.set(KEY1, VALUE1, TTL).get();
long cas = client.casGet(KEY1).get().getCas();
GetResult<String> result = binaryClient.casGetAndTouch(KEY1, TTL).get();
assertEquals(cas, result.getCas());
assertEquals(VALUE1, result.getValue());
}
@Test
public void testCasTouchAndGetNotFound() throws Throwable {
assumeBinary();
assumeNotEmbedded("touch is broken on embedded server");
GetResult<String> result = binaryClient.casGetAndTouch(KEY1, TTL).get();
assertNull(result);
}
@Test
public void testNoop() throws Throwable {
assumeBinary();
assumeNotEmbedded("touch is broken on embedded server");
binaryClient.noop().get();
}
@Test
public void testAlmostTooLargeValues() throws Exception {
String value = createValue(1024 * 1024 - 1000);
assertEquals(MemcacheStatus.OK, client.set(KEY1, value, TTL).get());
assertEquals(value, client.get(KEY1).get());
}
@Test
public void testTooLargeValues() throws Exception {
String value = createValue(2 * 1024 * 1024);
assertEquals(MemcacheStatus.OK, client.set(KEY1, "", TTL).get());
assertEquals(MemcacheStatus.VALUE_TOO_LARGE, client.set(KEY1, value, TTL).get());
assertEquals("", client.get(KEY1).get());
}
@Test
public void testTooLargeValueDoesNotCorruptConnection() throws Exception {
String largeValue = createValue(2 * 1024 * 1024);
client.set(KEY1, largeValue, TTL).get();
String smallValue = createValue(1000);
for (int i = 0; i < 1000; i++) {
assertEquals(MemcacheStatus.OK, client.set(KEY1, smallValue, TTL).get());
assertEquals(smallValue, client.get(KEY1).get());
}
}
private String createValue(int size) {
StringBuilder sb = new StringBuilder(size);
for (int i = 0; i < size; i++) {
sb.append('.');
}
return sb.toString();
}
private void assumeNotEmbedded(String msg) {
assumeFalse(msg, isEmbedded());
}
private void assumeAscii() {
assumeTrue(isAscii());
}
private void assumeBinary() {
assumeTrue(isBinary());
}
static void assertGetKeyNotFound(ListenableFuture<String> future) throws Throwable {
checkKeyNotFound(future);
}
static void checkKeyNotFound(final ListenableFuture<?> future) throws Throwable {
checkStatus(future, MemcacheStatus.KEY_NOT_FOUND);
}
static void checkStatus(final ListenableFuture<?> future, final MemcacheStatus... expected)
throws Throwable {
final Set<MemcacheStatus> expectedSet = Sets.newHashSet(expected);
try {
Object v = future.get();
if (v instanceof MemcacheStatus) {
final MemcacheStatus status = (MemcacheStatus) v;
if (!expectedSet.contains(status)) {
throw new AssertionFailedError(status + " not in " + expectedSet);
}
} else {
if (v == null && expectedSet.contains(MemcacheStatus.KEY_NOT_FOUND)) {
// ok
} else if (v != null && expectedSet.contains(MemcacheStatus.OK)) {
// ok
} else {
throw new AssertionFailedError();
}
}
} catch (final ExecutionException e) {
throw e.getCause();
}
}
}