Skip to content
This repository was archived by the owner on May 28, 2018. It is now read-only.

Commit 237d4fa

Browse files
author
Michal Gajdos
committed
JERSEY-2616: AsyncResponse and ChunkedOutput handlers called repeatedly
- fixed resume of suspended continuation with complete call (which doesn't redispatch repeatedly) - reproducible test-case Change-Id: Ica1988f79be6745092e7082c80b410e7d620ed94 Signed-off-by: Michal Gajdos <[email protected]>
1 parent 9300da5 commit 237d4fa

File tree

3 files changed

+103
-46
lines changed

3 files changed

+103
-46
lines changed

containers/jetty-http/pom.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@
4040
holder.
4141
4242
-->
43-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
43+
<project xmlns="http://maven.apache.org/POM/4.0.0"
44+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
45+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
46+
<modelVersion>4.0.0</modelVersion>
47+
4448
<parent>
4549
<artifactId>project</artifactId>
4650
<groupId>org.glassfish.jersey.containers</groupId>
4751
<version>2.13-SNAPSHOT</version>
4852
</parent>
49-
<modelVersion>4.0.0</modelVersion>
5053

5154
<artifactId>jersey-container-jetty-http</artifactId>
5255
<packaging>jar</packaging>
@@ -64,12 +67,10 @@
6467
<groupId>org.eclipse.jetty</groupId>
6568
<artifactId>jetty-server</artifactId>
6669
</dependency>
67-
6870
<dependency>
6971
<groupId>org.eclipse.jetty</groupId>
7072
<artifactId>jetty-continuation</artifactId>
7173
</dependency>
72-
7374
</dependencies>
7475

7576
<build>

containers/jetty-http/src/main/java/org/glassfish/jersey/jetty/JettyHttpContainer.java

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,13 @@
101101
*/
102102
public final class JettyHttpContainer extends AbstractHandler implements Container {
103103

104-
private static final ExtendedLogger logger =
104+
private static final ExtendedLogger LOGGER =
105105
new ExtendedLogger(Logger.getLogger(JettyHttpContainer.class.getName()), Level.FINEST);
106106

107-
private static final Type RequestTYPE = (new TypeLiteral<Ref<Request>>() {}).getType();
108-
private static final Type ResponseTYPE = (new TypeLiteral<Ref<Response>>() {}).getType();
107+
private static final Type REQUEST_TYPE = (new TypeLiteral<Ref<Request>>() {}).getType();
108+
private static final Type RESPONSE_TYPE = (new TypeLiteral<Ref<Response>>() {}).getType();
109+
110+
private static final int INTERNAL_SERVER_ERROR = javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR.getStatusCode();
109111

110112
/**
111113
* Cached value of configuration property
@@ -161,15 +163,17 @@ protected void configure() {
161163
private volatile ContainerLifecycleListener containerListener;
162164

163165
@Override
164-
public void handle(final String target, final Request request, final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse) throws IOException, ServletException {
166+
public void handle(final String target, final Request request, final HttpServletRequest httpServletRequest,
167+
final HttpServletResponse httpServletResponse) throws IOException, ServletException {
168+
165169
final Response response = Response.getResponse(httpServletResponse);
166170
final ResponseWriter responseWriter = new ResponseWriter(request, response, configSetStatusOverSendError);
167171
final URI baseUri = getBaseUri(request);
168172

169173
final String originalQuery = request.getUri().getQuery();
170174
final String encodedQuery = ContainerUtils.encodeUnsafeCharacters(originalQuery);
171-
final String uriString = (originalQuery == null || originalQuery.isEmpty() || originalQuery.equals(encodedQuery)) ?
172-
request.getUri().toString() : request.getUri().toString().replace(originalQuery, encodedQuery);
175+
final String uriString = (originalQuery == null || originalQuery.isEmpty() || originalQuery.equals(encodedQuery))
176+
? request.getUri().toString() : request.getUri().toString().replace(originalQuery, encodedQuery);
173177
final URI requestUri = baseUri.resolve(uriString);
174178
try {
175179
final ContainerRequest requestContext = new ContainerRequest(
@@ -188,8 +192,8 @@ public void handle(final String target, final Request request, final HttpServlet
188192
requestContext.setRequestScopedInitializer(new RequestScopedInitializer() {
189193
@Override
190194
public void initialize(final ServiceLocator locator) {
191-
locator.<Ref<Request>>getService(RequestTYPE).set(request);
192-
locator.<Ref<Response>>getService(ResponseTYPE).set(response);
195+
locator.<Ref<Request>>getService(REQUEST_TYPE).set(request);
196+
locator.<Ref<Response>>getService(RESPONSE_TYPE).set(response);
193197
}
194198
});
195199

@@ -250,6 +254,7 @@ private String getBasePath(final Request request) {
250254
}
251255

252256
private static final class ResponseWriter implements ContainerResponseWriter {
257+
253258
private final Response response;
254259
private final Continuation continuation;
255260
private final boolean configSetStatusOverSendError;
@@ -261,11 +266,14 @@ private static final class ResponseWriter implements ContainerResponseWriter {
261266
}
262267

263268
@Override
264-
public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse context) throws ContainerException {
269+
public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse context)
270+
throws ContainerException {
271+
265272
final javax.ws.rs.core.Response.StatusType statusInfo = context.getStatusInfo();
266273

267274
final int code = statusInfo.getStatusCode();
268-
final String reason = statusInfo.getReasonPhrase() == null ? HttpStatus.getMessage(code) : statusInfo.getReasonPhrase();
275+
final String reason = statusInfo.getReasonPhrase() == null
276+
? HttpStatus.getMessage(code) : statusInfo.getReasonPhrase();
269277

270278
response.setStatusWithReason(code, reason);
271279

@@ -304,7 +312,7 @@ public void onTimeout(final Continuation continuation) {
304312
}
305313
}
306314
});
307-
continuation.suspend();
315+
continuation.suspend(response);
308316
return true;
309317
} catch (final Exception ex) {
310318
return false;
@@ -322,14 +330,14 @@ public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) throw
322330
@Override
323331
public void commit() {
324332
try {
325-
if (continuation.isSuspended()) {
326-
continuation.resume();
327-
}
328333
response.closeOutput();
329334
} catch (final IOException e) {
330-
logger.log(Level.WARNING, LocalizationMessages.UNABLE_TO_CLOSE_RESPONSE(), e);
335+
LOGGER.log(Level.WARNING, LocalizationMessages.UNABLE_TO_CLOSE_RESPONSE(), e);
331336
} finally {
332-
logger.log(Level.FINEST, "commit() called");
337+
if (continuation.isSuspended()) {
338+
continuation.complete();
339+
}
340+
LOGGER.log(Level.FINEST, "commit() called");
333341
}
334342
}
335343

@@ -340,21 +348,21 @@ public void failure(final Throwable error) {
340348
try {
341349
if (configSetStatusOverSendError) {
342350
response.reset();
343-
response.setStatus(500, "Request failed.");
351+
//noinspection deprecation
352+
response.setStatus(INTERNAL_SERVER_ERROR, "Request failed.");
344353
} else {
345-
response.sendError(500, "Request failed.");
354+
response.sendError(INTERNAL_SERVER_ERROR, "Request failed.");
346355
}
347356
} catch (final IllegalStateException ex) {
348357
// a race condition externally committing the response can still occur...
349-
logger.log(Level.FINER, "Unable to reset failed response.", ex);
358+
LOGGER.log(Level.FINER, "Unable to reset failed response.", ex);
350359
} catch (final IOException ex) {
351-
throw new ContainerException(
352-
LocalizationMessages.EXCEPTION_SENDING_ERROR_RESPONSE(500, "Request failed."),
353-
ex);
360+
throw new ContainerException(LocalizationMessages.EXCEPTION_SENDING_ERROR_RESPONSE(INTERNAL_SERVER_ERROR,
361+
"Request failed."), ex);
354362
}
355363
}
356364
} finally {
357-
logger.log(Level.FINEST, "failure(...) called");
365+
LOGGER.log(Level.FINEST, "failure(...) called");
358366
commit();
359367
rethrow(error);
360368
}
@@ -365,9 +373,8 @@ public boolean enableResponseBuffering() {
365373
return false;
366374
}
367375

368-
369376
/**
370-
* Rethrow the original exception as required by JAX-RS, 3.3.4
377+
* Rethrow the original exception as required by JAX-RS, 3.3.4.
371378
*
372379
* @param error throwable to be re-thrown
373380
*/
@@ -381,7 +388,6 @@ private void rethrow(final Throwable error) {
381388

382389
}
383390

384-
385391
@Override
386392
public ResourceConfig getConfiguration() {
387393
return appHandler.getConfiguration();
@@ -409,8 +415,9 @@ public ApplicationHandler getApplicationHandler() {
409415

410416
/**
411417
* Inform this container that the server has been started.
412-
*
413418
* This method must be implicitly called after the server containing this container is started.
419+
*
420+
* @throws java.lang.Exception if a problem occurred during server startup.
414421
*/
415422
@Override
416423
protected void doStart() throws Exception {
@@ -420,8 +427,9 @@ protected void doStart() throws Exception {
420427

421428
/**
422429
* Inform this container that the server is being stopped.
423-
*
424430
* This method must be implicitly called before the server containing this container is stopped.
431+
*
432+
* @throws java.lang.Exception if a problem occurred during server shutdown.
425433
*/
426434
@Override
427435
public void doStop() throws Exception {

containers/jetty-http/src/test/java/org/glassfish/jersey/jetty/AsyncTest.java

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
33
*
4-
* Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved.
4+
* Copyright (c) 2013-2014 Oracle and/or its affiliates. All rights reserved.
55
*
66
* The contents of this file are subject to the terms of either the GNU
77
* General Public License Version 2 only ("GPL") or the Common Development
@@ -39,7 +39,11 @@
3939
*/
4040
package org.glassfish.jersey.jetty;
4141

42-
import org.junit.Test;
42+
import java.util.concurrent.ExecutionException;
43+
import java.util.concurrent.Future;
44+
import java.util.concurrent.TimeUnit;
45+
import java.util.concurrent.TimeoutException;
46+
import java.util.concurrent.atomic.AtomicInteger;
4347

4448
import javax.ws.rs.GET;
4549
import javax.ws.rs.Path;
@@ -49,32 +53,41 @@
4953
import javax.ws.rs.container.Suspended;
5054
import javax.ws.rs.container.TimeoutHandler;
5155
import javax.ws.rs.core.Response;
52-
import java.util.concurrent.*;
5356

57+
import org.junit.After;
58+
import org.junit.Before;
59+
import org.junit.Test;
60+
import static org.hamcrest.CoreMatchers.is;
5461
import static org.junit.Assert.assertEquals;
62+
import static org.junit.Assert.assertThat;
5563

5664
/**
5765
* @author Arul Dhesiaseelan (aruld at acm.org)
66+
* @author Michal Gajdos (michal.gajdos at oracle.com)
5867
*/
5968
public class AsyncTest extends AbstractJettyServerTester {
6069

6170
@Path("/async")
71+
@SuppressWarnings("VoidMethodAnnotatedWithGET")
6272
public static class AsyncResource {
73+
74+
public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0);
75+
6376
@GET
6477
public void asyncGet(@Suspended final AsyncResponse asyncResponse) {
6578
new Thread(new Runnable() {
6679

6780
@Override
6881
public void run() {
69-
String result = veryExpensiveOperation();
82+
final String result = veryExpensiveOperation();
7083
asyncResponse.resume(result);
7184
}
7285

7386
private String veryExpensiveOperation() {
7487
// ... very expensive operation that typically finishes within 5 seconds, simulated using sleep()
7588
try {
7689
Thread.sleep(5000);
77-
} catch (InterruptedException e) {
90+
} catch (final InterruptedException e) {
7891
// ignore
7992
}
8093
return "DONE";
@@ -88,9 +101,9 @@ public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
88101
asyncResponse.setTimeoutHandler(new TimeoutHandler() {
89102

90103
@Override
91-
public void handleTimeout(AsyncResponse asyncResponse) {
92-
asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
93-
.entity("Operation time out.").build());
104+
public void handleTimeout(final AsyncResponse asyncResponse) {
105+
asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.")
106+
.build());
94107
}
95108
});
96109
asyncResponse.setTimeout(3, TimeUnit.SECONDS);
@@ -99,28 +112,53 @@ public void handleTimeout(AsyncResponse asyncResponse) {
99112

100113
@Override
101114
public void run() {
102-
String result = veryExpensiveOperation();
115+
final String result = veryExpensiveOperation();
103116
asyncResponse.resume(result);
104117
}
105118

106119
private String veryExpensiveOperation() {
107120
// ... very expensive operation that typically finishes within 10 seconds, simulated using sleep()
108121
try {
109122
Thread.sleep(7000);
110-
} catch (InterruptedException e) {
123+
} catch (final InterruptedException e) {
111124
// ignore
112125
}
113126
return "DONE";
114127
}
115128
}).start();
116129
}
117130

131+
@GET
132+
@Path("multiple-invocations")
133+
public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) {
134+
INVOCATION_COUNT.incrementAndGet();
135+
136+
new Thread(new Runnable() {
137+
@Override
138+
public void run() {
139+
asyncResponse.resume("OK");
140+
}
141+
}).start();
142+
}
143+
}
144+
145+
private Client client;
146+
147+
@Before
148+
public void setUp() throws Exception {
149+
startServer(AsyncResource.class);
150+
client = ClientBuilder.newClient();
151+
}
152+
153+
@Override
154+
@After
155+
public void tearDown() {
156+
super.tearDown();
157+
client = null;
118158
}
119159

120160
@Test
121161
public void testAsyncGet() throws ExecutionException, InterruptedException {
122-
startServer(AsyncResource.class);
123-
Client client = ClientBuilder.newClient();
124162
final Future<Response> responseFuture = client.target(getUri().path("/async")).request().async().get();
125163
// Request is being processed asynchronously.
126164
final Response response = responseFuture.get();
@@ -130,8 +168,6 @@ public void testAsyncGet() throws ExecutionException, InterruptedException {
130168

131169
@Test
132170
public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException {
133-
startServer(AsyncResource.class);
134-
Client client = ClientBuilder.newClient();
135171
final Future<Response> responseFuture = client.target(getUri().path("/async/timeout")).request().async().get();
136172
// Request is being processed asynchronously.
137173
final Response response = responseFuture.get();
@@ -141,4 +177,16 @@ public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedExce
141177
assertEquals("Operation time out.", response.readEntity(String.class));
142178
}
143179

180+
/**
181+
* JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request.
182+
*/
183+
@Test
184+
public void testAsyncMultipleInvocations() throws Exception {
185+
final Response response = client.target(getUri().path("/async/multiple-invocations")).request().get();
186+
187+
assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1));
188+
189+
assertThat(response.getStatus(), is(200));
190+
assertThat(response.readEntity(String.class), is("OK"));
191+
}
144192
}

0 commit comments

Comments
 (0)