Skip to content

Commit 5373160

Browse files
committed
Whole operation timeouts
Strictly-speaking this change is backwards-incompatible because it adds a new method to the Call interface. The method returns the call's timeout. The trickiest part of this is signaling the end of the call, which occurs after the last byte is consumed of the last follow up request, or when the call fails. Fortunately this is made easier by borrowing the sites used by EventListener, which already plots out where calls end. #2840
1 parent 2b0a9f4 commit 5373160

6 files changed

Lines changed: 321 additions & 1 deletion

File tree

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/*
2+
* Copyright (C) 2018 Square, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package okhttp3;
17+
18+
import java.io.IOException;
19+
import java.io.InterruptedIOException;
20+
import java.net.HttpURLConnection;
21+
import java.util.concurrent.CountDownLatch;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.atomic.AtomicReference;
24+
import okhttp3.mockwebserver.MockResponse;
25+
import okhttp3.mockwebserver.MockWebServer;
26+
import okio.BufferedSink;
27+
import org.junit.Rule;
28+
import org.junit.Test;
29+
30+
import static okhttp3.TestUtil.defaultClient;
31+
import static org.junit.Assert.assertFalse;
32+
import static org.junit.Assert.assertNotNull;
33+
import static org.junit.Assert.assertTrue;
34+
import static org.junit.Assert.fail;
35+
36+
public final class WholeOperationTimeoutTest {
37+
/** A large response body. Smaller bodies might successfully read after the socket is closed! */
38+
private static final String BIG_ENOUGH_BODY = TestUtil.repeat('a', 64 * 1024);
39+
40+
@Rule public final MockWebServer server = new MockWebServer();
41+
42+
private OkHttpClient client = defaultClient();
43+
44+
@Test public void timeoutWritingRequest() throws Exception {
45+
server.enqueue(new MockResponse());
46+
47+
Request request = new Request.Builder()
48+
.url(server.url("/"))
49+
.post(sleepingRequestBody(500))
50+
.build();
51+
52+
Call call = client.newCall(request);
53+
call.timeout().timeout(250, TimeUnit.MILLISECONDS);
54+
try {
55+
call.execute();
56+
fail();
57+
} catch (IOException e) {
58+
assertTrue(call.isCanceled());
59+
}
60+
}
61+
62+
@Test public void timeoutWritingRequestWithEnqueue() throws Exception {
63+
server.enqueue(new MockResponse());
64+
65+
Request request = new Request.Builder()
66+
.url(server.url("/"))
67+
.post(sleepingRequestBody(500))
68+
.build();
69+
70+
final CountDownLatch latch = new CountDownLatch(1);
71+
final AtomicReference<Throwable> exceptionRef = new AtomicReference<>();
72+
73+
Call call = client.newCall(request);
74+
call.timeout().timeout(250, TimeUnit.MILLISECONDS);
75+
call.enqueue(new Callback() {
76+
@Override public void onFailure(Call call, IOException e) {
77+
exceptionRef.set(e);
78+
latch.countDown();
79+
}
80+
81+
@Override public void onResponse(Call call, Response response) throws IOException {
82+
response.close();
83+
latch.countDown();
84+
}
85+
});
86+
87+
latch.await();
88+
assertTrue(call.isCanceled());
89+
assertNotNull(exceptionRef.get());
90+
}
91+
92+
@Test public void timeoutProcessing() throws Exception {
93+
server.enqueue(new MockResponse()
94+
.setHeadersDelay(500, TimeUnit.MILLISECONDS));
95+
96+
Request request = new Request.Builder()
97+
.url(server.url("/"))
98+
.build();
99+
100+
Call call = client.newCall(request);
101+
call.timeout().timeout(250, TimeUnit.MILLISECONDS);
102+
try {
103+
call.execute();
104+
fail();
105+
} catch (IOException e) {
106+
assertTrue(call.isCanceled());
107+
}
108+
}
109+
110+
@Test public void timeoutProcessingWithEnqueue() throws Exception {
111+
server.enqueue(new MockResponse()
112+
.setHeadersDelay(500, TimeUnit.MILLISECONDS));
113+
114+
Request request = new Request.Builder()
115+
.url(server.url("/"))
116+
.build();
117+
118+
final CountDownLatch latch = new CountDownLatch(1);
119+
final AtomicReference<Throwable> exceptionRef = new AtomicReference<>();
120+
121+
Call call = client.newCall(request);
122+
call.timeout().timeout(250, TimeUnit.MILLISECONDS);
123+
call.enqueue(new Callback() {
124+
@Override public void onFailure(Call call, IOException e) {
125+
exceptionRef.set(e);
126+
latch.countDown();
127+
}
128+
129+
@Override public void onResponse(Call call, Response response) throws IOException {
130+
response.close();
131+
latch.countDown();
132+
}
133+
});
134+
135+
latch.await();
136+
assertTrue(call.isCanceled());
137+
assertNotNull(exceptionRef.get());
138+
}
139+
140+
@Test public void timeoutReadingResponse() throws Exception {
141+
server.enqueue(new MockResponse()
142+
.setBody(BIG_ENOUGH_BODY));
143+
144+
Request request = new Request.Builder()
145+
.url(server.url("/"))
146+
.build();
147+
148+
Call call = client.newCall(request);
149+
call.timeout().timeout(250, TimeUnit.MILLISECONDS);
150+
Response response = call.execute();
151+
Thread.sleep(500);
152+
try {
153+
response.body().source().readUtf8();
154+
fail();
155+
} catch (IOException e) {
156+
assertTrue(call.isCanceled());
157+
}
158+
}
159+
160+
@Test public void timeoutReadingResponseWithEnqueue() throws Exception {
161+
server.enqueue(new MockResponse()
162+
.setBody(BIG_ENOUGH_BODY));
163+
164+
Request request = new Request.Builder()
165+
.url(server.url("/"))
166+
.build();
167+
168+
final CountDownLatch latch = new CountDownLatch(1);
169+
final AtomicReference<Throwable> exceptionRef = new AtomicReference<>();
170+
171+
Call call = client.newCall(request);
172+
call.timeout().timeout(250, TimeUnit.MILLISECONDS);
173+
call.enqueue(new Callback() {
174+
@Override public void onFailure(Call call, IOException e) {
175+
latch.countDown();
176+
}
177+
178+
@Override public void onResponse(Call call, Response response) throws IOException {
179+
try {
180+
Thread.sleep(500);
181+
} catch (InterruptedException e) {
182+
throw new AssertionError();
183+
}
184+
try {
185+
response.body().source().readUtf8();
186+
fail();
187+
} catch (IOException e) {
188+
exceptionRef.set(e);
189+
} finally {
190+
latch.countDown();
191+
}
192+
}
193+
});
194+
195+
latch.await();
196+
assertTrue(call.isCanceled());
197+
assertNotNull(exceptionRef.get());
198+
}
199+
200+
@Test public void singleTimeoutForAllFollowUpRequests() throws Exception {
201+
server.enqueue(new MockResponse()
202+
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
203+
.setHeader("Location", "/b")
204+
.setHeadersDelay(100, TimeUnit.MILLISECONDS));
205+
server.enqueue(new MockResponse()
206+
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
207+
.setHeader("Location", "/c")
208+
.setHeadersDelay(100, TimeUnit.MILLISECONDS));
209+
server.enqueue(new MockResponse()
210+
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
211+
.setHeader("Location", "/d")
212+
.setHeadersDelay(100, TimeUnit.MILLISECONDS));
213+
server.enqueue(new MockResponse()
214+
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
215+
.setHeader("Location", "/e")
216+
.setHeadersDelay(100, TimeUnit.MILLISECONDS));
217+
server.enqueue(new MockResponse()
218+
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
219+
.setHeader("Location", "/f")
220+
.setHeadersDelay(100, TimeUnit.MILLISECONDS));
221+
server.enqueue(new MockResponse());
222+
223+
Request request = new Request.Builder()
224+
.url(server.url("/a"))
225+
.build();
226+
227+
Call call = client.newCall(request);
228+
call.timeout().timeout(250, TimeUnit.MILLISECONDS);
229+
try {
230+
call.execute();
231+
fail();
232+
} catch (IOException e) {
233+
assertTrue(call.isCanceled());
234+
}
235+
}
236+
237+
@Test public void noTimeout() throws Exception {
238+
server.enqueue(new MockResponse()
239+
.setHeadersDelay(250, TimeUnit.MILLISECONDS)
240+
.setBody(BIG_ENOUGH_BODY));
241+
242+
Request request = new Request.Builder()
243+
.url(server.url("/"))
244+
.post(sleepingRequestBody(250))
245+
.build();
246+
247+
Call call = client.newCall(request);
248+
call.timeout().timeout(1000, TimeUnit.MILLISECONDS);
249+
Response response = call.execute();
250+
Thread.sleep(250);
251+
response.body().source().readUtf8();
252+
response.close();
253+
assertFalse(call.isCanceled());
254+
}
255+
256+
private RequestBody sleepingRequestBody(final int sleepMillis) {
257+
return new RequestBody() {
258+
@Override public MediaType contentType() {
259+
return MediaType.parse("text/plain");
260+
}
261+
262+
@Override public void writeTo(BufferedSink sink) throws IOException {
263+
try {
264+
sink.writeUtf8("abc");
265+
sink.flush();
266+
Thread.sleep(sleepMillis);
267+
sink.writeUtf8("def");
268+
} catch (InterruptedException e) {
269+
throw new InterruptedIOException();
270+
}
271+
}
272+
};
273+
}
274+
}

