Skip to content

Commit 77074f1

Browse files
authored
Change JettyConnector 'readTimeout' behavior to match socket read tim… (#5114)
* Change JettyConnector 'readTimeout' behavior to match socket read timeout definition - e.g., ApacheConnector behavior matches it. * Read timeout: Time on waiting to receive the first data byte. The `timeout` method timeouts the request even if data were already received, capping the query to a maximum execution time. This behavior is problematic when streaming data over a prolonged duration; the client has already received data bytes, but data continues to flow. I provided a jetty specific property (jersey.config.jetty.client.totalTimeout) that configures the 'totalTimeout' when required.
1 parent 16eaaff commit 77074f1

File tree

3 files changed

+147
-7
lines changed

3 files changed

+147
-7
lines changed

connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyClientProperties.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ private JettyClientProperties() {
8585
public static final String SYNC_LISTENER_RESPONSE_MAX_SIZE =
8686
"jersey.config.jetty.client.syncListenerResponseMaxSize";
8787

88+
/**
89+
* Total timeout interval, in milliseconds.
90+
* <p>
91+
* The value MUST be an instance convertible to {@link java.lang.Integer}. A
92+
* value of zero (0) is equivalent to an interval of infinity.
93+
* </p>
94+
* <p>
95+
* The default value is infinity (0).
96+
* </p>
97+
* <p>
98+
* The name of the configuration property is <tt>{@value}</tt>.
99+
* </p>
100+
*/
101+
public static final String TOTAL_TIMEOUT = "jersey.config.jetty.client.totalTimeout";
102+
88103
/**
89104
* Get the value of the specified property.
90105
*

connectors/jetty-connector/src/main/java/org/glassfish/jersey/jetty/connector/JettyConnector.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,14 @@ private Request translateRequest(final ClientRequest clientRequest) {
314314
request.followRedirects(clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true));
315315
final Object readTimeout = clientRequest.resolveProperty(ClientProperties.READ_TIMEOUT, -1);
316316
if (readTimeout != null && readTimeout instanceof Integer && (Integer) readTimeout > 0) {
317-
request.timeout((Integer) readTimeout, TimeUnit.MILLISECONDS);
317+
request.idleTimeout((Integer) readTimeout, TimeUnit.MILLISECONDS);
318318
}
319+
320+
final Object totalTimeout = clientRequest.resolveProperty(JettyClientProperties.TOTAL_TIMEOUT, -1);
321+
if (totalTimeout != null && totalTimeout instanceof Integer && (Integer) totalTimeout > 0) {
322+
request.timeout((Integer) totalTimeout, TimeUnit.MILLISECONDS);
323+
}
324+
319325
return request;
320326
}
321327

connectors/jetty-connector/src/test/java/org/glassfish/jersey/jetty/connector/TimeoutTest.java

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2013, 2022 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,30 +16,37 @@
1616

1717
package org.glassfish.jersey.jetty.connector;
1818

19+
import static org.hamcrest.CoreMatchers.instanceOf;
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertThat;
22+
import static org.junit.Assert.assertTrue;
23+
import static org.junit.Assert.fail;
24+
1925
import java.net.URI;
26+
import java.nio.charset.StandardCharsets;
27+
import java.util.concurrent.TimeUnit;
2028
import java.util.concurrent.TimeoutException;
2129
import java.util.logging.Logger;
2230

31+
import javax.ws.rs.DefaultValue;
2332
import javax.ws.rs.GET;
2433
import javax.ws.rs.Path;
2534
import javax.ws.rs.ProcessingException;
35+
import javax.ws.rs.QueryParam;
2636
import javax.ws.rs.client.Client;
2737
import javax.ws.rs.client.ClientBuilder;
2838
import javax.ws.rs.client.WebTarget;
2939
import javax.ws.rs.core.Application;
3040
import javax.ws.rs.core.Response;
41+
import javax.ws.rs.core.StreamingOutput;
3142

43+
import org.glassfish.jersey.CommonProperties;
3244
import org.glassfish.jersey.client.ClientConfig;
3345
import org.glassfish.jersey.client.ClientProperties;
3446
import org.glassfish.jersey.logging.LoggingFeature;
3547
import org.glassfish.jersey.server.ResourceConfig;
3648
import org.glassfish.jersey.test.JerseyTest;
37-
3849
import org.junit.Test;
39-
import static org.hamcrest.CoreMatchers.instanceOf;
40-
import static org.junit.Assert.assertEquals;
41-
import static org.junit.Assert.assertThat;
42-
import static org.junit.Assert.fail;
4350

4451
/**
4552
* @author Martin Matula
@@ -65,6 +72,48 @@ public String getTimeout() {
6572
}
6673
return "GET";
6774
}
75+
76+
/**
77+
* Long-running streaming request
78+
*
79+
* @param count number of packets send
80+
* @param pauseMillis pause between each packets
81+
*/
82+
@GET
83+
@Path("stream")
84+
public Response streamsWithDelay(@QueryParam("start") @DefaultValue("0") int startMillis, @QueryParam("count") int count,
85+
@QueryParam("pauseMillis") int pauseMillis) {
86+
StreamingOutput streamingOutput = streamSlowly(startMillis, count, pauseMillis);
87+
88+
return Response.ok(streamingOutput)
89+
.build();
90+
}
91+
}
92+
93+
private static StreamingOutput streamSlowly(int startMillis, int count, int pauseMillis) {
94+
95+
return output -> {
96+
try {
97+
TimeUnit.MILLISECONDS.sleep(startMillis);
98+
}
99+
catch (InterruptedException e) {
100+
Thread.currentThread().interrupt();
101+
}
102+
output.write("begin\n".getBytes(StandardCharsets.UTF_8));
103+
output.flush();
104+
for (int i = 0; i < count; i++) {
105+
try {
106+
TimeUnit.MILLISECONDS.sleep(pauseMillis);
107+
}
108+
catch (InterruptedException e) {
109+
Thread.currentThread().interrupt();
110+
}
111+
112+
output.write(("message " + i + "\n").getBytes(StandardCharsets.UTF_8));
113+
output.flush();
114+
}
115+
output.write("end".getBytes(StandardCharsets.UTF_8));
116+
};
68117
}
69118

70119
@Override
@@ -121,4 +170,74 @@ public void testTimeoutInRequest() {
121170
c.close();
122171
}
123172
}
173+
174+
/**
175+
* Test accessing an operation that is streaming slowly
176+
*
177+
* @throws ProcessingException in case of a test error.
178+
*/
179+
@Test
180+
public void testSlowlyStreamedContentDoesNotReadTimeout() throws Exception {
181+
182+
int count = 5;
183+
int pauseMillis = 50;
184+
185+
final Response response = target("test")
186+
.property(ClientProperties.READ_TIMEOUT, 100L)
187+
.property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
188+
.path("stream")
189+
.queryParam("count", count)
190+
.queryParam("pauseMillis", pauseMillis)
191+
.request().get();
192+
193+
assertTrue(response.readEntity(String.class).contains("end"));
194+
}
195+
196+
@Test
197+
public void testSlowlyStreamedContentDoesTotalTimeout() throws Exception {
198+
199+
int count = 5;
200+
int pauseMillis = 50;
201+
202+
try {
203+
target("test")
204+
.property(JettyClientProperties.TOTAL_TIMEOUT, 100L)
205+
.property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
206+
.path("stream")
207+
.queryParam("count", count)
208+
.queryParam("pauseMillis", pauseMillis)
209+
.request().get();
210+
211+
fail("This operation should trigger total timeout");
212+
} catch (ProcessingException e) {
213+
assertEquals(TimeoutException.class, e.getCause().getClass());
214+
}
215+
}
216+
217+
/**
218+
* Test accessing an operation that is streaming slowly
219+
*
220+
* @throws ProcessingException in case of a test error.
221+
*/
222+
@Test
223+
public void testSlowToStartStreamedContentDoesReadTimeout() throws Exception {
224+
225+
int start = 150;
226+
int count = 5;
227+
int pauseMillis = 50;
228+
229+
try {
230+
target("test")
231+
.property(ClientProperties.READ_TIMEOUT, 100L)
232+
.property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
233+
.path("stream")
234+
.queryParam("start", start)
235+
.queryParam("count", count)
236+
.queryParam("pauseMillis", pauseMillis)
237+
.request().get();
238+
fail("This operation should trigger idle timeout");
239+
} catch (ProcessingException e) {
240+
assertEquals(TimeoutException.class, e.getCause().getClass());
241+
}
242+
}
124243
}

0 commit comments

Comments
 (0)