* Code cleanups. Signed-off-by: Simone Bordet <simone.bordet@gmail.com> * Improved test case handler. Signed-off-by: Simone Bordet <simone.bordet@gmail.com> * Improved exception message. Signed-off-by: Simone Bordet <simone.bordet@gmail.com> * Issue #1973 - Implement minimum response data rate. Implemented response content data rate control in HttpOutput. Introduced a WriteFlusher.Listener interface that produces events for every flush(). These events are forwarded to the Connection and from there to the HttpOutput so that the data rate control can be enforced. Both HTTP/1.1 and HTTP/2 are implemented. Data rate control for HTTP/1.1 is approximate because it will count also headers bytes and the chunk bytes, while for HTTP/2 is precise. Signed-off-by: Simone Bordet <simone.bordet@gmail.com> * Issue #1973 - Implement minimum response data rate. Addressed review comments. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
parent
696b60a541
commit
e86e8a752c
|
@ -28,6 +28,7 @@ import org.eclipse.jetty.http2.parser.Parser;
|
|||
import org.eclipse.jetty.io.AbstractConnection;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.WriteFlusher;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
|
@ -37,7 +38,7 @@ import org.eclipse.jetty.util.thread.ExecutionStrategy;
|
|||
import org.eclipse.jetty.util.thread.ReservedThreadExecutor;
|
||||
import org.eclipse.jetty.util.thread.strategy.EatWhatYouKill;
|
||||
|
||||
public class HTTP2Connection extends AbstractConnection
|
||||
public class HTTP2Connection extends AbstractConnection implements WriteFlusher.Listener
|
||||
{
|
||||
protected static final Logger LOG = Log.getLogger(HTTP2Connection.class);
|
||||
|
||||
|
@ -176,6 +177,13 @@ public class HTTP2Connection extends AbstractConnection
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFlushed(long bytes) throws IOException
|
||||
{
|
||||
// TODO: add method to ISession ?
|
||||
((HTTP2Session)session).onFlushed(bytes);
|
||||
}
|
||||
|
||||
protected class HTTP2Producer implements ExecutionStrategy.Producer
|
||||
{
|
||||
private final Callback fillableCallback = new FillableCallback();
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.eclipse.jetty.http2.frames.Frame;
|
|||
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.io.WriteFlusher;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IteratingCallback;
|
||||
import org.eclipse.jetty.util.component.ContainerLifeCycle;
|
||||
|
@ -175,7 +176,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
|||
{
|
||||
if (entry.generate(lease))
|
||||
{
|
||||
if (entry.dataRemaining() > 0)
|
||||
if (entry.getDataBytesRemaining() > 0)
|
||||
entries.offer(entry);
|
||||
}
|
||||
else
|
||||
|
@ -207,6 +208,31 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
|||
return Action.SCHEDULED;
|
||||
}
|
||||
|
||||
void onFlushed(long bytes) throws IOException
|
||||
{
|
||||
// For the given flushed bytes, we want to only
|
||||
// forward those that belong to data frame content.
|
||||
for (Entry entry : actives)
|
||||
{
|
||||
int frameBytesLeft = entry.getFrameBytesRemaining();
|
||||
if (frameBytesLeft > 0)
|
||||
{
|
||||
int update = (int)Math.min(bytes, frameBytesLeft);
|
||||
entry.onFrameBytesFlushed(update);
|
||||
bytes -= update;
|
||||
IStream stream = entry.stream;
|
||||
if (stream != null && !entry.isControl())
|
||||
{
|
||||
Object channel = stream.getAttribute(IStream.CHANNEL_ATTRIBUTE);
|
||||
if (channel instanceof WriteFlusher.Listener)
|
||||
((WriteFlusher.Listener)channel).onFlushed(update - Frame.HEADER_LENGTH);
|
||||
}
|
||||
if (bytes == 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded()
|
||||
{
|
||||
|
@ -234,13 +260,13 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
|||
for (int i = index; i < actives.size(); ++i)
|
||||
{
|
||||
Entry entry = actives.get(i);
|
||||
if (entry.dataRemaining() > 0)
|
||||
if (entry.getDataBytesRemaining() > 0)
|
||||
append(entry);
|
||||
}
|
||||
for (int i = 0; i < index; ++i)
|
||||
{
|
||||
Entry entry = actives.get(i);
|
||||
if (entry.dataRemaining() > 0)
|
||||
if (entry.getDataBytesRemaining() > 0)
|
||||
append(entry);
|
||||
}
|
||||
stalled = null;
|
||||
|
@ -333,7 +359,11 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
|||
this.stream = stream;
|
||||
}
|
||||
|
||||
public int dataRemaining()
|
||||
public abstract int getFrameBytesRemaining();
|
||||
|
||||
public abstract void onFrameBytesFlushed(int bytesFlushed);
|
||||
|
||||
public int getDataBytesRemaining()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
@ -387,6 +417,17 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isControl()
|
||||
{
|
||||
switch (frame.getType())
|
||||
{
|
||||
case DATA:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
|
@ -955,6 +955,11 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
{
|
||||
}
|
||||
|
||||
void onFlushed(long bytes) throws IOException
|
||||
{
|
||||
flusher.onFlushed(bytes);
|
||||
}
|
||||
|
||||
public void disconnect()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -1132,15 +1137,28 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
private class ControlEntry extends HTTP2Flusher.Entry
|
||||
{
|
||||
private int bytes;
|
||||
private int frameBytes;
|
||||
|
||||
private ControlEntry(Frame frame, IStream stream, Callback callback)
|
||||
{
|
||||
super(frame, stream, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFrameBytesRemaining()
|
||||
{
|
||||
return frameBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFrameBytesFlushed(int bytesFlushed)
|
||||
{
|
||||
frameBytes -= bytesFlushed;
|
||||
}
|
||||
|
||||
protected boolean generate(ByteBufferPool.Lease lease)
|
||||
{
|
||||
bytes = generator.control(lease, frame);
|
||||
bytes = frameBytes = generator.control(lease, frame);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Generated {}", frame);
|
||||
prepare();
|
||||
|
@ -1238,7 +1256,8 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
private class DataEntry extends HTTP2Flusher.Entry
|
||||
{
|
||||
private int bytes;
|
||||
private int dataRemaining;
|
||||
private int frameBytes;
|
||||
private int dataBytes;
|
||||
private int dataWritten;
|
||||
|
||||
private DataEntry(DataFrame frame, IStream stream, Callback callback)
|
||||
|
@ -1249,35 +1268,47 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
// of data frames that cannot be completely written due to
|
||||
// the flow control window exhausting, since in that case
|
||||
// we would have to count the padding only once.
|
||||
dataRemaining = frame.remaining();
|
||||
dataBytes = frame.remaining();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int dataRemaining()
|
||||
public int getFrameBytesRemaining()
|
||||
{
|
||||
return dataRemaining;
|
||||
return frameBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFrameBytesFlushed(int bytesFlushed)
|
||||
{
|
||||
frameBytes -= bytesFlushed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDataBytesRemaining()
|
||||
{
|
||||
return dataBytes;
|
||||
}
|
||||
|
||||
protected boolean generate(ByteBufferPool.Lease lease)
|
||||
{
|
||||
int dataRemaining = dataRemaining();
|
||||
int dataBytes = getDataBytesRemaining();
|
||||
|
||||
int sessionSendWindow = getSendWindow();
|
||||
int streamSendWindow = stream.updateSendWindow(0);
|
||||
int window = Math.min(streamSendWindow, sessionSendWindow);
|
||||
if (window <= 0 && dataRemaining > 0)
|
||||
if (window <= 0 && dataBytes > 0)
|
||||
return false;
|
||||
|
||||
int length = Math.min(dataRemaining, window);
|
||||
int length = Math.min(dataBytes, window);
|
||||
|
||||
// Only one DATA frame is generated.
|
||||
bytes = generator.data(lease, (DataFrame)frame, length);
|
||||
bytes = frameBytes = generator.data(lease, (DataFrame)frame, length);
|
||||
int written = bytes - Frame.HEADER_LENGTH;
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Generated {}, length/window/data={}/{}/{}", frame, written, window, dataRemaining);
|
||||
LOG.debug("Generated {}, length/window/data={}/{}/{}", frame, written, window, dataBytes);
|
||||
|
||||
this.dataWritten = written;
|
||||
this.dataRemaining -= written;
|
||||
this.dataBytes -= written;
|
||||
|
||||
flowControl.onDataSending(stream, written);
|
||||
|
||||
|
@ -1292,7 +1323,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
|
|||
|
||||
// Do we have more to send ?
|
||||
DataFrame dataFrame = (DataFrame)frame;
|
||||
if (dataRemaining() == 0)
|
||||
if (getDataBytesRemaining() == 0)
|
||||
{
|
||||
// Only now we can update the close state
|
||||
// and eventually remove the stream.
|
||||
|
|
|
@ -62,6 +62,11 @@ public class HttpClientTransportOverHTTP2 extends AbstractHttpClientTransport
|
|||
});
|
||||
}
|
||||
|
||||
public HTTP2Client getHTTP2Client()
|
||||
{
|
||||
return client;
|
||||
}
|
||||
|
||||
@ManagedAttribute(value = "The number of selectors", readonly = true)
|
||||
public int getSelectors()
|
||||
{
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.eclipse.jetty.http2.frames.DataFrame;
|
|||
import org.eclipse.jetty.http2.frames.HeadersFrame;
|
||||
import org.eclipse.jetty.io.ByteBufferPool;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.WriteFlusher;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
|
@ -48,7 +49,7 @@ import org.eclipse.jetty.util.Callback;
|
|||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable
|
||||
public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, WriteFlusher.Listener
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(HttpChannelOverHTTP2.class);
|
||||
private static final HttpField SERVER_VERSION = new PreEncodedHttpField(HttpHeader.SERVER, HttpConfiguration.SERVER_VERSION);
|
||||
|
@ -85,6 +86,12 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable
|
|||
return getStream().getIdleTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFlushed(long bytes) throws IOException
|
||||
{
|
||||
getResponse().getHttpOutput().onFlushed(bytes);
|
||||
}
|
||||
|
||||
public Runnable onRequest(HeadersFrame frame)
|
||||
{
|
||||
try
|
||||
|
|
|
@ -35,7 +35,6 @@ import org.eclipse.jetty.util.log.Logger;
|
|||
import org.eclipse.jetty.util.thread.Invocable;
|
||||
import org.eclipse.jetty.util.thread.Invocable.InvocationType;
|
||||
|
||||
|
||||
/**
|
||||
* A Utility class to help implement {@link EndPoint#write(Callback, ByteBuffer...)} by calling
|
||||
* {@link EndPoint#flush(ByteBuffer...)} until all content is written.
|
||||
|
@ -104,6 +103,7 @@ abstract public class WriteFlusher
|
|||
|
||||
/**
|
||||
* Tries to update the current state to the given new state.
|
||||
*
|
||||
* @param previous the expected current state
|
||||
* @param next the desired new state
|
||||
* @return the previous state or null if the state transition failed
|
||||
|
@ -209,6 +209,7 @@ abstract public class WriteFlusher
|
|||
private static class FailedState extends State
|
||||
{
|
||||
private final Throwable _cause;
|
||||
|
||||
private FailedState(Throwable cause)
|
||||
{
|
||||
super(StateType.FAILED);
|
||||
|
@ -300,7 +301,7 @@ abstract public class WriteFlusher
|
|||
* Tries to switch state to WRITING. If successful it writes the given buffers to the EndPoint. If state transition
|
||||
* fails it'll fail the callback.
|
||||
*
|
||||
* If not all buffers can be written in one go it creates a new <code>PendingState</code> object to preserve the state
|
||||
* If not all buffers can be written in one go it creates a new {@code PendingState} object to preserve the state
|
||||
* and then calls {@link #onIncompleteFlush()}. The remaining buffers will be written in {@link #completeWrite()}.
|
||||
*
|
||||
* If all buffers have been written it calls callback.complete().
|
||||
|
@ -354,7 +355,6 @@ abstract public class WriteFlusher
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Complete a write that has not completed and that called {@link #onIncompleteFlush()} to request a call to this
|
||||
* method when a call to {@link EndPoint#flush(ByteBuffer...)} is likely to be able to progress.
|
||||
|
@ -425,33 +425,47 @@ abstract public class WriteFlusher
|
|||
boolean progress = true;
|
||||
while (progress && buffers != null)
|
||||
{
|
||||
int before=buffers.length==0?0:buffers[0].remaining();
|
||||
long before = remaining(buffers);
|
||||
boolean flushed = _endPoint.flush(buffers);
|
||||
int r=buffers.length==0?0:buffers[0].remaining();
|
||||
long after = remaining(buffers);
|
||||
long written = before - after;
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Flushed={} {}/{}+{} {}",flushed,before-r,before,buffers.length-1,this);
|
||||
LOG.debug("Flushed={} written={} remaining={} {}", flushed, written, after, this);
|
||||
|
||||
if (written > 0)
|
||||
{
|
||||
Connection connection = _endPoint.getConnection();
|
||||
if (connection instanceof Listener)
|
||||
((Listener)connection).onFlushed(written);
|
||||
}
|
||||
|
||||
if (flushed)
|
||||
return null;
|
||||
|
||||
progress=before!=r;
|
||||
progress = written > 0;
|
||||
|
||||
int not_empty=0;
|
||||
while(r==0)
|
||||
int index = 0;
|
||||
while (true)
|
||||
{
|
||||
if (++not_empty==buffers.length)
|
||||
if (index == buffers.length)
|
||||
{
|
||||
// All buffers consumed.
|
||||
buffers = null;
|
||||
not_empty=0;
|
||||
index = 0;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
int remaining = buffers[index].remaining();
|
||||
if (remaining > 0)
|
||||
break;
|
||||
++index;
|
||||
progress = true;
|
||||
r=buffers[not_empty].remaining();
|
||||
}
|
||||
|
||||
if (not_empty>0)
|
||||
buffers=Arrays.copyOfRange(buffers,not_empty,buffers.length);
|
||||
}
|
||||
if (index > 0)
|
||||
buffers = Arrays.copyOfRange(buffers, index, buffers.length);
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
|
@ -463,8 +477,19 @@ abstract public class WriteFlusher
|
|||
return buffers == null ? EMPTY_BUFFERS : buffers;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** Notify the flusher of a failure
|
||||
private long remaining(ByteBuffer[] buffers)
|
||||
{
|
||||
if (buffers == null)
|
||||
return 0;
|
||||
long result = 0;
|
||||
for (ByteBuffer buffer : buffers)
|
||||
result += buffer.remaining();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the flusher of a failure
|
||||
*
|
||||
* @param cause The cause of the failure
|
||||
* @return true if the flusher passed the failure to a {@link Callback} instance
|
||||
*/
|
||||
|
@ -512,26 +537,6 @@ abstract public class WriteFlusher
|
|||
return _state.get().getType() == StateType.IDLE;
|
||||
}
|
||||
|
||||
public boolean isInProgress()
|
||||
{
|
||||
switch(_state.get().getType())
|
||||
{
|
||||
case WRITING:
|
||||
case PENDING:
|
||||
case COMPLETING:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
State s = _state.get();
|
||||
return String.format("WriteFlusher@%x{%s}->%s", hashCode(), s,s instanceof PendingState?((PendingState)s).getCallback():null);
|
||||
}
|
||||
|
||||
public String toStateString()
|
||||
{
|
||||
switch (_state.get().getType())
|
||||
|
@ -550,4 +555,33 @@ abstract public class WriteFlusher
|
|||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
State s = _state.get();
|
||||
return String.format("WriteFlusher@%x{%s}->%s", hashCode(), s, s instanceof PendingState ? ((PendingState)s).getCallback() : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>A listener of {@link WriteFlusher} events.</p>
|
||||
*/
|
||||
public interface Listener
|
||||
{
|
||||
/**
|
||||
* <p>Invoked when a {@link WriteFlusher} flushed bytes in a non-blocking way,
|
||||
* as part of a - possibly larger - write.</p>
|
||||
* <p>This method may be invoked multiple times, for example when writing a large
|
||||
* buffer: a first flush of bytes, then the connection became TCP congested, and
|
||||
* a subsequent flush of bytes when the connection became writable again.</p>
|
||||
* <p>This method is never invoked concurrently, but may be invoked by different
|
||||
* threads, so implementations may not rely on thread-local variables.</p>
|
||||
* <p>Implementations may throw an {@link IOException} to signal that the write
|
||||
* should fail, for example if the implementation enforces a minimum data rate.</p>
|
||||
*
|
||||
* @param bytes the number of bytes flushed
|
||||
* @throws IOException if the write should fail
|
||||
*/
|
||||
void onFlushed(long bytes) throws IOException;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ public class HttpConfiguration
|
|||
private boolean _persistentConnectionsEnabled = true;
|
||||
private int _maxErrorDispatches = 10;
|
||||
private long _minRequestDataRate;
|
||||
private long _minResponseDataRate;
|
||||
private CookieCompliance _cookieCompliance = CookieCompliance.RFC6265;
|
||||
private boolean _notifyRemoteAsyncErrors = true;
|
||||
|
||||
|
@ -127,6 +128,7 @@ public class HttpConfiguration
|
|||
_persistentConnectionsEnabled=config._persistentConnectionsEnabled;
|
||||
_maxErrorDispatches=config._maxErrorDispatches;
|
||||
_minRequestDataRate=config._minRequestDataRate;
|
||||
_minResponseDataRate=config._minResponseDataRate;
|
||||
_cookieCompliance=config._cookieCompliance;
|
||||
_notifyRemoteAsyncErrors=config._notifyRemoteAsyncErrors;
|
||||
}
|
||||
|
@ -497,6 +499,28 @@ public class HttpConfiguration
|
|||
_minRequestDataRate=bytesPerSecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The minimum response data rate in bytes per second; or <=0 for no limit
|
||||
*/
|
||||
@ManagedAttribute("The minimum response content data rate in bytes per second")
|
||||
public long getMinResponseDataRate()
|
||||
{
|
||||
return _minResponseDataRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Sets an minimum response content data rate.</p>
|
||||
* <p>The value is enforced only approximately - not precisely - due to the fact that
|
||||
* for efficiency reasons buffer writes may be comprised of both response headers and
|
||||
* response content.</p>
|
||||
*
|
||||
* @param bytesPerSecond The minimum response data rate in bytes per second; or <=0 for no limit
|
||||
*/
|
||||
public void setMinResponseDataRate(long bytesPerSecond)
|
||||
{
|
||||
_minResponseDataRate = bytesPerSecond;
|
||||
}
|
||||
|
||||
public CookieCompliance getCookieCompliance()
|
||||
{
|
||||
return _cookieCompliance;
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.eclipse.jetty.io.ByteBufferPool;
|
|||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.io.WriteFlusher;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IteratingCallback;
|
||||
|
@ -49,7 +50,7 @@ import org.eclipse.jetty.util.log.Logger;
|
|||
/**
|
||||
* <p>A {@link Connection} that handles the HTTP protocol.</p>
|
||||
*/
|
||||
public class HttpConnection extends AbstractConnection implements Runnable, HttpTransport, Connection.UpgradeFrom
|
||||
public class HttpConnection extends AbstractConnection implements Runnable, HttpTransport, Connection.UpgradeFrom, WriteFlusher.Listener
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(HttpConnection.class);
|
||||
public static final HttpField CONNECTION_CLOSE = new PreEncodedHttpField(HttpHeader.CONNECTION,HttpHeaderValue.CLOSE.asString());
|
||||
|
@ -195,6 +196,14 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
|
|||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFlushed(long bytes) throws IOException
|
||||
{
|
||||
// Unfortunately cannot distinguish between header and content
|
||||
// bytes, and for content bytes whether they are chunked or not.
|
||||
_channel.getResponse().getHttpOutput().onFlushed(bytes);
|
||||
}
|
||||
|
||||
void releaseRequestBuffer()
|
||||
{
|
||||
if (_requestBuffer != null && !_requestBuffer.hasRemaining())
|
||||
|
|
|
@ -282,7 +282,7 @@ public class HttpInput extends ServletInputStream implements Runnable
|
|||
{
|
||||
long minimum_data = minRequestDataRate * TimeUnit.NANOSECONDS.toMillis(period) / TimeUnit.SECONDS.toMillis(1);
|
||||
if (_contentArrived < minimum_data)
|
||||
throw new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408,String.format("Request data rate < %d B/s",minRequestDataRate));
|
||||
throw new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408,String.format("Request content data rate < %d B/s",minRequestDataRate));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.io.InputStream;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritePendingException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
|
@ -122,12 +123,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
|||
private final HttpChannel _channel;
|
||||
private final SharedBlockingCallback _writeBlocker;
|
||||
private Interceptor _interceptor;
|
||||
|
||||
/**
|
||||
* Bytes written via the write API (excludes bytes written via sendContent). Used to autocommit once content length is written.
|
||||
*/
|
||||
private long _written;
|
||||
|
||||
private long _flushed;
|
||||
private long _firstByteTimeStamp = -1;
|
||||
private ByteBuffer _aggregate;
|
||||
private int _bufferSize;
|
||||
private int _commitSize;
|
||||
|
@ -231,6 +229,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
|||
|
||||
protected void write(ByteBuffer content, boolean complete, Callback callback)
|
||||
{
|
||||
if (_firstByteTimeStamp == -1)
|
||||
{
|
||||
long minDataRate = getHttpChannel().getHttpConfiguration().getMinResponseDataRate();
|
||||
if (minDataRate > 0)
|
||||
_firstByteTimeStamp = System.nanoTime();
|
||||
else
|
||||
_firstByteTimeStamp = Long.MAX_VALUE;
|
||||
}
|
||||
_interceptor.write(content, complete, callback);
|
||||
}
|
||||
|
||||
|
@ -908,6 +914,30 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
|||
_commitSize = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Invoked when bytes have been flushed to the network.</p>
|
||||
* <p>The number of flushed bytes may be different from the bytes written
|
||||
* by the application if an {@link Interceptor} changed them, for example
|
||||
* by compressing them.</p>
|
||||
*
|
||||
* @param bytes the number of bytes flushed
|
||||
* @throws IOException if the minimum data rate, when set, is not respected
|
||||
* @see org.eclipse.jetty.io.WriteFlusher.Listener
|
||||
*/
|
||||
public void onFlushed(long bytes) throws IOException
|
||||
{
|
||||
if (_firstByteTimeStamp == -1 || _firstByteTimeStamp == Long.MAX_VALUE)
|
||||
return;
|
||||
long minDataRate = getHttpChannel().getHttpConfiguration().getMinResponseDataRate();
|
||||
_flushed += bytes;
|
||||
long elapsed = System.nanoTime() - _firstByteTimeStamp;
|
||||
long minFlushed = minDataRate * TimeUnit.NANOSECONDS.toMillis(elapsed) / TimeUnit.SECONDS.toMillis(1);
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Flushed bytes min/actual {}/{}", minFlushed, _flushed);
|
||||
if (_flushed < minFlushed)
|
||||
throw new IOException(String.format("Response content data rate < %d B/s", minDataRate));
|
||||
}
|
||||
|
||||
public void recycle()
|
||||
{
|
||||
_interceptor = _channel;
|
||||
|
@ -920,6 +950,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable
|
|||
_written = 0;
|
||||
_writeListener = null;
|
||||
_onError = null;
|
||||
_firstByteTimeStamp = -1;
|
||||
_flushed = 0;
|
||||
reopen();
|
||||
}
|
||||
|
||||
|
|
|
@ -27,11 +27,16 @@ import javax.servlet.http.HttpServletResponse;
|
|||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
|
||||
public class EmptyServerHandler extends AbstractHandler
|
||||
public class EmptyServerHandler extends AbstractHandler.ErrorDispatchHandler
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
jettyRequest.setHandled(true);
|
||||
service(target, jettyRequest, request, response);
|
||||
}
|
||||
|
||||
protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
baseRequest.setHandled(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ import javax.servlet.http.HttpServlet;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.util.BytesContentProvider;
|
||||
|
@ -497,9 +496,8 @@ public class HttpClientTest extends AbstractTest
|
|||
start(new EmptyServerHandler()
|
||||
{
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
super.handle(target, baseRequest, request, response);
|
||||
response.getWriter().write("Jetty");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -43,6 +43,8 @@ import org.eclipse.jetty.client.util.BufferingResponseListener;
|
|||
import org.eclipse.jetty.client.util.DeferredContentProvider;
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http2.FlowControlStrategy;
|
||||
import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
|
||||
import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
|
@ -50,7 +52,9 @@ import org.eclipse.jetty.server.handler.AbstractHandler;
|
|||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.log.StacklessLogging;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ServerTimeoutsTest extends AbstractTest
|
||||
|
@ -736,6 +740,76 @@ public class ServerTimeoutsTest extends AbstractTest
|
|||
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockingWriteWithMinimumDataRateBelowLimit() throws Exception
|
||||
{
|
||||
// This test needs a large write to stall the server, and a slow reading client.
|
||||
// In HTTP/1.1, when using the loopback interface, the buffers are so large that
|
||||
// it would require a very large write (32 MiB) and a lot of time for this test
|
||||
// to pass. On the first writes, the server fills in the large buffers with a lot
|
||||
// of bytes (about 4 MiB), and so it would take a lot of time for the client to
|
||||
// read those bytes and eventually produce a write rate that will make the server
|
||||
// fail; and the write should be large enough to _not_ complete before the rate
|
||||
// is below the minimum.
|
||||
// In HTTP/2, we force the flow control window to be small, so that the server
|
||||
// stalls almost immediately without having written many bytes, so that the test
|
||||
// completes quickly.
|
||||
Assume.assumeThat(transport, Matchers.isOneOf(Transport.H2, Transport.H2C));
|
||||
|
||||
int bytesPerSecond = 16 * 1024;
|
||||
httpConfig.setMinResponseDataRate(bytesPerSecond);
|
||||
CountDownLatch serverLatch = new CountDownLatch(1);
|
||||
start(new EmptyServerHandler()
|
||||
{
|
||||
@Override
|
||||
protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
||||
{
|
||||
try
|
||||
{
|
||||
ServletOutputStream output = response.getOutputStream();
|
||||
output.write(new byte[8 * 1024 * 1024]);
|
||||
}
|
||||
catch (IOException x)
|
||||
{
|
||||
serverLatch.countDown();
|
||||
}
|
||||
}
|
||||
});
|
||||
((HttpClientTransportOverHTTP2)client.getTransport()).getHTTP2Client().setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
|
||||
|
||||
// Setup the client to read slower than the min data rate.
|
||||
BlockingQueue<Object> objects = new LinkedBlockingQueue<>();
|
||||
CountDownLatch clientLatch = new CountDownLatch(1);
|
||||
client.newRequest(newURI())
|
||||
.onResponseContentAsync((response, content, callback) ->
|
||||
{
|
||||
objects.offer(content.remaining());
|
||||
objects.offer(callback);
|
||||
})
|
||||
.send(result ->
|
||||
{
|
||||
objects.offer(-1);
|
||||
objects.offer(Callback.NOOP);
|
||||
if (result.isFailed())
|
||||
clientLatch.countDown();
|
||||
});
|
||||
|
||||
long readRate = bytesPerSecond / 2;
|
||||
while (true)
|
||||
{
|
||||
int bytes = (Integer)objects.poll(5, TimeUnit.SECONDS);
|
||||
if (bytes < 0)
|
||||
break;
|
||||
long ms = bytes * 1000L / readRate;
|
||||
Thread.sleep(ms);
|
||||
Callback callback = (Callback)objects.poll();
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
Assert.assertTrue(serverLatch.await(15, TimeUnit.SECONDS));
|
||||
Assert.assertTrue(clientLatch.await(15, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
private static class BlockingReadHandler extends AbstractHandler.ErrorDispatchHandler
|
||||
{
|
||||
private final CountDownLatch handlerLatch;
|
||||
|
|
Loading…
Reference in New Issue