diff --git a/files/space.jpg b/files/space.jpg new file mode 100644 index 0000000..4cba9cd Binary files /dev/null and b/files/space.jpg differ diff --git a/library/src/main/java/com/danikula/videocache/HttpProxyCache.java b/library/src/main/java/com/danikula/videocache/HttpProxyCache.java index 395fb06..0620fde 100644 --- a/library/src/main/java/com/danikula/videocache/HttpProxyCache.java +++ b/library/src/main/java/com/danikula/videocache/HttpProxyCache.java @@ -83,18 +83,18 @@ class HttpProxyCache extends ProxyCache { } private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException { + HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source); try { - HttpUrlSource source = new HttpUrlSource(this.source); - source.open((int) offset); + newSourceNoCache.open((int) offset); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int readBytes; - while ((readBytes = source.read(buffer)) != -1) { + while ((readBytes = newSourceNoCache.read(buffer)) != -1) { out.write(buffer, 0, readBytes); offset += readBytes; } out.flush(); } finally { - source.close(); + newSourceNoCache.close(); } } diff --git a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java index 60cf31f..52da075 100644 --- a/library/src/main/java/com/danikula/videocache/HttpUrlSource.java +++ b/library/src/main/java/com/danikula/videocache/HttpUrlSource.java @@ -78,10 +78,11 @@ public class HttpUrlSource implements Source { if (connection != null) { try { connection.disconnect(); - } catch (NullPointerException e) { - // https://github.com/danikula/AndroidVideoCache/issues/32 - // https://github.com/danikula/AndroidVideoCache/issues/29 - throw new ProxyCacheException("Error disconnecting HttpUrlConnection", e); + } catch (NullPointerException | IllegalArgumentException e) { + String message = "Wait... but why? WTF!? " + + "Really shouldn't happen any more after fixing https://github.com/danikula/AndroidVideoCache/issues/43. " + + "If you read it on your device log, please, notify me danikula@gmail.com or create issue here https://github.com/danikula/AndroidVideoCache/issues."; + throw new RuntimeException(message, e); } } } diff --git a/test/src/main/assets/space.jpg b/test/src/main/assets/space.jpg new file mode 100644 index 0000000..4cba9cd Binary files /dev/null and b/test/src/main/assets/space.jpg differ diff --git a/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java b/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java index 5fbc95d..1b69fb1 100644 --- a/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java +++ b/test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java @@ -13,9 +13,20 @@ import org.robolectric.annotation.Config; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; import java.net.Socket; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME; +import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL; +import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_SIZE; import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL; +import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile; import static com.danikula.videocache.support.ProxyCacheTestUtils.loadTestData; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Matchers.any; @@ -37,37 +48,22 @@ public class HttpProxyCacheTest { @Test public void testProcessRequestNoCache() throws Exception { - HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL); - FileCache cache = new FileCache(ProxyCacheTestUtils.newCacheFile()); - HttpProxyCache proxyCache = new HttpProxyCache(source, cache); - GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1"); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Socket socket = mock(Socket.class); - when(socket.getOutputStream()).thenReturn(out); - - proxyCache.processRequest(request, socket); - Response response = new Response(out.toByteArray()); + Response response = processRequest(HTTP_DATA_URL, "GET /" + HTTP_DATA_URL + " HTTP/1.1"); assertThat(response.data).isEqualTo(loadTestData()); assertThat(response.code).isEqualTo(200); - assertThat(response.contentLength).isEqualTo(ProxyCacheTestUtils.HTTP_DATA_SIZE); + assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE); assertThat(response.contentType).isEqualTo("image/jpeg"); } @Test public void testProcessPartialRequestWithoutCache() throws Exception { - HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL); FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile()); FileCache spyFileCache = Mockito.spy(fileCache); doThrow(new RuntimeException()).when(spyFileCache).read(any(byte[].class), anyLong(), anyInt()); - HttpProxyCache proxyCache = new HttpProxyCache(source, spyFileCache); - GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=2000-"); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Socket socket = mock(Socket.class); - when(socket.getOutputStream()).thenReturn(out); - proxyCache.processRequest(request, socket); - Response response = new Response(out.toByteArray()); + String httpRequest = "GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=2000-"; + Response response = processRequest(HTTP_DATA_URL, httpRequest, spyFileCache); byte[] fullData = loadTestData(); byte[] partialData = new byte[fullData.length - 2000]; @@ -76,6 +72,73 @@ public class HttpProxyCacheTest { assertThat(response.code).isEqualTo(206); } + @Test // https://github.com/danikula/AndroidVideoCache/issues/43 + public void testPreventClosingOriginalSourceForNewPartialRequestWithoutCache() throws Exception { + HttpUrlSource source = new HttpUrlSource(HTTP_DATA_BIG_URL); + FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile()); + HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache); + ExecutorService executor = Executors.newFixedThreadPool(5); + Future firstRequestFeature = processAsync(executor, proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1"); + Thread.sleep(100); // wait for first request started to process + + int offset = 30000; + String partialRequest = "GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=" + offset + "-"; + Future secondRequestFeature = processAsync(executor, proxyCache, partialRequest); + + Response secondResponse = secondRequestFeature.get(); + Response firstResponse = firstRequestFeature.get(); + + byte[] responseData = loadAssetFile(ASSETS_DATA_BIG_NAME); + assertThat(firstResponse.data).isEqualTo(responseData); + + byte[] partialData = new byte[responseData.length - offset]; + System.arraycopy(responseData, offset, partialData, 0, partialData.length); + assertThat(secondResponse.data).isEqualTo(partialData); + } + + @Test + public void testProcessManyThreads() throws Exception { + final String url = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/space.jpg"; + HttpUrlSource source = new HttpUrlSource(url); + FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile()); + final HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache); + final byte[] loadedData = loadAssetFile("space.jpg"); + final Random random = new Random(System.currentTimeMillis()); + int concurrentRequests = 10; + ExecutorService executor = Executors.newFixedThreadPool(concurrentRequests); + Future[] results = new Future[concurrentRequests]; + int[] offsets = new int[concurrentRequests]; + final CountDownLatch finishLatch = new CountDownLatch(concurrentRequests); + final CountDownLatch startLatch = new CountDownLatch(1); + for (int i = 0; i < concurrentRequests; i++) { + final int offset = random.nextInt(loadedData.length); + offsets[i] = offset; + results[i] = executor.submit(new Callable() { + + @Override + public Response call() throws Exception { + try { + startLatch.await(); + String partialRequest = "GET /" + url + " HTTP/1.1\nRange: bytes=" + offset + "-"; + return processRequest(proxyCache, partialRequest); + } finally { + finishLatch.countDown(); + } + } + }); + } + startLatch.countDown(); + finishLatch.await(); + + for (int i = 0; i < results.length; i++) { + Response response = (Response) results[i].get(); + int offset = offsets[i]; + byte[] partialData = new byte[loadedData.length - offset]; + System.arraycopy(loadedData, offset, partialData, 0, partialData.length); + assertThat(response.data).isEqualTo(partialData); + } + } + @Test public void testLoadEmptyFile() throws Exception { String zeroSizeUrl = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/empty.txt"; @@ -95,4 +158,34 @@ public class HttpProxyCacheTest { Mockito.verify(listener).onCacheAvailable(Mockito.any(), eq(zeroSizeUrl), eq(100)); assertThat(response.data).isEmpty(); } + + private Response processRequest(String sourceUrl, String httpRequest) throws ProxyCacheException, IOException { + FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile()); + return processRequest(sourceUrl, httpRequest, fileCache); + } + + private Response processRequest(String sourceUrl, String httpRequest, FileCache fileCache) throws ProxyCacheException, IOException { + HttpUrlSource source = new HttpUrlSource(sourceUrl); + HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache); + return processRequest(proxyCache, httpRequest); + } + + private Response processRequest(HttpProxyCache proxyCache, String httpRequest) throws ProxyCacheException, IOException { + GetRequest request = new GetRequest(httpRequest); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Socket socket = mock(Socket.class); + when(socket.getOutputStream()).thenReturn(out); + proxyCache.processRequest(request, socket); + return new Response(out.toByteArray()); + } + + private Future processAsync(ExecutorService executor, final HttpProxyCache proxyCache, final String httpRequest) { + return executor.submit(new Callable() { + + @Override + public Response call() throws Exception { + return processRequest(proxyCache, httpRequest); + } + }); + } }