Implemented idle timeout functionality for streams.
This commit is contained in:
parent
4dca6a71d3
commit
9d9260e634
|
@ -129,7 +129,7 @@ public class HTTP2Client extends ContainerLifeCycle
|
||||||
{
|
{
|
||||||
Context context = (Context)attachment;
|
Context context = (Context)attachment;
|
||||||
Generator generator = new Generator(byteBufferPool, 4096);
|
Generator generator = new Generator(byteBufferPool, 4096);
|
||||||
HTTP2Session session = new HTTP2ClientSession(endpoint, generator, context.listener, new HTTP2FlowControl(65535));
|
HTTP2Session session = new HTTP2ClientSession(getScheduler(), endpoint, generator, context.listener, new HTTP2FlowControl(65535));
|
||||||
Parser parser = new Parser(byteBufferPool, session, 4096, 8192);
|
Parser parser = new Parser(byteBufferPool, session, 4096, 8192);
|
||||||
return new HTTP2ClientConnection(byteBufferPool, getExecutor(), endpoint, parser, 8192, context.promise, session);
|
return new HTTP2ClientConnection(byteBufferPool, getExecutor(), endpoint, parser, 8192, context.promise, session);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,14 +30,15 @@ import org.eclipse.jetty.io.EndPoint;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.log.Log;
|
import org.eclipse.jetty.util.log.Log;
|
||||||
import org.eclipse.jetty.util.log.Logger;
|
import org.eclipse.jetty.util.log.Logger;
|
||||||
|
import org.eclipse.jetty.util.thread.Scheduler;
|
||||||
|
|
||||||
public class HTTP2ClientSession extends HTTP2Session
|
public class HTTP2ClientSession extends HTTP2Session
|
||||||
{
|
{
|
||||||
private static final Logger LOG = Log.getLogger(HTTP2ClientSession.class);
|
private static final Logger LOG = Log.getLogger(HTTP2ClientSession.class);
|
||||||
|
|
||||||
public HTTP2ClientSession(EndPoint endPoint, Generator generator, Listener listener, FlowControl flowControl)
|
public HTTP2ClientSession(Scheduler scheduler, EndPoint endPoint, Generator generator, Listener listener, FlowControl flowControl)
|
||||||
{
|
{
|
||||||
super(endPoint, generator, listener, flowControl, -1, 1);
|
super(scheduler, endPoint, generator, listener, flowControl, -1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -48,7 +49,7 @@ public class HTTP2ClientSession extends HTTP2Session
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
{
|
{
|
||||||
ResetFrame reset = new ResetFrame(streamId, ErrorCode.STREAM_CLOSED_ERROR);
|
ResetFrame reset = new ResetFrame(streamId, ErrorCode.STREAM_CLOSED_ERROR);
|
||||||
reset(reset, disconnectCallback);
|
reset(reset, disconnectOnFailure());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,8 +19,15 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.http2.client;
|
package org.eclipse.jetty.http2.client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpFields;
|
import org.eclipse.jetty.http.HttpFields;
|
||||||
import org.eclipse.jetty.http.HttpVersion;
|
import org.eclipse.jetty.http.HttpVersion;
|
||||||
|
@ -28,13 +35,18 @@ import org.eclipse.jetty.http.MetaData;
|
||||||
import org.eclipse.jetty.http2.api.Session;
|
import org.eclipse.jetty.http2.api.Session;
|
||||||
import org.eclipse.jetty.http2.api.Stream;
|
import org.eclipse.jetty.http2.api.Stream;
|
||||||
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
|
||||||
|
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||||
import org.eclipse.jetty.http2.frames.GoAwayFrame;
|
import org.eclipse.jetty.http2.frames.GoAwayFrame;
|
||||||
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.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.Promise;
|
import org.eclipse.jetty.util.Promise;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.hamcrest.core.IsInstanceOf.instanceOf;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
public class IdleTimeoutTest extends AbstractTest
|
public class IdleTimeoutTest extends AbstractTest
|
||||||
{
|
{
|
||||||
private final int idleTimeout = 1000;
|
private final int idleTimeout = 1000;
|
||||||
|
@ -250,4 +262,161 @@ public class IdleTimeoutTest extends AbstractTest
|
||||||
Assert.assertFalse(closeLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
|
Assert.assertFalse(closeLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
|
||||||
Assert.assertTrue(replyLatch.await(3 * idleTimeout, TimeUnit.MILLISECONDS));
|
Assert.assertTrue(replyLatch.await(3 * idleTimeout, TimeUnit.MILLISECONDS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClientEnforcingStreamIdleTimeout() throws Exception
|
||||||
|
{
|
||||||
|
final int idleTimeout = 1000;
|
||||||
|
startServer(new HttpServlet()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.sleep(2 * idleTimeout);
|
||||||
|
}
|
||||||
|
catch (InterruptedException x)
|
||||||
|
{
|
||||||
|
throw new RuntimeException(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Session session = newClient(new Session.Listener.Adapter());
|
||||||
|
|
||||||
|
final CountDownLatch dataLatch = new CountDownLatch(1);
|
||||||
|
final CountDownLatch timeoutLatch = new CountDownLatch(1);
|
||||||
|
MetaData.Request metaData = newRequest("GET", new HttpFields());
|
||||||
|
HeadersFrame requestFrame = new HeadersFrame(0, metaData, null, true);
|
||||||
|
session.newStream(requestFrame, new Promise.Adapter<Stream>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void succeeded(Stream stream)
|
||||||
|
{
|
||||||
|
stream.setIdleTimeout(idleTimeout);
|
||||||
|
}
|
||||||
|
}, new Stream.Listener.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onData(Stream stream, DataFrame frame, Callback callback)
|
||||||
|
{
|
||||||
|
dataLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Stream stream, Throwable x)
|
||||||
|
{
|
||||||
|
assertThat(x, instanceOf(TimeoutException.class));
|
||||||
|
timeoutLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.assertTrue(timeoutLatch.await(5, TimeUnit.SECONDS));
|
||||||
|
// We must not receive any DATA frame.
|
||||||
|
Assert.assertFalse(dataLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
|
||||||
|
// Stream must be gone.
|
||||||
|
Assert.assertTrue(session.getStreams().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testServerEnforcingStreamIdleTimeout() throws Exception
|
||||||
|
{
|
||||||
|
final CountDownLatch timeoutLatch = new CountDownLatch(1);
|
||||||
|
startServer(new ServerSessionListener.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
|
||||||
|
{
|
||||||
|
stream.setIdleTimeout(idleTimeout);
|
||||||
|
return new Stream.Listener.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onFailure(Stream stream, Throwable x)
|
||||||
|
{
|
||||||
|
timeoutLatch.countDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final CountDownLatch resetLatch = new CountDownLatch(1);
|
||||||
|
Session session = newClient(new Session.Listener.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onReset(Session session, ResetFrame frame)
|
||||||
|
{
|
||||||
|
resetLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
MetaData.Request metaData = newRequest("GET", new HttpFields());
|
||||||
|
// Stream does not end here, but we won't send any DATA frame.
|
||||||
|
HeadersFrame requestFrame = new HeadersFrame(0, metaData, null, false);
|
||||||
|
session.newStream(requestFrame, new Promise.Adapter<Stream>(), new Stream.Listener.Adapter());
|
||||||
|
|
||||||
|
Assert.assertTrue(timeoutLatch.await(5, TimeUnit.SECONDS));
|
||||||
|
Assert.assertTrue(resetLatch.await(5, TimeUnit.SECONDS));
|
||||||
|
// Stream must be gone.
|
||||||
|
Assert.assertTrue(session.getStreams().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIdleTimeoutIsInterruptedWhenReceiving() throws Exception
|
||||||
|
{
|
||||||
|
final CountDownLatch timeoutLatch = new CountDownLatch(1);
|
||||||
|
startServer(new ServerSessionListener.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
|
||||||
|
{
|
||||||
|
stream.setIdleTimeout(idleTimeout);
|
||||||
|
return new Stream.Listener.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onFailure(Stream stream, Throwable x)
|
||||||
|
{
|
||||||
|
timeoutLatch.countDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Session session = newClient(new Session.Listener.Adapter());
|
||||||
|
MetaData.Request metaData = newRequest("GET", new HttpFields());
|
||||||
|
HeadersFrame requestFrame = new HeadersFrame(0, metaData, null, false);
|
||||||
|
session.newStream(requestFrame, new Promise.Adapter<Stream>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void succeeded(final Stream stream)
|
||||||
|
{
|
||||||
|
sleep(idleTimeout / 2);
|
||||||
|
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), false), new Callback.Adapter()
|
||||||
|
{
|
||||||
|
private int sends;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void succeeded()
|
||||||
|
{
|
||||||
|
sleep(idleTimeout / 2);
|
||||||
|
boolean last = ++sends == 2;
|
||||||
|
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), last), last ? INSTANCE : this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, new Stream.Listener.Adapter());
|
||||||
|
|
||||||
|
Assert.assertFalse(timeoutLatch.await(1, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleep(long value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TimeUnit.MILLISECONDS.sleep(value);
|
||||||
|
}
|
||||||
|
catch (InterruptedException x)
|
||||||
|
{
|
||||||
|
Assert.fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,12 +59,13 @@ import org.eclipse.jetty.util.IteratingCallback;
|
||||||
import org.eclipse.jetty.util.Promise;
|
import org.eclipse.jetty.util.Promise;
|
||||||
import org.eclipse.jetty.util.log.Log;
|
import org.eclipse.jetty.util.log.Log;
|
||||||
import org.eclipse.jetty.util.log.Logger;
|
import org.eclipse.jetty.util.log.Logger;
|
||||||
|
import org.eclipse.jetty.util.thread.Scheduler;
|
||||||
|
|
||||||
public abstract class HTTP2Session implements ISession, Parser.Listener
|
public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
{
|
{
|
||||||
private static final Logger LOG = Log.getLogger(HTTP2Session.class);
|
private static final Logger LOG = Log.getLogger(HTTP2Session.class);
|
||||||
|
|
||||||
protected final Callback disconnectCallback = new Callback.Adapter()
|
private final Callback disconnectOnFailure = new Callback.Adapter()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void failed(Throwable x)
|
public void failed(Throwable x)
|
||||||
|
@ -72,13 +73,13 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final ConcurrentMap<Integer, IStream> streams = new ConcurrentHashMap<>();
|
private final ConcurrentMap<Integer, IStream> streams = new ConcurrentHashMap<>();
|
||||||
private final AtomicInteger streamIds = new AtomicInteger();
|
private final AtomicInteger streamIds = new AtomicInteger();
|
||||||
private final AtomicInteger lastStreamId = new AtomicInteger();
|
private final AtomicInteger lastStreamId = new AtomicInteger();
|
||||||
private final AtomicInteger streamCount = new AtomicInteger();
|
private final AtomicInteger streamCount = new AtomicInteger();
|
||||||
private final AtomicInteger windowSize = new AtomicInteger();
|
private final AtomicInteger windowSize = new AtomicInteger();
|
||||||
private final AtomicBoolean closed = new AtomicBoolean();
|
private final AtomicBoolean closed = new AtomicBoolean();
|
||||||
|
private final Scheduler scheduler;
|
||||||
private final EndPoint endPoint;
|
private final EndPoint endPoint;
|
||||||
private final Generator generator;
|
private final Generator generator;
|
||||||
private final Listener listener;
|
private final Listener listener;
|
||||||
|
@ -86,8 +87,9 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
private final Flusher flusher;
|
private final Flusher flusher;
|
||||||
private volatile int maxStreamCount;
|
private volatile int maxStreamCount;
|
||||||
|
|
||||||
public HTTP2Session(EndPoint endPoint, Generator generator, Listener listener, FlowControl flowControl, int maxStreams, int initialStreamId)
|
public HTTP2Session(Scheduler scheduler, EndPoint endPoint, Generator generator, Listener listener, FlowControl flowControl, int maxStreams, int initialStreamId)
|
||||||
{
|
{
|
||||||
|
this.scheduler = scheduler;
|
||||||
this.endPoint = endPoint;
|
this.endPoint = endPoint;
|
||||||
this.generator = generator;
|
this.generator = generator;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
|
@ -103,16 +105,6 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
return generator;
|
return generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMaxStreamCount()
|
|
||||||
{
|
|
||||||
return maxStreamCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowControl getFlowControl()
|
|
||||||
{
|
|
||||||
return flowControl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onData(final DataFrame frame)
|
public boolean onData(final DataFrame frame)
|
||||||
{
|
{
|
||||||
|
@ -140,7 +132,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ResetFrame resetFrame = new ResetFrame(streamId, ErrorCode.STREAM_CLOSED_ERROR);
|
ResetFrame resetFrame = new ResetFrame(streamId, ErrorCode.STREAM_CLOSED_ERROR);
|
||||||
reset(resetFrame, disconnectCallback);
|
reset(resetFrame, disconnectOnFailure());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,6 +149,18 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
@Override
|
@Override
|
||||||
public boolean onReset(ResetFrame frame)
|
public boolean onReset(ResetFrame frame)
|
||||||
{
|
{
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("Received {}", frame);
|
||||||
|
|
||||||
|
IStream stream = getStream(frame.getStreamId());
|
||||||
|
if (stream != null)
|
||||||
|
stream.process(frame, Callback.Adapter.INSTANCE);
|
||||||
|
|
||||||
|
notifyReset(this, frame);
|
||||||
|
|
||||||
|
if (stream != null)
|
||||||
|
removeStream(stream, false);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +190,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
|
|
||||||
// SPEC: SETTINGS frame MUST be replied.
|
// SPEC: SETTINGS frame MUST be replied.
|
||||||
SettingsFrame reply = new SettingsFrame(Collections.<Integer, Integer>emptyMap(), true);
|
SettingsFrame reply = new SettingsFrame(Collections.<Integer, Integer>emptyMap(), true);
|
||||||
settings(reply, disconnectCallback);
|
settings(reply, disconnectOnFailure());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,7 +213,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
PingFrame reply = new PingFrame(frame.getPayload(), true);
|
PingFrame reply = new PingFrame(frame.getPayload(), true);
|
||||||
control(null, reply, disconnectCallback);
|
control(null, reply, disconnectOnFailure());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -265,7 +269,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionFailure(int error, String reason)
|
public void onConnectionFailure(int error, String reason)
|
||||||
{
|
{
|
||||||
close(error, reason, disconnectCallback);
|
close(error, reason, disconnectOnFailure());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -314,7 +318,15 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
@Override
|
@Override
|
||||||
public void reset(ResetFrame frame, Callback callback)
|
public void reset(ResetFrame frame, Callback callback)
|
||||||
{
|
{
|
||||||
control(null, frame, callback);
|
if (closed.get())
|
||||||
|
{
|
||||||
|
callback.succeeded();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: think about moving reset() to Stream.
|
||||||
|
control(getStream(frame.getStreamId()), frame, callback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -352,7 +364,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
flusher.iterate();
|
flusher.iterate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void disconnect()
|
public void disconnect()
|
||||||
{
|
{
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("Disconnecting");
|
LOG.debug("Disconnecting");
|
||||||
|
@ -365,6 +377,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
int streamId = stream.getId();
|
int streamId = stream.getId();
|
||||||
if (streams.putIfAbsent(streamId, stream) == null)
|
if (streams.putIfAbsent(streamId, stream) == null)
|
||||||
{
|
{
|
||||||
|
stream.setIdleTimeout(endPoint.getIdleTimeout());
|
||||||
flowControl.onNewStream(stream);
|
flowControl.onNewStream(stream);
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("Created local {}", stream);
|
LOG.debug("Created local {}", stream);
|
||||||
|
@ -387,7 +400,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
int maxStreams = maxStreamCount;
|
int maxStreams = maxStreamCount;
|
||||||
if (maxStreams >= 0 && currentStreams >= maxStreams)
|
if (maxStreams >= 0 && currentStreams >= maxStreams)
|
||||||
{
|
{
|
||||||
reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR), disconnectCallback);
|
reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR), disconnectOnFailure());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (streamCount.compareAndSet(currentStreams, currentStreams + 1))
|
if (streamCount.compareAndSet(currentStreams, currentStreams + 1))
|
||||||
|
@ -400,6 +413,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
if (streams.putIfAbsent(streamId, stream) == null)
|
if (streams.putIfAbsent(streamId, stream) == null)
|
||||||
{
|
{
|
||||||
updateLastStreamId(streamId);
|
updateLastStreamId(streamId);
|
||||||
|
stream.setIdleTimeout(endPoint.getIdleTimeout());
|
||||||
flowControl.onNewStream(stream);
|
flowControl.onNewStream(stream);
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("Created remote {}", stream);
|
LOG.debug("Created remote {}", stream);
|
||||||
|
@ -407,14 +421,14 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
close(ErrorCode.PROTOCOL_ERROR, "duplicate_stream", disconnectCallback);
|
close(ErrorCode.PROTOCOL_ERROR, "duplicate_stream", disconnectOnFailure());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IStream newStream(HeadersFrame frame)
|
protected IStream newStream(HeadersFrame frame)
|
||||||
{
|
{
|
||||||
return new HTTP2Stream(this, frame);
|
return new HTTP2Stream(scheduler, this, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void removeStream(IStream stream, boolean local)
|
protected void removeStream(IStream stream, boolean local)
|
||||||
|
@ -461,6 +475,11 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
Atomics.updateMax(lastStreamId, streamId);
|
Atomics.updateMax(lastStreamId, streamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Callback disconnectOnFailure()
|
||||||
|
{
|
||||||
|
return disconnectOnFailure;
|
||||||
|
}
|
||||||
|
|
||||||
protected Stream.Listener notifyNewStream(Stream stream, HeadersFrame frame)
|
protected Stream.Listener notifyNewStream(Stream stream, HeadersFrame frame)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -498,6 +517,18 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void notifyReset(Session session, ResetFrame frame)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
listener.onReset(session, frame);
|
||||||
|
}
|
||||||
|
catch (Throwable x)
|
||||||
|
{
|
||||||
|
LOG.info("Failure while notifying listener " + listener, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void notifyClose(Session session, GoAwayFrame frame)
|
protected void notifyClose(Session session, GoAwayFrame frame)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -755,6 +786,24 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
@Override
|
@Override
|
||||||
public void succeeded()
|
public void succeeded()
|
||||||
{
|
{
|
||||||
|
switch (frame.getType())
|
||||||
|
{
|
||||||
|
case RST_STREAM:
|
||||||
|
{
|
||||||
|
if (stream != null)
|
||||||
|
removeStream(stream, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GO_AWAY:
|
||||||
|
{
|
||||||
|
disconnect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
callback.succeeded();
|
callback.succeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -812,7 +861,7 @@ public abstract class HTTP2Session implements ISession, Parser.Listener
|
||||||
stream.updateClose(dataFrame.isEndStream(), true);
|
stream.updateClose(dataFrame.isEndStream(), true);
|
||||||
if (stream.isClosed())
|
if (stream.isClosed())
|
||||||
removeStream(stream, true);
|
removeStream(stream, true);
|
||||||
super.succeeded();
|
callback.succeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,30 +20,45 @@ package org.eclipse.jetty.http2;
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.http2.api.Stream;
|
||||||
import org.eclipse.jetty.http2.frames.DataFrame;
|
import org.eclipse.jetty.http2.frames.DataFrame;
|
||||||
import org.eclipse.jetty.http2.frames.Frame;
|
import org.eclipse.jetty.http2.frames.Frame;
|
||||||
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.parser.ErrorCode;
|
||||||
|
import org.eclipse.jetty.io.IdleTimeout;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.log.Log;
|
import org.eclipse.jetty.util.log.Log;
|
||||||
import org.eclipse.jetty.util.log.Logger;
|
import org.eclipse.jetty.util.log.Logger;
|
||||||
|
import org.eclipse.jetty.util.thread.Scheduler;
|
||||||
|
|
||||||
public class HTTP2Stream implements IStream
|
public class HTTP2Stream extends IdleTimeout implements IStream
|
||||||
{
|
{
|
||||||
private static final Logger LOG = Log.getLogger(HTTP2Stream.class);
|
private static final Logger LOG = Log.getLogger(HTTP2Stream.class);
|
||||||
|
|
||||||
|
private final Callback disconnectOnFailure = new Callback.Adapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void failed(Throwable x)
|
||||||
|
{
|
||||||
|
session.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
private final AtomicReference<ConcurrentMap<String, Object>> attributes = new AtomicReference<>();
|
private final AtomicReference<ConcurrentMap<String, Object>> attributes = new AtomicReference<>();
|
||||||
private final AtomicReference<CloseState> closeState = new AtomicReference<>(CloseState.NOT_CLOSED);
|
private final AtomicReference<CloseState> closeState = new AtomicReference<>(CloseState.NOT_CLOSED);
|
||||||
private final AtomicInteger windowSize = new AtomicInteger();
|
private final AtomicInteger windowSize = new AtomicInteger();
|
||||||
private final ISession session;
|
private final ISession session;
|
||||||
private final HeadersFrame frame;
|
private final HeadersFrame frame;
|
||||||
private Listener listener;
|
private volatile Listener listener;
|
||||||
private volatile boolean reset = false;
|
private volatile boolean reset = false;
|
||||||
|
|
||||||
public HTTP2Stream(ISession session, HeadersFrame frame)
|
public HTTP2Stream(Scheduler scheduler, ISession session, HeadersFrame frame)
|
||||||
{
|
{
|
||||||
|
super(scheduler);
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.frame = frame;
|
this.frame = frame;
|
||||||
}
|
}
|
||||||
|
@ -102,6 +117,28 @@ public class HTTP2Stream implements IStream
|
||||||
return closeState.get() == CloseState.CLOSED;
|
return closeState.get() == CloseState.CLOSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOpen()
|
||||||
|
{
|
||||||
|
return !isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onIdleExpired(TimeoutException timeout)
|
||||||
|
{
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("Idle timeout {}ms expired on {}", getIdleTimeout(), this);
|
||||||
|
|
||||||
|
// The stream is now gone, we must close it to
|
||||||
|
// avoid that its idle timeout is rescheduled.
|
||||||
|
closeState.set(CloseState.CLOSED);
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
session.reset(new ResetFrame(getId(), ErrorCode.CANCEL_STREAM_ERROR), disconnectOnFailure);
|
||||||
|
|
||||||
|
notifyFailure(this, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
private ConcurrentMap<String, Object> attributes()
|
private ConcurrentMap<String, Object> attributes()
|
||||||
{
|
{
|
||||||
ConcurrentMap<String, Object> map = attributes.get();
|
ConcurrentMap<String, Object> map = attributes.get();
|
||||||
|
@ -131,14 +168,21 @@ public class HTTP2Stream implements IStream
|
||||||
@Override
|
@Override
|
||||||
public boolean process(Frame frame, Callback callback)
|
public boolean process(Frame frame, Callback callback)
|
||||||
{
|
{
|
||||||
|
notIdle();
|
||||||
|
|
||||||
switch (frame.getType())
|
switch (frame.getType())
|
||||||
{
|
{
|
||||||
case DATA:
|
case DATA:
|
||||||
{
|
{
|
||||||
return notifyData((DataFrame)frame, callback);
|
// TODO: handle cases where:
|
||||||
|
// TODO: A) stream already remotely close.
|
||||||
|
// TODO: B) DATA before HEADERS.
|
||||||
|
notifyData(this, (DataFrame)frame, callback);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
case HEADERS:
|
case HEADERS:
|
||||||
{
|
{
|
||||||
|
// TODO: handle case where HEADERS after DATA.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
case RST_STREAM:
|
case RST_STREAM:
|
||||||
|
@ -210,28 +254,41 @@ public class HTTP2Stream implements IStream
|
||||||
return windowSize.getAndAdd(delta);
|
return windowSize.getAndAdd(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean notifyData(DataFrame frame, Callback callback)
|
protected void notifyData(Stream stream, DataFrame frame, Callback callback)
|
||||||
{
|
{
|
||||||
final Listener listener = this.listener;
|
final Listener listener = this.listener;
|
||||||
if (listener == null)
|
if (listener == null)
|
||||||
return false;
|
return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
listener.onData(this, frame, callback);
|
listener.onData(stream, frame, callback);
|
||||||
return false;
|
}
|
||||||
|
catch (Throwable x)
|
||||||
|
{
|
||||||
|
LOG.info("Failure while notifying listener " + listener, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyFailure(Stream stream, Throwable failure)
|
||||||
|
{
|
||||||
|
Listener listener = this.listener;
|
||||||
|
if (listener == null)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
listener.onFailure(stream, failure);
|
||||||
}
|
}
|
||||||
catch (Throwable x)
|
catch (Throwable x)
|
||||||
{
|
{
|
||||||
LOG.info("Failure while notifying listener " + listener, x);
|
LOG.info("Failure while notifying listener " + listener, x);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
return String.format("%s@%x{id=%d,windowSize=%s,%s}", getClass().getSimpleName(),
|
return String.format("%s@%x{id=%d,windowSize=%s,reset=%b,%s}", getClass().getSimpleName(),
|
||||||
hashCode(), getId(), windowSize, closeState);
|
hashCode(), getId(), windowSize, reset, closeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CloseState
|
private enum CloseState
|
||||||
|
|
|
@ -33,4 +33,6 @@ public interface ISession extends Session
|
||||||
public void data(IStream stream, DataFrame frame, Callback callback);
|
public void data(IStream stream, DataFrame frame, Callback callback);
|
||||||
|
|
||||||
public int updateWindowSize(int delta);
|
public int updateWindowSize(int delta);
|
||||||
|
|
||||||
|
public void disconnect();
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,10 @@ public interface Stream
|
||||||
|
|
||||||
public boolean isClosed();
|
public boolean isClosed();
|
||||||
|
|
||||||
|
public long getIdleTimeout();
|
||||||
|
|
||||||
|
public void setIdleTimeout(long idleTimeout);
|
||||||
|
|
||||||
// TODO: see SPDY's Stream
|
// TODO: see SPDY's Stream
|
||||||
|
|
||||||
public interface Listener
|
public interface Listener
|
||||||
|
|
|
@ -81,7 +81,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
|
||||||
ServerSessionListener listener = newSessionListener(connector, endPoint);
|
ServerSessionListener listener = newSessionListener(connector, endPoint);
|
||||||
|
|
||||||
Generator generator = new Generator(connector.getByteBufferPool(), getMaxHeaderTableSize());
|
Generator generator = new Generator(connector.getByteBufferPool(), getMaxHeaderTableSize());
|
||||||
HTTP2ServerSession session = new HTTP2ServerSession(endPoint, generator, listener,
|
HTTP2ServerSession session = new HTTP2ServerSession(connector.getScheduler(), endPoint, generator, listener,
|
||||||
new HTTP2FlowControl(getInitialWindowSize()), getMaxConcurrentStreams());
|
new HTTP2FlowControl(getInitialWindowSize()), getMaxConcurrentStreams());
|
||||||
|
|
||||||
Parser parser = newServerParser(connector.getByteBufferPool(), session);
|
Parser parser = newServerParser(connector.getByteBufferPool(), session);
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.eclipse.jetty.io.EndPoint;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.log.Log;
|
import org.eclipse.jetty.util.log.Log;
|
||||||
import org.eclipse.jetty.util.log.Logger;
|
import org.eclipse.jetty.util.log.Logger;
|
||||||
|
import org.eclipse.jetty.util.thread.Scheduler;
|
||||||
|
|
||||||
public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Listener
|
public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Listener
|
||||||
{
|
{
|
||||||
|
@ -42,9 +43,9 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
|
||||||
|
|
||||||
private final ServerSessionListener listener;
|
private final ServerSessionListener listener;
|
||||||
|
|
||||||
public HTTP2ServerSession(EndPoint endPoint, Generator generator, ServerSessionListener listener, FlowControl flowControl, int maxStreams)
|
public HTTP2ServerSession(Scheduler scheduler, EndPoint endPoint, Generator generator, ServerSessionListener listener, FlowControl flowControl, int maxStreams)
|
||||||
{
|
{
|
||||||
super(endPoint, generator, listener, flowControl, maxStreams, 2);
|
super(scheduler, endPoint, generator, listener, flowControl, maxStreams, 2);
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +57,7 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
|
||||||
if (settings == null)
|
if (settings == null)
|
||||||
settings = Collections.emptyMap();
|
settings = Collections.emptyMap();
|
||||||
SettingsFrame frame = new SettingsFrame(settings, false);
|
SettingsFrame frame = new SettingsFrame(settings, false);
|
||||||
settings(frame, disconnectCallback);
|
settings(frame, disconnectOnFailure());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue