/* * Copyright (c) 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.client; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.net.HostAndPort; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.spotify.folsom.ConnectFuture; import com.spotify.folsom.EmbeddedServer; import com.spotify.folsom.GetResult; import com.spotify.folsom.MemcacheClosedException; import com.spotify.folsom.RawMemcacheClient; import com.spotify.folsom.client.ascii.AsciiRequest; import com.spotify.folsom.client.ascii.AsciiResponse; import com.spotify.folsom.client.ascii.DefaultAsciiMemcacheClient; import com.spotify.folsom.client.ascii.GetRequest; import com.spotify.folsom.transcoder.StringTranscoder; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class DefaultRawMemcacheClientTest { private EmbeddedServer asciiServer; private EmbeddedServer binaryServer; @Before public void setUpBinaryServer() throws Exception { binaryServer = new EmbeddedServer(true); } @Before public void setUpAsciiServer() throws Exception { asciiServer = new EmbeddedServer(false); } @After public void tearDownAsciiServer() throws Exception { asciiServer.stop(); } @After public void tearDownBinaryServer() throws Exception { binaryServer.stop(); } @Test public void testInvalidRequest() throws Exception { final String exceptionString = "Crash the client"; RawMemcacheClient rawClient = DefaultRawMemcacheClient.connect( HostAndPort.fromParts("localhost", asciiServer.getPort()), 5000, false, null, 3000, Charsets.UTF_8, new NoopMetrics(), 1024 * 1024 ).get(); DefaultAsciiMemcacheClient<String> asciiClient = new DefaultAsciiMemcacheClient<>( rawClient, new NoopMetrics(), new StringTranscoder(Charsets.UTF_8), Charsets.UTF_8); List<ListenableFuture<?>> futures = Lists.newArrayList(); for (int i = 0; i < 2; i++) { futures.add(asciiClient.set("key", "value" + i, 0)); } sendFailRequest(exceptionString, rawClient); for (int i = 0; i < 2; i++) { futures.add(asciiClient.set("key", "value" + i, 0)); } assertFalse(rawClient.isConnected()); int total = futures.size(); int stuck = 0; StringBuilder sb = new StringBuilder(); long t1 = System.currentTimeMillis(); int i = 0; for (ListenableFuture<?> future : futures) { try { long elapsed = System.currentTimeMillis() - t1; future.get(Math.max(0, 1000 - elapsed), TimeUnit.MILLISECONDS); sb.append('.'); } catch (ExecutionException e) { assertEquals(MemcacheClosedException.class, e.getCause().getClass()); sb.append('.'); } catch (TimeoutException e) { sb.append('X'); stuck++; } i++; if (0 == (i % 50)) { sb.append("\n"); } } assertEquals(stuck + " out of " + total + " requests got stuck:\n" + sb.toString(), 0, stuck); } private void sendFailRequest(final String exceptionString, RawMemcacheClient rawClient) throws InterruptedException { try { rawClient.send(new AsciiRequest<String>("key", Charsets.UTF_8) { @Override protected void handle(AsciiResponse response) throws IOException { throw new IOException(exceptionString); } @Override public ByteBuf writeRequest(ByteBufAllocator alloc, ByteBuffer dst) { dst.put("invalid command".getBytes()); dst.put(NEWLINE_BYTES); return toBuffer(alloc, dst); } }).get(); fail(); } catch (ExecutionException e) { assertEquals("Unexpected line: CLIENT_ERROR", e.getCause().getMessage()); } } @Test public void testRequestTimeout() throws IOException, ExecutionException, InterruptedException { final ServerSocket server = new ServerSocket(); server.bind(null); final HostAndPort address = HostAndPort.fromParts("127.0.0.1", server.getLocalPort()); RawMemcacheClient rawClient = DefaultRawMemcacheClient.connect( address, 5000, false, null, 1000, Charsets.UTF_8, new NoopMetrics(), 1024 * 1024).get(); final Future<?> future = rawClient.send(new GetRequest("foo", Charsets.UTF_8, false)); try { future.get(); fail(); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof MemcacheClosedException); } } @Test public void testBinaryRequestRetry() throws Exception { final int outstandingRequestLimit = 5000; final boolean binary = true; final Executor executor = MoreExecutors.getExitingExecutorService( (ThreadPoolExecutor) Executors.newCachedThreadPool()); final int timeoutMillis = 3000; final int maxSetLength = 1024 * 1024; final HostAndPort address = HostAndPort.fromParts("localhost", binaryServer.getPort()); final RawMemcacheClient rawClient = DefaultRawMemcacheClient.connect( address, outstandingRequestLimit, binary, executor, timeoutMillis, Charsets.UTF_8, new NoopMetrics(), maxSetLength).get(); final com.spotify.folsom.client.binary.GetRequest request = new com.spotify.folsom.client.binary.GetRequest("foo", Charsets.UTF_8, OpCode.GET, 123); // Send request once rawClient.send(request).get(); binaryServer.flush(); // Pretend that the above request failed and retry it by sending it again rawClient.send(request).get(); } @Test public void testAsciiRequestRetry() throws Exception { final boolean binary = false; final int outstandingRequestLimit = 5000; final Executor executor = MoreExecutors.getExitingExecutorService( (ThreadPoolExecutor) Executors.newCachedThreadPool()); final int timeoutMillis = 3000; final int maxSetLength = 1024 * 1024; final HostAndPort address = HostAndPort.fromParts("localhost", asciiServer.getPort()); final RawMemcacheClient rawClient = DefaultRawMemcacheClient.connect( address, outstandingRequestLimit, binary, executor, timeoutMillis, Charsets.UTF_8, new NoopMetrics(), maxSetLength).get(); final com.spotify.folsom.client.ascii.GetRequest request = new com.spotify.folsom.client.ascii.GetRequest("foo", Charsets.UTF_8, false); // Send request once rawClient.send(request).get(); asciiServer.flush(); // Pretend that the above request failed and retry it by sending it again rawClient.send(request).get(); } @Test public void testShutdown() throws IOException, ExecutionException, InterruptedException { final ServerSocket server = new ServerSocket(); server.bind(null); final HostAndPort address = HostAndPort.fromParts("127.0.0.1", server.getLocalPort()); RawMemcacheClient rawClient = DefaultRawMemcacheClient.connect( address, 5000, false, null, 1000, Charsets.UTF_8, new NoopMetrics(), 1024 * 1024).get(); rawClient.shutdown(); ConnectFuture.disconnectFuture(rawClient).get(); assertFalse(rawClient.isConnected()); } @Test(expected = MemcacheClosedException.class) public void testShutdownRequestExceptionInsteadOfOverloaded() throws Throwable { final ServerSocket server = new ServerSocket(); server.bind(null); final HostAndPort address = HostAndPort.fromParts("127.0.0.1", server.getLocalPort()); RawMemcacheClient rawClient = DefaultRawMemcacheClient.connect( address, 1, false, null, 1000, Charsets.UTF_8, new NoopMetrics(), 1024 * 1024).get(); rawClient.shutdown(); ConnectFuture.disconnectFuture(rawClient).get(); assertFalse(rawClient.isConnected()); // Try to fill up the outstanding request limit for (int i = 0; i < 100; i++) { try { rawClient.send(new GetRequest("key", Charsets.UTF_8, false)).get(); } catch (ExecutionException e) { // Ignore any errors here } } try { rawClient.send(new GetRequest("key", Charsets.UTF_8, false)).get(); } catch (ExecutionException e) { throw e.getCause(); } } @Test(expected = MemcacheClosedException.class) public void testShutdownRequestException() throws Throwable { final ServerSocket server = new ServerSocket(); server.bind(null); final HostAndPort address = HostAndPort.fromParts("127.0.0.1", server.getLocalPort()); RawMemcacheClient rawClient = DefaultRawMemcacheClient.connect( address, 1, false, null, 1000, Charsets.UTF_8, new NoopMetrics(), 1024 * 1024).get(); rawClient.shutdown(); ConnectFuture.disconnectFuture(rawClient).get(); assertFalse(rawClient.isConnected()); try { rawClient.send(new GetRequest("key", Charsets.UTF_8, false)).get(); } catch (ExecutionException e) { throw e.getCause(); } } @Test public void testOutstandingRequestMetric() throws Exception { final OutstandingRequestListenerMetrics metrics = new OutstandingRequestListenerMetrics(); final Charset charset = Charsets.UTF_8; final byte[] response = "END\r\n".getBytes(charset); try (SlowStaticServer server = new SlowStaticServer(response, 1000)) { final int port = server.start(0); RawMemcacheClient rawClient = DefaultRawMemcacheClient.connect( HostAndPort.fromParts("localhost", port), 5000, false, null, 3000, charset, metrics, 1024 * 1024 ).get(); assertNotNull(metrics.getGauge()); assertEquals(0, metrics.getGauge().getOutstandingRequests()); List<ListenableFuture<GetResult<byte[]>>> futures = new ArrayList<>(); futures.add(rawClient.send(new GetRequest("key", charset, false))); assertEquals(1, metrics.getGauge().getOutstandingRequests()); futures.add(rawClient.send(new GetRequest("key", charset, false))); assertEquals(2, metrics.getGauge().getOutstandingRequests()); // ensure that counter goes back to zero after request is done Futures.allAsList(futures).get(5, TimeUnit.SECONDS); assertEquals(0, metrics.getGauge().getOutstandingRequests()); } } private static class OutstandingRequestListenerMetrics extends NoopMetrics { private OutstandingRequestsGauge gauge; @Override public void registerOutstandingRequestsGauge(OutstandingRequestsGauge gauge) { this.gauge = gauge; } public OutstandingRequestsGauge getGauge() { return this.gauge; } } }