Fixes #2037 - HTTP/2 stream reset leaves stream frames in the flusher.
Now waking up the flusher via iterate() after a reset has been received. This ensures that frames that may have stalled are removed from the flusher queue. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
6b7f906f9d
commit
d88e2b767f
|
@ -46,6 +46,8 @@ import org.eclipse.jetty.http.HttpVersion;
|
||||||
import org.eclipse.jetty.http.MetaData;
|
import org.eclipse.jetty.http.MetaData;
|
||||||
import org.eclipse.jetty.http2.ErrorCode;
|
import org.eclipse.jetty.http2.ErrorCode;
|
||||||
import org.eclipse.jetty.http2.FlowControlStrategy;
|
import org.eclipse.jetty.http2.FlowControlStrategy;
|
||||||
|
import org.eclipse.jetty.http2.HTTP2Flusher;
|
||||||
|
import org.eclipse.jetty.http2.HTTP2Session;
|
||||||
import org.eclipse.jetty.http2.ISession;
|
import org.eclipse.jetty.http2.ISession;
|
||||||
import org.eclipse.jetty.http2.IStream;
|
import org.eclipse.jetty.http2.IStream;
|
||||||
import org.eclipse.jetty.http2.api.Session;
|
import org.eclipse.jetty.http2.api.Session;
|
||||||
|
@ -54,6 +56,7 @@ import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
||||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||||
import org.eclipse.jetty.http2.frames.ResetFrame;
|
import org.eclipse.jetty.http2.frames.ResetFrame;
|
||||||
|
import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory;
|
||||||
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
|
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
|
||||||
import org.eclipse.jetty.server.HttpChannel;
|
import org.eclipse.jetty.server.HttpChannel;
|
||||||
import org.eclipse.jetty.server.HttpConfiguration;
|
import org.eclipse.jetty.server.HttpConfiguration;
|
||||||
|
@ -631,6 +634,55 @@ public class StreamResetTest extends AbstractTest
|
||||||
Assert.assertTrue(writeLatch.await(5, TimeUnit.SECONDS));
|
Assert.assertTrue(writeLatch.await(5, TimeUnit.SECONDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResetAfterBlockingWrite() throws Exception
|
||||||
|
{
|
||||||
|
int windowSize = FlowControlStrategy.DEFAULT_WINDOW_SIZE;
|
||||||
|
CountDownLatch writeLatch = new CountDownLatch(1);
|
||||||
|
start(new HttpServlet()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ServletOutputStream output = response.getOutputStream();
|
||||||
|
output.write(new byte[10 * windowSize]);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
writeLatch.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AtomicLong received = new AtomicLong();
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
Session client = newClient(new Session.Listener.Adapter());
|
||||||
|
MetaData.Request request = newRequest("GET", new HttpFields());
|
||||||
|
HeadersFrame frame = new HeadersFrame(request, null, true);
|
||||||
|
FuturePromise<Stream> promise = new FuturePromise<>();
|
||||||
|
client.newStream(frame, promise, new Stream.Listener.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onData(Stream stream, DataFrame frame, Callback callback)
|
||||||
|
{
|
||||||
|
if (received.addAndGet(frame.getData().remaining()) == windowSize)
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Stream stream = promise.get(5, TimeUnit.SECONDS);
|
||||||
|
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
// Reset.
|
||||||
|
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP);
|
||||||
|
Assert.assertTrue(writeLatch.await(5, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
HTTP2Session session = connector.getConnectionFactory(AbstractHTTP2ServerConnectionFactory.class).getBean(HTTP2Session.class);
|
||||||
|
HTTP2Flusher flusher = session.getBean(HTTP2Flusher.class);
|
||||||
|
Assert.assertEquals(0, flusher.getFrameQueueSize());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testResetAfterAsyncRequestAsyncWriteStalledByFlowControl() throws Exception
|
public void testResetAfterAsyncRequestAsyncWriteStalledByFlowControl() throws Exception
|
||||||
{
|
{
|
||||||
|
|
|
@ -116,7 +116,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getFrameQueueSize()
|
public int getFrameQueueSize()
|
||||||
{
|
{
|
||||||
synchronized (this)
|
synchronized (this)
|
||||||
{
|
{
|
||||||
|
@ -141,15 +141,12 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||||
entry.perform();
|
entry.perform();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!frames.isEmpty())
|
for (Entry entry : frames)
|
||||||
{
|
{
|
||||||
for (Entry entry : frames)
|
entries.offer(entry);
|
||||||
{
|
actives.add(entry);
|
||||||
entries.offer(entry);
|
|
||||||
actives.add(entry);
|
|
||||||
}
|
|
||||||
frames.clear();
|
|
||||||
}
|
}
|
||||||
|
frames.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -166,11 +163,11 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("Processing {}", entry);
|
LOG.debug("Processing {}", entry);
|
||||||
|
|
||||||
// If the stream has been reset, don't send the frame.
|
// If the stream has been reset or removed, don't send the frame.
|
||||||
if (entry.reset())
|
if (entry.isStale())
|
||||||
{
|
{
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("Resetting {}", entry);
|
LOG.debug("Stale {}", entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,7 +325,6 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||||
{
|
{
|
||||||
protected final Frame frame;
|
protected final Frame frame;
|
||||||
protected final IStream stream;
|
protected final IStream stream;
|
||||||
private boolean reset;
|
|
||||||
|
|
||||||
protected Entry(Frame frame, IStream stream, Callback callback)
|
protected Entry(Frame frame, IStream stream, Callback callback)
|
||||||
{
|
{
|
||||||
|
@ -346,7 +342,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||||
|
|
||||||
private void complete()
|
private void complete()
|
||||||
{
|
{
|
||||||
if (reset)
|
if (!isProtocol() && stream != null && stream.isReset())
|
||||||
failed(new EofException("reset"));
|
failed(new EofException("reset"));
|
||||||
else
|
else
|
||||||
succeeded();
|
succeeded();
|
||||||
|
@ -363,23 +359,31 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
||||||
super.failed(x);
|
super.failed(x);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean reset()
|
private boolean isStale()
|
||||||
{
|
{
|
||||||
return this.reset = stream != null && stream.isReset() && !isProtocol();
|
return !isProtocol() && stream != null && (stream.isReset() || stream.getSession().getStream(stream.getId()) == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isProtocol()
|
private boolean isProtocol()
|
||||||
{
|
{
|
||||||
switch (frame.getType())
|
switch (frame.getType())
|
||||||
{
|
{
|
||||||
|
case DATA:
|
||||||
|
case HEADERS:
|
||||||
|
case PUSH_PROMISE:
|
||||||
|
case CONTINUATION:
|
||||||
|
return false;
|
||||||
case PRIORITY:
|
case PRIORITY:
|
||||||
case RST_STREAM:
|
case RST_STREAM:
|
||||||
|
case SETTINGS:
|
||||||
|
case PING:
|
||||||
case GO_AWAY:
|
case GO_AWAY:
|
||||||
case WINDOW_UPDATE:
|
case WINDOW_UPDATE:
|
||||||
|
case PREFACE:
|
||||||
case DISCONNECT:
|
case DISCONNECT:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -282,7 +282,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
||||||
|
|
||||||
IStream stream = getStream(frame.getStreamId());
|
IStream stream = getStream(frame.getStreamId());
|
||||||
if (stream != null)
|
if (stream != null)
|
||||||
stream.process(frame, Callback.NOOP);
|
stream.process(frame, new ResetCallback());
|
||||||
else
|
else
|
||||||
notifyReset(this, frame);
|
notifyReset(this, frame);
|
||||||
}
|
}
|
||||||
|
@ -753,8 +753,6 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
||||||
IStream removed = streams.remove(stream.getId());
|
IStream removed = streams.remove(stream.getId());
|
||||||
if (removed != null)
|
if (removed != null)
|
||||||
{
|
{
|
||||||
assert removed == stream;
|
|
||||||
|
|
||||||
boolean local = stream.isLocal();
|
boolean local = stream.isLocal();
|
||||||
if (local)
|
if (local)
|
||||||
localStreamCount.decrementAndGet();
|
localStreamCount.decrementAndGet();
|
||||||
|
@ -1329,6 +1327,32 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ResetCallback implements Callback
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void succeeded()
|
||||||
|
{
|
||||||
|
complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void failed(Throwable x)
|
||||||
|
{
|
||||||
|
complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InvocationType getInvocationType()
|
||||||
|
{
|
||||||
|
return InvocationType.NON_BLOCKING;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void complete()
|
||||||
|
{
|
||||||
|
flusher.iterate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class CloseCallback implements Callback
|
private class CloseCallback implements Callback
|
||||||
{
|
{
|
||||||
private final int error;
|
private final int error;
|
||||||
|
|
|
@ -88,9 +88,8 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
||||||
@Override
|
@Override
|
||||||
public void headers(HeadersFrame frame, Callback callback)
|
public void headers(HeadersFrame frame, Callback callback)
|
||||||
{
|
{
|
||||||
if (!startWrite(callback))
|
if (startWrite(callback))
|
||||||
return;
|
session.frames(this, this, frame, Frame.EMPTY_ARRAY);
|
||||||
session.frames(this, this, frame, Frame.EMPTY_ARRAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -102,9 +101,8 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
||||||
@Override
|
@Override
|
||||||
public void data(DataFrame frame, Callback callback)
|
public void data(DataFrame frame, Callback callback)
|
||||||
{
|
{
|
||||||
if (!startWrite(callback))
|
if (startWrite(callback))
|
||||||
return;
|
session.data(this, this, frame);
|
||||||
session.data(this, this, frame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -292,8 +290,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
||||||
remoteReset = true;
|
remoteReset = true;
|
||||||
close();
|
close();
|
||||||
session.removeStream(this);
|
session.removeStream(this);
|
||||||
callback.succeeded();
|
notifyReset(this, frame, callback);
|
||||||
notifyReset(this, frame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onPush(PushPromiseFrame frame, Callback callback)
|
private void onPush(PushPromiseFrame frame, Callback callback)
|
||||||
|
@ -377,8 +374,8 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
||||||
@Override
|
@Override
|
||||||
public void close()
|
public void close()
|
||||||
{
|
{
|
||||||
closeState.set(CloseState.CLOSED);
|
if (closeState.getAndSet(CloseState.CLOSED) != CloseState.CLOSED)
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -417,14 +414,14 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyReset(Stream stream, ResetFrame frame)
|
private void notifyReset(Stream stream, ResetFrame frame, Callback callback)
|
||||||
{
|
{
|
||||||
final Listener listener = this.listener;
|
final Listener listener = this.listener;
|
||||||
if (listener == null)
|
if (listener == null)
|
||||||
return;
|
return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
listener.onReset(stream, frame);
|
listener.onReset(stream, frame, callback);
|
||||||
}
|
}
|
||||||
catch (Throwable x)
|
catch (Throwable x)
|
||||||
{
|
{
|
||||||
|
|
|
@ -163,6 +163,19 @@ public interface Stream
|
||||||
*/
|
*/
|
||||||
public void onData(Stream stream, DataFrame frame, Callback callback);
|
public void onData(Stream stream, DataFrame frame, Callback callback);
|
||||||
|
|
||||||
|
public default void onReset(Stream stream, ResetFrame frame, Callback callback)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
onReset(stream, frame);
|
||||||
|
callback.succeeded();
|
||||||
|
}
|
||||||
|
catch (Throwable x)
|
||||||
|
{
|
||||||
|
callback.failed(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Callback method invoked when a RST_STREAM frame has been received for this stream.</p>
|
* <p>Callback method invoked when a RST_STREAM frame has been received for this stream.</p>
|
||||||
*
|
*
|
||||||
|
@ -170,7 +183,9 @@ public interface Stream
|
||||||
* @param frame the RST_FRAME received
|
* @param frame the RST_FRAME received
|
||||||
* @see Session.Listener#onReset(Session, ResetFrame)
|
* @see Session.Listener#onReset(Session, ResetFrame)
|
||||||
*/
|
*/
|
||||||
public void onReset(Stream stream, ResetFrame frame);
|
public default void onReset(Stream stream, ResetFrame frame)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Callback method invoked when the stream exceeds its idle timeout.</p>
|
* <p>Callback method invoked when the stream exceeds its idle timeout.</p>
|
||||||
|
|
|
@ -163,12 +163,12 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReset(Stream stream, ResetFrame frame)
|
public void onReset(Stream stream, ResetFrame frame, Callback callback)
|
||||||
{
|
{
|
||||||
ErrorCode error = ErrorCode.from(frame.getError());
|
ErrorCode error = ErrorCode.from(frame.getError());
|
||||||
if (error == null)
|
if (error == null)
|
||||||
error = ErrorCode.CANCEL_STREAM_ERROR;
|
error = ErrorCode.CANCEL_STREAM_ERROR;
|
||||||
getConnection().onStreamFailure((IStream)stream, new EofException("HTTP/2 " + error), Callback.NOOP);
|
getConnection().onStreamFailure((IStream)stream, new EofException("HTTP/2 " + error), callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in New Issue