This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 8.5.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/8.5.x by this push: new 50ea37e Fix concurrency issue that caused intermittent h2 test failures 50ea37e is described below commit 50ea37ec4ccdabb615b53744c7c242970bb4ae7a Author: Mark Thomas <ma...@apache.org> AuthorDate: Mon May 13 11:13:38 2019 +0100 Fix concurrency issue that caused intermittent h2 test failures The Stream object was being used for both Stream window allocations and connection window allocations. If a stream allocation occurred while waiting for a connection allocation (and vice versa) processing would continue on the incorrect assumption that the allocation being waited for had occurred. --- .../apache/coyote/http2/Http2UpgradeHandler.java | 60 ++++++++++++---------- .../apache/coyote/http2/LocalStrings.properties | 5 +- java/org/apache/coyote/http2/Stream.java | 22 ++++++-- test/org/apache/coyote/http2/Http2TestBase.java | 10 ++-- webapps/docs/changelog.xml | 4 ++ 5 files changed, 64 insertions(+), 37 deletions(-) diff --git a/java/org/apache/coyote/http2/Http2UpgradeHandler.java b/java/org/apache/coyote/http2/Http2UpgradeHandler.java index a9ab68d..ffb0919 100644 --- a/java/org/apache/coyote/http2/Http2UpgradeHandler.java +++ b/java/org/apache/coyote/http2/Http2UpgradeHandler.java @@ -752,10 +752,11 @@ public class Http2UpgradeHandler extends AbstractStream implements InternalHttpU int reserveWindowSize(Stream stream, int reservation, boolean block) throws IOException { - // Need to be holding the stream lock so releaseBacklog() can't notify - // this thread until after this thread enters wait() + // Need to be holding the connection allocation lock so releaseBacklog() + // can't notify this thread until after this thread enters wait() int allocation = 0; - synchronized (stream) { + Object connectionAllocationLock = stream.getConnectionAllocationLock(); + synchronized (connectionAllocationLock) { do { synchronized (this) { if (!stream.canWrite()) { @@ -810,22 +811,30 @@ public class Http2UpgradeHandler extends AbstractStream implements InternalHttpU // timeout long writeTimeout = protocol.getWriteTimeout(); if (writeTimeout < 0) { - stream.wait(); + connectionAllocationLock.wait(); } else { - stream.wait(writeTimeout); - } - // Has this stream been granted an allocation - // Note: If the stream in not in this Map then the - // requested write has been fully allocated - int[] value = backLogStreams.get(stream); - if (value != null && value[1] == 0) { - // No allocation - // Close the connection. Do this first since - // closing the stream will raise an exception - close(); - // Close the stream (in app code so need to - // signal to app stream is closing) - stream.doWriteTimeout(); + connectionAllocationLock.wait(writeTimeout); + // Has this stream been granted an allocation + // Note: If the stream in not in this Map then the + // requested write has been fully allocated + int[] value; + // Ensure allocations made in other threads are visible + synchronized (this) { + value = backLogStreams.get(stream); + } + if (value != null && value[1] == 0) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("upgradeHandler.noAllocation", + connectionId, stream.getIdentifier())); + } + // No allocation + // Close the connection. Do this first since + // closing the stream will raise an exception + close(); + // Close the stream (in app code so need to + // signal to app stream is closing) + stream.doWriteTimeout(); + } } } catch (InterruptedException e) { throw new IOException(sm.getString( @@ -843,7 +852,7 @@ public class Http2UpgradeHandler extends AbstractStream implements InternalHttpU - @SuppressWarnings("sync-override") // notifyAll() needs to be outside sync + @SuppressWarnings("sync-override") // notify() needs to be outside sync // to avoid deadlock @Override protected void incrementWindowSize(int increment) throws Http2Exception { @@ -872,12 +881,13 @@ public class Http2UpgradeHandler extends AbstractStream implements InternalHttpU Response coyoteResponse = ((Stream) stream).getCoyoteResponse(); if (coyoteResponse.getWriteListener() == null) { if (log.isDebugEnabled()) { - log.debug(sm.getString("upgradeHandler.notifyAll", + log.debug(sm.getString("upgradeHandler.notify", connectionId, stream.getIdentifier())); } // Blocking, so use notify to release StreamOutputBuffer - synchronized (stream) { - stream.notifyAll(); + Object connectionAllocationLock = ((Stream) stream).getConnectionAllocationLock(); + synchronized (connectionAllocationLock) { + connectionAllocationLock.notify(); } } else { if (log.isDebugEnabled()) { @@ -1052,12 +1062,8 @@ public class Http2UpgradeHandler extends AbstractStream implements InternalHttpU for (Stream stream : streams.values()) { // The connection is closing. Close the associated streams as no - // longer required. + // longer required (also notifies any threads waiting for allocations). stream.receiveReset(Http2Error.CANCEL.getCode()); - // Release any streams waiting for an allocation - synchronized (stream) { - stream.notifyAll(); - } } try { socketWrapper.close(); diff --git a/java/org/apache/coyote/http2/LocalStrings.properties b/java/org/apache/coyote/http2/LocalStrings.properties index 24a7d1a..dc3bd1c 100644 --- a/java/org/apache/coyote/http2/LocalStrings.properties +++ b/java/org/apache/coyote/http2/LocalStrings.properties @@ -124,8 +124,9 @@ upgradeHandler.init=Connection [{0}], State [{1}] upgradeHandler.initialWindowSize.invalid=Connection [{0}], Illegal value of [{1}] ignored for initial window size upgradeHandler.invalidPreface=Connection [{0}], Invalid connection preface upgradeHandler.ioerror=Connection [{0}] +upgradeHandler.noAllocation=Connection [{0}], Stream [{1}], Timeout waiting for allocation upgradeHandler.noNewStreams=Connection [{0}], Stream [{1}], Stream ignored as no new streams are permitted on this connection -upgradeHandler.notifyAll=Connection [{0}], Stream [{1}], notifyAll() called to release StreamOutputBuffer +upgradeHandler.notify=Connection [{0}], Stream [{1}], notify() called to release StreamOutputBuffer upgradeHandler.pause.entry=Connection [{0}] Pausing upgradeHandler.prefaceReceived=Connection [{0}], Connection preface received from client upgradeHandler.pingFailed=Connection [{0}] Failed to send ping to client @@ -156,4 +157,4 @@ upgradeHandler.writeHeaders=Connection [{0}], Stream [{1}] upgradeHandler.writePushHeaders=Connection [{0}], Stream [{1}], Pushed stream [{2}], EndOfStream [{3}] writeStateMachine.endWrite.ise=It is illegal to specify [{0}] for the new state once a write has completed -writeStateMachine.ise=It is illegal to call [{0}()] in state [{1}] \ No newline at end of file +writeStateMachine.ise=It is illegal to call [{0}()] in state [{1}] diff --git a/java/org/apache/coyote/http2/Stream.java b/java/org/apache/coyote/http2/Stream.java index d7b2006..7f783fd 100644 --- a/java/org/apache/coyote/http2/Stream.java +++ b/java/org/apache/coyote/http2/Stream.java @@ -69,6 +69,8 @@ public class Stream extends AbstractStream implements HeaderEmitter { private final Http2UpgradeHandler handler; private final StreamStateMachine state; + private final Object connectionAllocationLock = new Object(); + // State machine would be too much overhead private int headerState = HEADER_STATE_START; private StreamException headerException = null; @@ -227,9 +229,18 @@ public class Stream extends AbstractStream implements HeaderEmitter { if (inputBuffer != null) { inputBuffer.receiveReset(); } - // Writes wait on Stream so we can notify directly + cancelAllocationRequests(); + } + + + final void cancelAllocationRequests() { + // Cancel wait on stream allocation request (if any) synchronized (this) { - this.notifyAll(); + this.notify(); + } + // Cancel wait on connection allocation request (if any) + synchronized (connectionAllocationLock) { + connectionAllocationLock.notify(); } } @@ -250,7 +261,7 @@ public class Stream extends AbstractStream implements HeaderEmitter { if (notify && getWindowSize() > 0) { if (coyoteResponse.getWriteListener() == null) { // Blocking, so use notify to release StreamOutputBuffer - notifyAll(); + notify(); } else { // Non-blocking so dispatch coyoteResponse.action(ActionCode.DISPATCH_WRITE, null); @@ -717,6 +728,11 @@ public class Stream extends AbstractStream implements HeaderEmitter { } + Object getConnectionAllocationLock() { + return connectionAllocationLock; + } + + private static void push(final Http2UpgradeHandler handler, final Request request, final Stream stream) throws IOException { if (org.apache.coyote.Constants.IS_SECURITY_ENABLED) { diff --git a/test/org/apache/coyote/http2/Http2TestBase.java b/test/org/apache/coyote/http2/Http2TestBase.java index 827be45..db5451b 100644 --- a/test/org/apache/coyote/http2/Http2TestBase.java +++ b/test/org/apache/coyote/http2/Http2TestBase.java @@ -535,11 +535,11 @@ public abstract class Http2TestBase extends TomcatBaseTest { Connector connector = getTomcatInstance().getConnector(); Http2Protocol http2Protocol = new Http2Protocol(); // Short timeouts for now. May need to increase these for CI systems. - http2Protocol.setReadTimeout(2000); - http2Protocol.setWriteTimeout(2000); - http2Protocol.setKeepAliveTimeout(5000); - http2Protocol.setStreamReadTimeout(1000); - http2Protocol.setStreamWriteTimeout(1000); + http2Protocol.setReadTimeout(6000); + http2Protocol.setWriteTimeout(6000); + http2Protocol.setKeepAliveTimeout(15000); + http2Protocol.setStreamReadTimeout(3000); + http2Protocol.setStreamWriteTimeout(3000); http2Protocol.setMaxConcurrentStreams(maxConcurrentStreams); connector.addUpgradeProtocol(http2Protocol); } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index eaba5dd..6d28e08 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -66,6 +66,10 @@ <bug>63412</bug>: Security manager failure when using the async IO API from a webapp. (remm) </fix> + <fix> + Fix concurrency issue that lead to incorrect HTTP/2 connection timeout. + (remm/markt) + </fix> </changelog> </subsection> <subsection name="Other"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org