-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ping server each time before wrapping to proxy url
- Loading branch information
Showing
3 changed files
with
190 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
@@ -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. | ||
|
@@ -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); | ||
|
@@ -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()); | ||
|
@@ -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) { | ||
|
@@ -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()) { | ||
|
@@ -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); | ||
|
@@ -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); | ||
|
@@ -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}. | ||
*/ | ||
|
112 changes: 112 additions & 0 deletions
112
library/src/main/java/com/danikula/videocache/Pinger.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
64
test/src/test/java/com/danikula/videocache/PingerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |