Test wrapping of response for async request
The following adjustments are also made as a result: - Use int to check if lock is held and unlock is needed, given that for non-async requests we don't need to obtain a lock. - Protect access methods getOutputStream and getWriter with the same locking and state checks. Closes gh-32340
This commit is contained in:
@@ -21,7 +21,6 @@ import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -189,7 +188,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
public void onError(AsyncEvent event) throws IOException {
|
||||
this.stateLock.lock();
|
||||
try {
|
||||
transitionToErrorState();
|
||||
this.state = State.ERROR;
|
||||
Throwable ex = event.getThrowable();
|
||||
this.exceptionHandlers.forEach(consumer -> consumer.accept(ex));
|
||||
}
|
||||
@@ -198,12 +197,6 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
}
|
||||
}
|
||||
|
||||
private void transitionToErrorState() {
|
||||
if (!isAsyncComplete()) {
|
||||
this.state = State.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(AsyncEvent event) throws IOException {
|
||||
this.stateLock.lock();
|
||||
@@ -218,8 +211,17 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Package private access for testing only.
|
||||
*/
|
||||
ReentrantLock stateLock() {
|
||||
return this.stateLock;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Response wrapper to wrap the output stream with {@link LifecycleServletOutputStream}.
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private static final class LifecycleHttpServletResponse extends HttpServletResponseWrapper {
|
||||
|
||||
@@ -242,26 +244,44 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public ServletOutputStream getOutputStream() throws IOException {
|
||||
if (this.outputStream == null) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
ServletOutputStream delegate = getResponse().getOutputStream();
|
||||
this.outputStream = new LifecycleServletOutputStream(delegate, this);
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
if (this.outputStream == null) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
ServletOutputStream delegate = getResponse().getOutputStream();
|
||||
this.outputStream = new LifecycleServletOutputStream(delegate, this);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
handleIOException(ex, "Failed to get ServletResponseOutput");
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
return this.outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getWriter() throws IOException {
|
||||
if (this.writer == null) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
this.writer = new LifecyclePrintWriter(getResponse().getWriter(), this.asyncWebRequest);
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
if (this.writer == null) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
this.writer = new LifecyclePrintWriter(getResponse().getWriter(), this.asyncWebRequest);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
handleIOException(ex, "Failed to get PrintWriter");
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
return this.writer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushBuffer() throws IOException {
|
||||
obtainLockAndCheckState();
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
getResponse().flushBuffer();
|
||||
}
|
||||
@@ -269,32 +289,40 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
handleIOException(ex, "ServletResponse failed to flushBuffer");
|
||||
}
|
||||
finally {
|
||||
releaseLock();
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
private void obtainLockAndCheckState() throws AsyncRequestNotUsableException {
|
||||
/**
|
||||
* Return 0 if checks passed and lock is not needed, 1 if checks passed
|
||||
* and lock is held, or raise AsyncRequestNotUsableException.
|
||||
*/
|
||||
private int obtainLockAndCheckState() throws AsyncRequestNotUsableException {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
if (this.asyncWebRequest.state != State.NEW) {
|
||||
this.asyncWebRequest.stateLock.lock();
|
||||
if (this.asyncWebRequest.state != State.ASYNC) {
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
throw new AsyncRequestNotUsableException("Response not usable after " +
|
||||
(this.asyncWebRequest.state == State.COMPLETED ?
|
||||
"async request completion" : "onError notification") + ".");
|
||||
}
|
||||
if (this.asyncWebRequest.state == State.NEW) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.asyncWebRequest.stateLock.lock();
|
||||
if (this.asyncWebRequest.state == State.ASYNC) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
throw new AsyncRequestNotUsableException("Response not usable after " +
|
||||
(this.asyncWebRequest.state == State.COMPLETED ?
|
||||
"async request completion" : "response errors") + ".");
|
||||
}
|
||||
|
||||
void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
this.asyncWebRequest.transitionToErrorState();
|
||||
throw new AsyncRequestNotUsableException(msg, ex);
|
||||
this.asyncWebRequest.state = State.ERROR;
|
||||
throw new AsyncRequestNotUsableException(msg + ": " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
void releaseLock() {
|
||||
void releaseLock(int level) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
if (this.asyncWebRequest.state != State.NEW) {
|
||||
if (level > 0) {
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
}
|
||||
}
|
||||
@@ -304,6 +332,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
/**
|
||||
* Wraps a ServletOutputStream to prevent use after Servlet container onError
|
||||
* notifications, and after async request completion.
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private static final class LifecycleServletOutputStream extends ServletOutputStream {
|
||||
|
||||
@@ -328,7 +357,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
this.response.obtainLockAndCheckState();
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.write(b);
|
||||
}
|
||||
@@ -336,12 +365,12 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
this.response.handleIOException(ex, "ServletOutputStream failed to write");
|
||||
}
|
||||
finally {
|
||||
this.response.releaseLock();
|
||||
this.response.releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
public void write(byte[] buf, int offset, int len) throws IOException {
|
||||
this.response.obtainLockAndCheckState();
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.write(buf, offset, len);
|
||||
}
|
||||
@@ -349,13 +378,13 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
this.response.handleIOException(ex, "ServletOutputStream failed to write");
|
||||
}
|
||||
finally {
|
||||
this.response.releaseLock();
|
||||
this.response.releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
this.response.obtainLockAndCheckState();
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.flush();
|
||||
}
|
||||
@@ -363,13 +392,13 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
this.response.handleIOException(ex, "ServletOutputStream failed to flush");
|
||||
}
|
||||
finally {
|
||||
this.response.releaseLock();
|
||||
this.response.releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.response.obtainLockAndCheckState();
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.close();
|
||||
}
|
||||
@@ -377,7 +406,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
this.response.handleIOException(ex, "ServletOutputStream failed to close");
|
||||
}
|
||||
finally {
|
||||
this.response.releaseLock();
|
||||
this.response.releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +416,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
/**
|
||||
* Wraps a PrintWriter to prevent use after Servlet container onError
|
||||
* notifications, and after async request completion.
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private static final class LifecyclePrintWriter extends PrintWriter {
|
||||
|
||||
@@ -402,24 +432,26 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
if (tryObtainLockAndCheckState()) {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.flush();
|
||||
}
|
||||
finally {
|
||||
releaseLock();
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (tryObtainLockAndCheckState()) {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.close();
|
||||
}
|
||||
finally {
|
||||
releaseLock();
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,24 +463,26 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void write(int c) {
|
||||
if (tryObtainLockAndCheckState()) {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(c);
|
||||
}
|
||||
finally {
|
||||
releaseLock();
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] buf, int off, int len) {
|
||||
if (tryObtainLockAndCheckState()) {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(buf, off, len);
|
||||
}
|
||||
finally {
|
||||
releaseLock();
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,12 +494,13 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void write(String s, int off, int len) {
|
||||
if (tryObtainLockAndCheckState()) {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(s, off, len);
|
||||
}
|
||||
finally {
|
||||
releaseLock();
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -475,33 +510,28 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
this.delegate.write(s);
|
||||
}
|
||||
|
||||
private boolean tryObtainLockAndCheckState() {
|
||||
if (state() == State.NEW) {
|
||||
return true;
|
||||
/**
|
||||
* Return 0 if checks passed and lock is not needed, 1 if checks passed
|
||||
* and lock is held, and -1 if checks did not pass.
|
||||
*/
|
||||
private int tryObtainLockAndCheckState() {
|
||||
if (this.asyncWebRequest.state == State.NEW) {
|
||||
return 0;
|
||||
}
|
||||
if (stateLock().tryLock()) {
|
||||
if (state() == State.ASYNC) {
|
||||
return true;
|
||||
}
|
||||
stateLock().unlock();
|
||||
this.asyncWebRequest.stateLock.lock();
|
||||
if (this.asyncWebRequest.state == State.ASYNC) {
|
||||
return 1;
|
||||
}
|
||||
return false;
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void releaseLock() {
|
||||
if (state() != State.NEW) {
|
||||
stateLock().unlock();
|
||||
private void releaseLock(int level) {
|
||||
if (level > 0) {
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private State state() {
|
||||
return this.asyncWebRequest.state;
|
||||
}
|
||||
|
||||
private Lock stateLock() {
|
||||
return this.asyncWebRequest.stateLock;
|
||||
}
|
||||
|
||||
// Plain delegates
|
||||
|
||||
@Override
|
||||
@@ -639,28 +669,28 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
/**
|
||||
* Represents a state for {@link StandardServletAsyncWebRequest} to be in.
|
||||
* <p><pre>
|
||||
* NEW
|
||||
* |
|
||||
* v
|
||||
* ASYNC----> +
|
||||
* | |
|
||||
* v |
|
||||
* ERROR |
|
||||
* | |
|
||||
* v |
|
||||
* COMPLETED <--+
|
||||
* +------ NEW
|
||||
* | |
|
||||
* | v
|
||||
* | ASYNC ----> +
|
||||
* | | |
|
||||
* | v |
|
||||
* +----> ERROR |
|
||||
* | |
|
||||
* v |
|
||||
* COMPLETED <---+
|
||||
* </pre>
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private enum State {
|
||||
|
||||
/** New request (thas may not do async handling). */
|
||||
/** New request (may not start async handling). */
|
||||
NEW,
|
||||
|
||||
/** Async handling has started. */
|
||||
ASYNC,
|
||||
|
||||
/** onError notification received, or ServletOutputStream failed. */
|
||||
/** ServletOutputStream failed, or onError notification received. */
|
||||
ERROR,
|
||||
|
||||
/** onComplete notification received. */
|
||||
|
||||
Reference in New Issue
Block a user