okhttp/src/main/java/okhttp3/Call.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package okhttp3;
1717

1818
import java.io.IOException;
19+
import okio.Timeout;
1920

2021
/**
2122
* A call is a request that has been prepared for execution. A call can be canceled. As this object
@@ -80,6 +81,12 @@ public interface Call extends Cloneable {
8081

8182
boolean isCanceled();
8283

84+
/**
85+
* Returns a timeout that applies to the entire call: writing the request, server processing,
86+
* and reading the response.
87+
*/
88+
Timeout timeout();
89+
8390
/**
8491
* Create a new, identical call to this one which can be enqueued or executed even if this call
8592
* has already been.

okhttp/src/main/java/okhttp3/OkHttpClient.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package okhttp3;
1717

18+
import java.io.IOException;
1819
import java.net.Proxy;
1920
import java.net.ProxySelector;
2021
import java.net.Socket;
@@ -37,11 +38,11 @@
3738
import okhttp3.internal.Internal;
3839
import okhttp3.internal.Util;
3940
import okhttp3.internal.cache.InternalCache;
40-
import okhttp3.internal.proxy.NullProxySelector;
4141
import okhttp3.internal.connection.RealConnection;
4242
import okhttp3.internal.connection.RouteDatabase;
4343
import okhttp3.internal.connection.StreamAllocation;
4444
import okhttp3.internal.platform.Platform;
45+
import okhttp3.internal.proxy.NullProxySelector;
4546
import okhttp3.internal.tls.CertificateChainCleaner;
4647
import okhttp3.internal.tls.OkHostnameVerifier;
4748
import okhttp3.internal.ws.RealWebSocket;
@@ -187,6 +188,10 @@ public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean
187188
return ((RealCall) call).streamAllocation();
188189
}
189190

191+
@Override public @Nullable IOException timeoutExit(Call call, @Nullable IOException e) {
192+
return ((RealCall) call).timeoutExit(e);
193+
}
194+
190195
@Override public Call newWebSocketCall(OkHttpClient client, Request originalRequest) {
191196
return RealCall.newRealCall(client, originalRequest, true);
192197
}

0 commit comments

Comments
 (0)