Issue #6728 - QUIC and HTTP/3
- Removed dataFrame queue in HTTP3StreamConnection. - Made readData() idempotent. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
21464f85ff
commit
5ca88d3231
|
@ -33,7 +33,7 @@ public abstract class AbstractConnectorHttpClientTransport extends AbstractHttpC
|
||||||
protected AbstractConnectorHttpClientTransport(ClientConnector connector)
|
protected AbstractConnectorHttpClientTransport(ClientConnector connector)
|
||||||
{
|
{
|
||||||
this.connector = Objects.requireNonNull(connector);
|
this.connector = Objects.requireNonNull(connector);
|
||||||
addBean(connector);
|
addBean(connector, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientConnector getClientConnector()
|
public ClientConnector getClientConnector()
|
||||||
|
|
|
@ -16,8 +16,6 @@ package org.eclipse.jetty.http3.internal;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
import org.eclipse.jetty.http.MetaData;
|
import org.eclipse.jetty.http.MetaData;
|
||||||
|
@ -42,7 +40,6 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
private static final ByteBuffer EMPTY_DATA_FRAME = ByteBuffer.allocate(2);
|
private static final ByteBuffer EMPTY_DATA_FRAME = ByteBuffer.allocate(2);
|
||||||
|
|
||||||
private final AutoLock lock = new AutoLock();
|
private final AutoLock lock = new AutoLock();
|
||||||
private final Queue<DataFrame> dataFrames = new ArrayDeque<>();
|
|
||||||
private final RetainableByteBufferPool buffers;
|
private final RetainableByteBufferPool buffers;
|
||||||
private final MessageParser parser;
|
private final MessageParser parser;
|
||||||
private boolean useInputDirectByteBuffers = true;
|
private boolean useInputDirectByteBuffers = true;
|
||||||
|
@ -50,7 +47,9 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
private boolean dataMode;
|
private boolean dataMode;
|
||||||
private boolean dataDemand;
|
private boolean dataDemand;
|
||||||
private boolean dataStalled;
|
private boolean dataStalled;
|
||||||
|
private DataFrame dataFrame;
|
||||||
private boolean dataLast;
|
private boolean dataLast;
|
||||||
|
private boolean noData;
|
||||||
private boolean remotelyClosed;
|
private boolean remotelyClosed;
|
||||||
|
|
||||||
public HTTP3StreamConnection(QuicStreamEndPoint endPoint, Executor executor, ByteBufferPool byteBufferPool, MessageParser parser)
|
public HTTP3StreamConnection(QuicStreamEndPoint endPoint, Executor executor, ByteBufferPool byteBufferPool, MessageParser parser)
|
||||||
|
@ -120,7 +119,7 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (parseAndFill() == MessageParser.Result.NO_FRAME)
|
if (parseAndFill(true) == MessageParser.Result.NO_FRAME)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// TODO: we should also exit if the connection was closed due to errors.
|
// TODO: we should also exit if the connection was closed due to errors.
|
||||||
|
@ -168,11 +167,12 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
if (hasDemand())
|
if (hasDemand())
|
||||||
throw new IllegalStateException("invalid call to readData(): outstanding demand");
|
throw new IllegalStateException("invalid call to readData(): outstanding demand");
|
||||||
|
|
||||||
switch (parseAndFill())
|
switch (parseAndFill(false))
|
||||||
{
|
{
|
||||||
case FRAME:
|
case FRAME:
|
||||||
{
|
{
|
||||||
DataFrame frame = dataFrames.poll();
|
DataFrame frame = dataFrame;
|
||||||
|
dataFrame = null;
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("read data {} on {}", frame, this);
|
LOG.debug("read data {} on {}", frame, this);
|
||||||
if (frame == null)
|
if (frame == null)
|
||||||
|
@ -193,6 +193,8 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
}
|
}
|
||||||
case NO_FRAME:
|
case NO_FRAME:
|
||||||
{
|
{
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("read no data on {}", this);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -211,9 +213,11 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
|
|
||||||
public void demand()
|
public void demand()
|
||||||
{
|
{
|
||||||
|
boolean interested;
|
||||||
boolean process = false;
|
boolean process = false;
|
||||||
try (AutoLock l = lock.lock())
|
try (AutoLock l = lock.lock())
|
||||||
{
|
{
|
||||||
|
interested = noData;
|
||||||
dataDemand = true;
|
dataDemand = true;
|
||||||
if (dataStalled)
|
if (dataStalled)
|
||||||
{
|
{
|
||||||
|
@ -225,6 +229,8 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
LOG.debug("demand, wasStalled={} on {}", process, this);
|
LOG.debug("demand, wasStalled={} on {}", process, this);
|
||||||
if (process)
|
if (process)
|
||||||
processDataDemand();
|
processDataDemand();
|
||||||
|
else if (interested)
|
||||||
|
fillInterested();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasDemand()
|
public boolean hasDemand()
|
||||||
|
@ -243,6 +249,14 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setNoData(boolean noData)
|
||||||
|
{
|
||||||
|
try (AutoLock l = lock.lock())
|
||||||
|
{
|
||||||
|
this.noData = noData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void processDataDemand()
|
private void processDataDemand()
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
|
@ -275,16 +289,18 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageParser.Result parseAndFill()
|
public MessageParser.Result parseAndFill(boolean setFillInterest)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("parse+fill on {} with buffer {}", this, buffer);
|
LOG.debug("parse+fill interest={} on {} with buffer {}", setFillInterest, this, buffer);
|
||||||
|
|
||||||
if (buffer == null)
|
if (buffer == null)
|
||||||
buffer = buffers.acquire(getInputBufferSize(), isUseInputDirectByteBuffers());
|
buffer = buffers.acquire(getInputBufferSize(), isUseInputDirectByteBuffers());
|
||||||
|
|
||||||
|
setNoData(false);
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
ByteBuffer byteBuffer = buffer.getBuffer();
|
ByteBuffer byteBuffer = buffer.getBuffer();
|
||||||
|
@ -320,7 +336,9 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
{
|
{
|
||||||
buffer.release();
|
buffer.release();
|
||||||
buffer = null;
|
buffer = null;
|
||||||
fillInterested();
|
setNoData(true);
|
||||||
|
if (setFillInterest)
|
||||||
|
fillInterested();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -355,11 +373,6 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataFrame pollContent()
|
|
||||||
{
|
|
||||||
return dataFrames.poll();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toConnectionString()
|
public String toConnectionString()
|
||||||
{
|
{
|
||||||
|
@ -397,9 +410,11 @@ public abstract class HTTP3StreamConnection extends AbstractConnection
|
||||||
@Override
|
@Override
|
||||||
public void onData(long streamId, DataFrame frame)
|
public void onData(long streamId, DataFrame frame)
|
||||||
{
|
{
|
||||||
remotelyClosed = frame.isLast();
|
if (dataFrame != null)
|
||||||
|
throw new IllegalStateException();
|
||||||
|
dataFrame = frame;
|
||||||
dataLast = frame.isLast();
|
dataLast = frame.isLast();
|
||||||
dataFrames.offer(frame);
|
remotelyClosed = frame.isLast();
|
||||||
super.onData(streamId, frame);
|
super.onData(streamId, frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,6 @@ import org.slf4j.LoggerFactory;
|
||||||
public class HttpChannelOverHTTP3 extends HttpChannel
|
public class HttpChannelOverHTTP3 extends HttpChannel
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverHTTP3.class);
|
private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverHTTP3.class);
|
||||||
private static final HttpInput.Content NULL_CONTENT = new NullContent();
|
|
||||||
|
|
||||||
private static final HttpField SERVER_VERSION = new PreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION);
|
private static final HttpField SERVER_VERSION = new PreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION);
|
||||||
private static final HttpField POWERED_BY = new PreEncodedHttpField(HttpHeader.X_POWERED_BY, HttpConfiguration.SERVER_VERSION);
|
private static final HttpField POWERED_BY = new PreEncodedHttpField(HttpHeader.X_POWERED_BY, HttpConfiguration.SERVER_VERSION);
|
||||||
|
@ -215,9 +214,6 @@ public class HttpChannelOverHTTP3 extends HttpChannel
|
||||||
@Override
|
@Override
|
||||||
public boolean needContent()
|
public boolean needContent()
|
||||||
{
|
{
|
||||||
if (content == NULL_CONTENT)
|
|
||||||
content = null;
|
|
||||||
|
|
||||||
if (content != null)
|
if (content != null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
@ -230,8 +226,6 @@ public class HttpChannelOverHTTP3 extends HttpChannel
|
||||||
{
|
{
|
||||||
if (content != null)
|
if (content != null)
|
||||||
{
|
{
|
||||||
if (content == NULL_CONTENT)
|
|
||||||
return null;
|
|
||||||
HttpInput.Content result = content;
|
HttpInput.Content result = content;
|
||||||
if (!result.isSpecial())
|
if (!result.isSpecial())
|
||||||
content = null;
|
content = null;
|
||||||
|
@ -244,10 +238,7 @@ public class HttpChannelOverHTTP3 extends HttpChannel
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("read {} on {}", data, this);
|
LOG.debug("read {} on {}", data, this);
|
||||||
if (data == null)
|
if (data == null)
|
||||||
{
|
|
||||||
content = NULL_CONTENT;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
content = new HttpInput.Content(data.getByteBuffer())
|
content = new HttpInput.Content(data.getByteBuffer())
|
||||||
{
|
{
|
||||||
|
@ -305,8 +296,4 @@ public class HttpChannelOverHTTP3 extends HttpChannel
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class NullContent extends HttpInput.SpecialContent
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -511,4 +511,56 @@ public class DataDemandTest extends AbstractClientServerTest
|
||||||
|
|
||||||
assertTrue(dataLatch.await(5, TimeUnit.SECONDS));
|
assertTrue(dataLatch.await(5, TimeUnit.SECONDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReadDataIdempotent() throws Exception
|
||||||
|
{
|
||||||
|
CountDownLatch nullDataLatch = new CountDownLatch(1);
|
||||||
|
CountDownLatch lastDataLatch = new CountDownLatch(1);
|
||||||
|
start(new Session.Server.Listener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Stream.Listener onRequest(Stream stream, HeadersFrame frame)
|
||||||
|
{
|
||||||
|
stream.demand();
|
||||||
|
return new Stream.Listener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onDataAvailable(Stream stream)
|
||||||
|
{
|
||||||
|
Stream.Data data = stream.readData();
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
// Second attempt to read still has no data, should be idempotent.
|
||||||
|
assertNull(stream.readData());
|
||||||
|
stream.demand();
|
||||||
|
nullDataLatch.countDown();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data.complete();
|
||||||
|
if (data.isLast())
|
||||||
|
lastDataLatch.countDown();
|
||||||
|
else
|
||||||
|
stream.demand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Session.Client session = newSession(new Session.Client.Listener() {});
|
||||||
|
|
||||||
|
HeadersFrame request = new HeadersFrame(newRequest("/"), false);
|
||||||
|
Stream stream = session.newRequest(request, new Stream.Listener() {}).get(5, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
// Send a first chunk to trigger reads.
|
||||||
|
stream.data(new DataFrame(ByteBuffer.allocate(16), false));
|
||||||
|
|
||||||
|
assertTrue(nullDataLatch.await(555, TimeUnit.SECONDS));
|
||||||
|
|
||||||
|
stream.data(new DataFrame(ByteBuffer.allocate(4096), true));
|
||||||
|
|
||||||
|
assertTrue(lastDataLatch.await(5, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,9 +94,12 @@ public abstract class ProtocolSession extends ContainerLifeCycle
|
||||||
List<Long> readableStreamIds = session.getReadableStreamIds();
|
List<Long> readableStreamIds = session.getReadableStreamIds();
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("readable stream ids: {}", readableStreamIds);
|
LOG.debug("readable stream ids: {}", readableStreamIds);
|
||||||
return readableStreamIds.stream()
|
boolean result = false;
|
||||||
.map(this::onReadable)
|
for (long readableStreamId : readableStreamIds)
|
||||||
.reduce(false, (result, readable) -> result || readable);
|
{
|
||||||
|
result = result || onReadable(readableStreamId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract boolean onReadable(long readableStreamId);
|
protected abstract boolean onReadable(long readableStreamId);
|
||||||
|
|
Loading…
Reference in New Issue