Skip to content

Commit

Permalink
🐛 don't close source after processing partial request without cache #43
Browse files Browse the repository at this point in the history
prevent invalid calling source.close() in different threads to avoid crashes on Lollipop (#37, #29, #63, #66)
  • Loading branch information
danikula committed Jul 29, 2016
1 parent 582832f commit 6c996ea
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 27 deletions.
Binary file added files/space.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,18 @@ private void responseWithCache(OutputStream out, long offset) throws ProxyCacheE
}

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();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ public void close() throws ProxyCacheException {
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);
}
}
}
Expand Down
Binary file added test/src/main/assets/space.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
131 changes: 112 additions & 19 deletions test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,20 @@

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;
Expand All @@ -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];
Expand All @@ -76,6 +72,73 @@ public void testProcessPartialRequestWithoutCache() throws Exception {
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<Response> 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<Response> 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<Response>() {

@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";
Expand All @@ -95,4 +158,34 @@ public void testLoadEmptyFile() throws Exception {
Mockito.verify(listener).onCacheAvailable(Mockito.<File>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<Response> processAsync(ExecutorService executor, final HttpProxyCache proxyCache, final String httpRequest) {
return executor.submit(new Callable<Response>() {

@Override
public Response call() throws Exception {
return processRequest(proxyCache, httpRequest);
}
});
}
}

0 comments on commit 6c996ea

Please sign in to comment.