🐛 don't close source after processing partial request without cache #43

prevent invalid calling source.close() in different threads to avoid crashes on Lollipop (#37, #29, #63, #66)
This commit is contained in:
Alexey Danilov
2016-07-29 14:09:11 +03:00
parent 582832f8b5
commit 6c996ea66c
5 changed files with 121 additions and 27 deletions

BIN
files/space.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -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<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";
@@ -95,4 +158,34 @@ public class HttpProxyCacheTest {
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);
}
});
}
}