package okhttp3;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import okhttp3.RealCall.AsyncCall;
import org.junit.Before;
import org.junit.Test;
import static java.util.concurrent.TimeUnit.SECONDS;
import static okhttp3.TestUtil.defaultClient;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class DispatcherTest {
RecordingExecutor executor = new RecordingExecutor();
RecordingCallback callback = new RecordingCallback();
Dispatcher dispatcher = new Dispatcher(executor);
OkHttpClient client = defaultClient().newBuilder()
.dispatcher(dispatcher)
.build();
@Before public void setUp() throws Exception {
dispatcher.setMaxRequests(20);
dispatcher.setMaxRequestsPerHost(10);
}
@Test public void maxRequestsZero() throws Exception {
try {
dispatcher.setMaxRequests(0);
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void maxPerHostZero() throws Exception {
try {
dispatcher.setMaxRequestsPerHost(0);
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void enqueuedJobsRunImmediately() throws Exception {
client.newCall(newRequest("http://a/1")).enqueue(callback);
executor.assertJobs("http://a/1");
}
@Test public void maxRequestsEnforced() throws Exception {
dispatcher.setMaxRequests(3);
client.newCall(newRequest("http://a/1")).enqueue(callback);
client.newCall(newRequest("http://a/2")).enqueue(callback);
client.newCall(newRequest("http://b/1")).enqueue(callback);
client.newCall(newRequest("http://b/2")).enqueue(callback);
executor.assertJobs("http://a/1", "http://a/2", "http://b/1");
}
@Test public void maxPerHostEnforced() throws Exception {
dispatcher.setMaxRequestsPerHost(2);
client.newCall(newRequest("http://a/1")).enqueue(callback);
client.newCall(newRequest("http://a/2")).enqueue(callback);
client.newCall(newRequest("http://a/3")).enqueue(callback);
executor.assertJobs("http://a/1", "http://a/2");
}
@Test public void increasingMaxRequestsPromotesJobsImmediately() throws Exception {
dispatcher.setMaxRequests(2);
client.newCall(newRequest("http://a/1")).enqueue(callback);
client.newCall(newRequest("http://b/1")).enqueue(callback);
client.newCall(newRequest("http://c/1")).enqueue(callback);
client.newCall(newRequest("http://a/2")).enqueue(callback);
client.newCall(newRequest("http://b/2")).enqueue(callback);
dispatcher.setMaxRequests(4);
executor.assertJobs("http://a/1", "http://b/1", "http://c/1", "http://a/2");
}
@Test public void increasingMaxPerHostPromotesJobsImmediately() throws Exception {
dispatcher.setMaxRequestsPerHost(2);
client.newCall(newRequest("http://a/1")).enqueue(callback);
client.newCall(newRequest("http://a/2")).enqueue(callback);
client.newCall(newRequest("http://a/3")).enqueue(callback);
client.newCall(newRequest("http://a/4")).enqueue(callback);
client.newCall(newRequest("http://a/5")).enqueue(callback);
dispatcher.setMaxRequestsPerHost(4);
executor.assertJobs("http://a/1", "http://a/2", "http://a/3", "http://a/4");
}
@Test public void oldJobFinishesNewJobCanRunDifferentHost() throws Exception {
dispatcher.setMaxRequests(1);
client.newCall(newRequest("http://a/1")).enqueue(callback);
client.newCall(newRequest("http://b/1")).enqueue(callback);
executor.finishJob("http://a/1");
executor.assertJobs("http://b/1");
}
@Test public void oldJobFinishesNewJobWithSameHostStarts() throws Exception {
dispatcher.setMaxRequests(2);
dispatcher.setMaxRequestsPerHost(1);
client.newCall(newRequest("http://a/1")).enqueue(callback);
client.newCall(newRequest("http://b/1")).enqueue(callback);
client.newCall(newRequest("http://b/2")).enqueue(callback);
client.newCall(newRequest("http://a/2")).enqueue(callback);
executor.finishJob("http://a/1");
executor.assertJobs("http://b/1", "http://a/2");
}
@Test public void oldJobFinishesNewJobCantRunDueToHostLimit() throws Exception {
dispatcher.setMaxRequestsPerHost(1);
client.newCall(newRequest("http://a/1")).enqueue(callback);
client.newCall(newRequest("http://b/1")).enqueue(callback);
client.newCall(newRequest("http://a/2")).enqueue(callback);
executor.finishJob("http://b/1");
executor.assertJobs("http://a/1");
}
@Test public void cancelingRunningJobTakesNoEffectUntilJobFinishes() throws Exception {
dispatcher.setMaxRequests(1);
Call c1 = client.newCall(newRequest("http://a/1", "tag1"));
Call c2 = client.newCall(newRequest("http://a/2"));
c1.enqueue(callback);
c2.enqueue(callback);
c1.cancel();
executor.assertJobs("http://a/1");
executor.finishJob("http://a/1");
executor.assertJobs("http://a/2");
}
@Test public void asyncCallAccessors() throws Exception {
dispatcher.setMaxRequests(3);
Call a1 = client.newCall(newRequest("http://a/1"));
Call a2 = client.newCall(newRequest("http://a/2"));
Call a3 = client.newCall(newRequest("http://a/3"));
Call a4 = client.newCall(newRequest("http://a/4"));
Call a5 = client.newCall(newRequest("http://a/5"));
a1.enqueue(callback);
a2.enqueue(callback);
a3.enqueue(callback);
a4.enqueue(callback);
a5.enqueue(callback);
assertEquals(3, dispatcher.runningCallsCount());
assertEquals(2, dispatcher.queuedCallsCount());
assertEquals(set(a1, a2, a3), set(dispatcher.runningCalls()));
assertEquals(set(a4, a5), set(dispatcher.queuedCalls()));
}
@Test public void synchronousCallAccessors() throws Exception {
final CountDownLatch ready = new CountDownLatch(2);
final CountDownLatch waiting = new CountDownLatch(1);
client = client.newBuilder()
.addInterceptor(
new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
try {
ready.countDown();
waiting.await();
} catch (InterruptedException e) {
throw new AssertionError();
}
throw new IOException();
}
})
.build();
Call a1 = client.newCall(newRequest("http://a/1"));
Call a2 = client.newCall(newRequest("http://a/2"));
Call a3 = client.newCall(newRequest("http://a/3"));
Call a4 = client.newCall(newRequest("http://a/4"));
Thread t1 = makeSynchronousCall(a1);
Thread t2 = makeSynchronousCall(a2);
// We created 4 calls and started 2 of them. That's 2 running calls and 0 queued.
ready.await();
assertEquals(2, dispatcher.runningCallsCount());
assertEquals(0, dispatcher.queuedCallsCount());
assertEquals(set(a1, a2), set(dispatcher.runningCalls()));
assertEquals(Collections.emptyList(), dispatcher.queuedCalls());
// Cancel some calls. That doesn't impact running or queued.
a2.cancel();
a3.cancel();
assertEquals(set(a1, a2), set(dispatcher.runningCalls()));
assertEquals(Collections.emptyList(), dispatcher.queuedCalls());
// Let the calls finish.
waiting.countDown();
t1.join();
t2.join();
// Now we should have 0 running calls and 0 queued calls.
assertEquals(0, dispatcher.runningCallsCount());
assertEquals(0, dispatcher.queuedCallsCount());
assertEquals(Collections.emptyList(), dispatcher.runningCalls());
assertEquals(Collections.emptyList(), dispatcher.queuedCalls());
assertTrue(a1.isExecuted());
assertFalse(a1.isCanceled());
assertTrue(a2.isExecuted());
assertTrue(a2.isCanceled());
assertFalse(a3.isExecuted());
assertTrue(a3.isCanceled());
assertFalse(a4.isExecuted());
assertFalse(a4.isCanceled());
}
@Test public void idleCallbackInvokedWhenIdle() throws InterruptedException {
final AtomicBoolean idle = new AtomicBoolean();
dispatcher.setIdleCallback(new Runnable() {
@Override public void run() {
idle.set(true);
}
});
client.newCall(newRequest("http://a/1")).enqueue(callback);
client.newCall(newRequest("http://a/2")).enqueue(callback);
executor.finishJob("http://a/1");
assertFalse(idle.get());
final CountDownLatch ready = new CountDownLatch(1);
final CountDownLatch proceed = new CountDownLatch(1);
client = client.newBuilder()
.addInterceptor(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
ready.countDown();
try {
proceed.await(5, SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return chain.proceed(chain.request());
}
})
.build();
Thread t1 = makeSynchronousCall(client.newCall(newRequest("http://a/3")));
ready.await(5, SECONDS);
executor.finishJob("http://a/2");
assertFalse(idle.get());
proceed.countDown();
t1.join();
assertTrue(idle.get());
}
private <T> Set<T> set(T... values) {
return set(Arrays.asList(values));
}
private <T> Set<T> set(List<T> list) {
return new LinkedHashSet<>(list);
}
private Thread makeSynchronousCall(final Call call) {
Thread thread = new Thread() {
@Override public void run() {
try {
call.execute();
throw new AssertionError();
} catch (IOException expected) {
}
}
};
thread.start();
return thread;
}
class RecordingExecutor extends AbstractExecutorService {
private List<AsyncCall> calls = new ArrayList<>();
@Override public void execute(Runnable command) {
calls.add((AsyncCall) command);
}
public void assertJobs(String... expectedUrls) {
List<String> actualUrls = new ArrayList<>();
for (AsyncCall call : calls) {
actualUrls.add(call.request().url().toString());
}
assertEquals(Arrays.asList(expectedUrls), actualUrls);
}
public void finishJob(String url) {
for (Iterator<AsyncCall> i = calls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (call.request().url().toString().equals(url)) {
i.remove();
dispatcher.finished(call);
return;
}
}
throw new AssertionError("No such job: " + url);
}
@Override public void shutdown() {
throw new UnsupportedOperationException();
}
@Override public List<Runnable> shutdownNow() {
throw new UnsupportedOperationException();
}
@Override public boolean isShutdown() {
throw new UnsupportedOperationException();
}
@Override public boolean isTerminated() {
throw new UnsupportedOperationException();
}
@Override public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
throw new UnsupportedOperationException();
}
}
private Request newRequest(String url) {
return new Request.Builder().url(url).build();
}
private Request newRequest(String url, String tag) {
return new Request.Builder().url(url).tag(tag).build();
}
}