Skip to content

Commit

Permalink
ping server each time before wrapping to proxy url
Browse files Browse the repository at this point in the history
  • Loading branch information
danikula committed Sep 27, 2016
1 parent 44318d8 commit 0d1131b
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.danikula.videocache;

import android.content.Context;
import android.os.SystemClock;

import com.danikula.videocache.file.DiskUsage;
import com.danikula.videocache.file.FileNameGenerator;
Expand All @@ -16,26 +15,19 @@

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;

import static com.danikula.videocache.Preconditions.checkAllNotNull;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
* Simple lightweight proxy server with file caching support that handles HTTP requests.
Expand All @@ -59,10 +51,7 @@
public class HttpProxyCacheServer {

private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer");

private static final String PROXY_HOST = "127.0.0.1";
private static final String PING_REQUEST = "ping";
private static final String PING_RESPONSE = "ping ok";

private final Object clientsLock = new Object();
private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
Expand All @@ -71,7 +60,7 @@ public class HttpProxyCacheServer {
private final int port;
private final Thread waitConnectionThread;
private final Config config;
private boolean pinged;
private final Pinger pinger;

public HttpProxyCacheServer(Context context) {
this(new Builder(context).buildConfig());
Expand All @@ -87,65 +76,16 @@ private HttpProxyCacheServer(Config config) {
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
LOG.info("Proxy cache server started. Ping it...");
makeSureServerWorks();
this.pinger = new Pinger(PROXY_HOST, port);
LOG.info("Proxy cache server started. Is it alive? " + isAlive());
} catch (IOException | InterruptedException e) {
socketProcessor.shutdown();
throw new IllegalStateException("Error starting local proxy server", e);
}
}

private void makeSureServerWorks() {
int maxPingAttempts = 3;
int delay = 300;
int pingAttempts = 0;
while (pingAttempts < maxPingAttempts) {
try {
Future<Boolean> pingFuture = socketProcessor.submit(new PingCallable());
this.pinged = pingFuture.get(delay, MILLISECONDS);
if (this.pinged) {
return;
}
SystemClock.sleep(delay);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
LOG.error("Error pinging server [attempt: " + pingAttempts + ", timeout: " + delay + "]. ", e);
}
pingAttempts++;
delay *= 2;
}
LOG.error("Shutdown server… Error pinging server [attempts: " + pingAttempts + ", max timeout: " + delay / 2 + "]. " +
"If you see this message, please, email me [email protected]");
shutdown();
}

private boolean pingServer() throws ProxyCacheException {
String pingUrl = appendToProxyUrl(PING_REQUEST);
HttpUrlSource source = new HttpUrlSource(pingUrl);
try {
byte[] expectedResponse = PING_RESPONSE.getBytes();
source.open(0);
byte[] response = new byte[expectedResponse.length];
source.read(response);
boolean pingOk = Arrays.equals(expectedResponse, response);
LOG.info("Ping response: `" + new String(response) + "`, pinged? " + pingOk);
return pingOk;
} catch (ProxyCacheException e) {
LOG.error("Error reading ping response", e);
return false;
} finally {
source.close();
}
}

public String getProxyUrl(String url) {
if (!pinged) {
LOG.error("Proxy server isn't pinged. Caching doesn't work. If you see this message, please, email me [email protected]");
}
return pinged ? appendToProxyUrl(url) : url;
}

private String appendToProxyUrl(String url) {
return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
return isAlive() ? appendToProxyUrl(url) : url;
}

public void registerCacheListener(CacheListener cacheListener, String url) {
Expand Down Expand Up @@ -210,6 +150,14 @@ public void shutdown() {
}
}

private boolean isAlive() {
return pinger.ping(3, 70); // 70+140+280=max~500ms
}

private String appendToProxyUrl(String url) {
return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
}

private void shutdownClients() {
synchronized (clientsLock) {
for (HttpProxyCacheServerClients clients : clientsMap.values()) {
Expand All @@ -236,8 +184,8 @@ private void processSocket(Socket socket) {
GetRequest request = GetRequest.read(socket.getInputStream());
LOG.debug("Request to cache proxy:" + request);
String url = ProxyCacheUtils.decode(request.uri);
if (PING_REQUEST.equals(url)) {
responseToPing(socket);
if (pinger.isPingRequest(url)) {
pinger.responseToPing(socket);
} else {
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request, socket);
Expand All @@ -254,12 +202,6 @@ private void processSocket(Socket socket) {
}
}

private void responseToPing(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\n\n".getBytes());
out.write(PING_RESPONSE.getBytes());
}

private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
synchronized (clientsLock) {
HttpProxyCacheServerClients clients = clientsMap.get(url);
Expand Down Expand Up @@ -354,14 +296,6 @@ public void run() {
}
}

private class PingCallable implements Callable<Boolean> {

@Override
public Boolean call() throws Exception {
return pingServer();
}
}

/**
* Builder for {@link HttpProxyCacheServer}.
*/
Expand Down
112 changes: 112 additions & 0 deletions library/src/main/java/com/danikula/videocache/Pinger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.danikula.videocache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;

import static com.danikula.videocache.Preconditions.checkArgument;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
* Pings {@link HttpProxyCacheServer} to make sure it works.
*
* @author Alexey Danilov ([email protected]).
*/

class Pinger {

private static final Logger LOG = LoggerFactory.getLogger("Pinger");
private static final String PING_REQUEST = "ping";
private static final String PING_RESPONSE = "ping ok";

private final ExecutorService pingExecutor = Executors.newSingleThreadExecutor();
private final String host;
private final int port;

Pinger(String host, int port) {
this.host = checkNotNull(host);
this.port = port;
}

boolean ping(int maxAttempts, int startTimeout) {
checkArgument(maxAttempts >= 1);
checkArgument(startTimeout > 0);

int timeout = startTimeout;
int attempts = 0;
while (attempts < maxAttempts) {
try {
Future<Boolean> pingFuture = pingExecutor.submit(new PingCallable());
boolean pinged = pingFuture.get(timeout, MILLISECONDS);
if (pinged) {
return true;
}
} catch (TimeoutException e) {
LOG.warn("Error pinging server (attempt: " + attempts + ", timeout: " + timeout + "). ");
} catch (InterruptedException | ExecutionException e) {
LOG.error("Error pinging server due to unexpected error", e);
}
attempts++;
timeout *= 2;
}
String error = String.format("Error pinging server (attempts: %d, max timeout: %d). " +
"If you see this message, please, email me [email protected] " +
"or create issue here https://github.com/danikula/AndroidVideoCache/issues", attempts, timeout / 2);
LOG.error(error, new ProxyCacheException(error));
return false;
}

boolean isPingRequest(String request) {
return PING_REQUEST.equals(request);
}

void responseToPing(Socket socket) throws IOException {
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK\n\n".getBytes());
out.write(PING_RESPONSE.getBytes());
}

private boolean pingServer() throws ProxyCacheException {
String pingUrl = getPingUrl();
HttpUrlSource source = new HttpUrlSource(pingUrl);
try {
byte[] expectedResponse = PING_RESPONSE.getBytes();
source.open(0);
byte[] response = new byte[expectedResponse.length];
source.read(response);
boolean pingOk = Arrays.equals(expectedResponse, response);
LOG.info("Ping response: `" + new String(response) + "`, pinged? " + pingOk);
return pingOk;
} catch (ProxyCacheException e) {
LOG.error("Error reading ping response", e);
return false;
} finally {
source.close();
}
}

private String getPingUrl() {
return String.format(Locale.US, "http://%s:%d/%s", host, port, PING_REQUEST);
}

private class PingCallable implements Callable<Boolean> {

@Override
public Boolean call() throws Exception {
return pingServer();
}
}

}
64 changes: 64 additions & 0 deletions test/src/test/java/com/danikula/videocache/PingerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.danikula.videocache;

import org.junit.Test;
import org.robolectric.RuntimeEnvironment;

import java.io.ByteArrayOutputStream;
import java.net.Socket;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
* Tests {@link Pinger}.
*
* @author Alexey Danilov ([email protected]).
*/
public class PingerTest extends BaseTest {

@Test
public void testPingSuccess() throws Exception {
HttpProxyCacheServer server = new HttpProxyCacheServer(RuntimeEnvironment.application);
Pinger pinger = new Pinger("127.0.0.1", getPort(server));
boolean pinged = pinger.ping(1, 100);
assertThat(pinged).isTrue();

server.shutdown();
}

@Test
public void testPingFail() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 33);
boolean pinged = pinger.ping(3, 70);
assertThat(pinged).isFalse();
}

@Test
public void testIsPingRequest() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 1);
assertThat(pinger.isPingRequest("ping")).isTrue();
assertThat(pinger.isPingRequest("notPing")).isFalse();
}

@Test
public void testResponseToPing() throws Exception {
Pinger pinger = new Pinger("127.0.0.1", 1);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Socket socket = mock(Socket.class);
when(socket.getOutputStream()).thenReturn(out);
pinger.responseToPing(socket);
assertThat(out.toString()).isEqualTo("HTTP/1.1 200 OK\n\nping ok");
}

private int getPort(HttpProxyCacheServer server) {
String proxyUrl = server.getProxyUrl("test");
Pattern pattern = Pattern.compile("http://127.0.0.1:(\\d*)/test");
Matcher matcher = pattern.matcher(proxyUrl);
assertThat(matcher.find()).isTrue();
String portAsString = matcher.group(1);
return Integer.parseInt(portAsString);
}
}

0 comments on commit 0d1131b

Please sign in to comment.