Skip to content

Commit 5f23689

Browse files
committed
Issue #6254 - Total timeout not enforced for queued requests.
Various code cleanups. Renamed HttpDestination.TimeoutTask to RequestTimeouts for clarity. Improved javadocs, code comments and logging. Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
1 parent 003c313 commit 5f23689

File tree

3 files changed

+59
-52
lines changed

3 files changed

+59
-52
lines changed

jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java

+32-36
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
6565
private final ProxyConfiguration.Proxy proxy;
6666
private final ClientConnectionFactory connectionFactory;
6767
private final HttpField hostField;
68-
private final TimeoutTask timeout;
68+
private final RequestTimeouts requestTimeouts;
6969
private ConnectionPool connectionPool;
7070

7171
public HttpDestination(HttpClient client, Origin origin)
@@ -78,7 +78,7 @@ public HttpDestination(HttpClient client, Origin origin)
7878
this.requestNotifier = new RequestNotifier(client);
7979
this.responseNotifier = new ResponseNotifier();
8080

81-
this.timeout = new TimeoutTask(client.getScheduler());
81+
this.requestTimeouts = new RequestTimeouts(client.getScheduler());
8282

8383
ProxyConfiguration proxyConfig = client.getProxyConfiguration();
8484
proxy = proxyConfig.match(origin);
@@ -272,7 +272,7 @@ public void send(HttpExchange exchange)
272272
{
273273
long expiresAt = request.getTimeoutAt();
274274
if (expiresAt != -1)
275-
timeout.schedule(expiresAt);
275+
requestTimeouts.schedule(expiresAt);
276276

277277
if (!client.isRunning() && exchanges.remove(exchange))
278278
{
@@ -425,7 +425,7 @@ public void close()
425425
if (LOG.isDebugEnabled())
426426
LOG.debug("Closed {}", this);
427427
connectionPool.close();
428-
timeout.destroy();
428+
requestTimeouts.destroy();
429429
}
430430

431431
public void release(Connection connection)
@@ -547,15 +547,15 @@ public String toString()
547547
}
548548

549549
/**
550-
* This class enforces the total timeout for exchanges that are still in the queue.
551-
* The total timeout for exchanges that are not in the destination queue is enforced
552-
* by {@link HttpChannel}.
550+
* <p>Enforces the total timeout for for exchanges that are still in the queue.</p>
551+
* <p>The total timeout for exchanges that are not in the destination queue
552+
* is enforced in {@link HttpChannel} by {@link TimeoutCompleteListener}.</p>
553553
*/
554-
private class TimeoutTask extends CyclicTimeout
554+
private class RequestTimeouts extends CyclicTimeout
555555
{
556-
private final AtomicLong nextTimeout = new AtomicLong(Long.MAX_VALUE);
556+
private final AtomicLong earliestTimeout = new AtomicLong(Long.MAX_VALUE);
557557

558-
private TimeoutTask(Scheduler scheduler)
558+
private RequestTimeouts(Scheduler scheduler)
559559
{
560560
super(scheduler);
561561
}
@@ -564,14 +564,17 @@ private TimeoutTask(Scheduler scheduler)
564564
public void onTimeoutExpired()
565565
{
566566
if (LOG.isDebugEnabled())
567-
LOG.debug("{} timeout expired", this);
567+
LOG.debug("{} timeouts check", this);
568568

569-
nextTimeout.set(Long.MAX_VALUE);
570569
long now = System.nanoTime();
571-
long nextExpiresAt = Long.MAX_VALUE;
572570

573-
// Check all queued exchanges for those that have expired
574-
// and to determine when the next check must be.
571+
// Reset the earliest timeout so we can expire again.
572+
// A concurrent call to schedule(long) may lose an earliest
573+
// value, but the corresponding exchange is already enqueued
574+
// and will be seen by scanning the exchange queue below.
575+
earliestTimeout.set(Long.MAX_VALUE);
576+
577+
long earliest = Long.MAX_VALUE;
575578
for (HttpExchange exchange : exchanges)
576579
{
577580
HttpRequest request = exchange.getRequest();
@@ -580,34 +583,27 @@ public void onTimeoutExpired()
580583
continue;
581584
if (expiresAt <= now)
582585
request.abort(new TimeoutException("Total timeout " + request.getTimeout() + " ms elapsed"));
583-
else if (expiresAt < nextExpiresAt)
584-
nextExpiresAt = expiresAt;
586+
else if (expiresAt < earliest)
587+
earliest = expiresAt;
585588
}
586589

587-
if (nextExpiresAt < Long.MAX_VALUE && client.isRunning())
588-
schedule(nextExpiresAt);
590+
if (earliest < Long.MAX_VALUE && client.isRunning())
591+
schedule(earliest);
589592
}
590593

591594
private void schedule(long expiresAt)
592595
{
593-
// Schedule a timeout for the soonest any known exchange can expire.
594-
// If subsequently that exchange is removed from the queue, the
595-
// timeout is not cancelled, instead the entire queue is swept
596-
// for expired exchanges and a new timeout is set.
597-
long timeoutAt = nextTimeout.getAndUpdate(e -> Math.min(e, expiresAt));
598-
if (timeoutAt != expiresAt)
596+
// Schedule a timeout for the earliest exchange that may expire.
597+
// When the timeout expires, scan the exchange queue for the next
598+
// earliest exchange that may expire, and reschedule a new timeout.
599+
long earliest = earliestTimeout.getAndUpdate(t -> Math.min(t, expiresAt));
600+
if (expiresAt != earliest)
599601
{
600-
long delay = expiresAt - System.nanoTime();
601-
if (delay <= 0)
602-
{
603-
onTimeoutExpired();
604-
}
605-
else
606-
{
607-
schedule(delay, TimeUnit.NANOSECONDS);
608-
if (LOG.isDebugEnabled())
609-
LOG.debug("{} scheduled timeout in {} ms", this, TimeUnit.NANOSECONDS.toMillis(delay));
610-
}
602+
// A new request expires earlier than previous requests, schedule it.
603+
long delay = Math.max(0, expiresAt - System.nanoTime());
604+
if (LOG.isDebugEnabled())
605+
LOG.debug("{} scheduling timeout in {} ms", this, TimeUnit.NANOSECONDS.toMillis(delay));
606+
schedule(delay, TimeUnit.NANOSECONDS);
611607
}
612608
}
613609
}

jetty-client/src/main/java/org/eclipse/jetty/client/TimeoutCompleteListener.java

+8-15
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class TimeoutCompleteListener extends CyclicTimeout implements Response.C
3434
{
3535
private static final Logger LOG = Log.getLogger(TimeoutCompleteListener.class);
3636

37-
private final AtomicReference<Request> request = new AtomicReference<>();
37+
private final AtomicReference<Request> requestTimeout = new AtomicReference<>();
3838

3939
public TimeoutCompleteListener(Scheduler scheduler)
4040
{
@@ -44,7 +44,7 @@ public TimeoutCompleteListener(Scheduler scheduler)
4444
@Override
4545
public void onTimeoutExpired()
4646
{
47-
Request request = this.request.getAndSet(null);
47+
Request request = requestTimeout.getAndSet(null);
4848
if (LOG.isDebugEnabled())
4949
LOG.debug("Total timeout {} ms elapsed for {} on {}", request.getTimeout(), request, this);
5050
if (request != null)
@@ -54,7 +54,7 @@ public void onTimeoutExpired()
5454
@Override
5555
public void onComplete(Result result)
5656
{
57-
Request request = this.request.getAndSet(null);
57+
Request request = requestTimeout.getAndSet(null);
5858
if (request != null)
5959
{
6060
boolean cancelled = cancel();
@@ -65,19 +65,12 @@ public void onComplete(Result result)
6565

6666
void schedule(HttpRequest request, long timeoutAt)
6767
{
68-
if (this.request.compareAndSet(null, request))
68+
if (requestTimeout.compareAndSet(null, request))
6969
{
70-
long delay = timeoutAt - System.nanoTime();
71-
if (delay <= 0)
72-
{
73-
onTimeoutExpired();
74-
}
75-
else
76-
{
77-
schedule(delay, TimeUnit.NANOSECONDS);
78-
if (LOG.isDebugEnabled())
79-
LOG.debug("Scheduled timeout in {} ms for {} on {}", TimeUnit.NANOSECONDS.toMillis(delay), request, this);
80-
}
70+
long delay = Math.max(0, timeoutAt - System.nanoTime());
71+
if (LOG.isDebugEnabled())
72+
LOG.debug("Scheduling timeout in {} ms for {} on {}", TimeUnit.NANOSECONDS.toMillis(delay), request, this);
73+
schedule(delay, TimeUnit.NANOSECONDS);
8174
}
8275
}
8376
}

jetty-io/src/main/java/org/eclipse/jetty/io/CyclicTimeout.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@
3333
* <p>Subclasses should implement {@link #onTimeoutExpired()}.</p>
3434
* <p>This implementation is optimised assuming that the timeout
3535
* will mostly be cancelled and then reused with a similar value.</p>
36+
* <p>The typical scenario to use this class is when you have events
37+
* that postpone (by re-scheduling), or cancel then re-schedule, a
38+
* timeout for a single entity.
39+
* For example: connection idleness, where for each connection there
40+
* is a CyclicTimeout and a read/write postpones the timeout; when
41+
* the timeout expires, the implementation checks against a timestamp
42+
* if the connection is really idle.
43+
* Another example: HTTP session expiration, where for each HTTP
44+
* session there is a CyclicTimeout and at the beginning of the
45+
* request processing the timeout is canceled (via cancel()), but at
46+
* the end of the request processing the timeout is re-scheduled.</p>
47+
* <p>Another typical scenario is for a parent entity to manage
48+
* the timeouts of many children entities; the timeout is scheduled
49+
* for the child entity that expires the earlier; when the timeout
50+
* expires, the implementation scans the children entities to find
51+
* the expired child entities and to find the next child entity
52+
* that expires the earlier. </p>
3653
* <p>This implementation has a {@link Timeout} holding the time
3754
* at which the scheduled task should fire, and a linked list of
3855
* {@link Wakeup}, each holding the actual scheduled task.</p>
@@ -102,8 +119,9 @@ public boolean schedule(long delay, TimeUnit units)
102119
if (_timeout.compareAndSet(timeout, new Timeout(newTimeoutAt, wakeup)))
103120
{
104121
if (LOG.isDebugEnabled())
105-
LOG.debug("Installed timeout in {} ms, waking up in {} ms",
122+
LOG.debug("Installed timeout in {} ms, {} wake up in {} ms",
106123
units.toMillis(delay),
124+
newWakeup != null ? "new" : "existing",
107125
TimeUnit.NANOSECONDS.toMillis(wakeup._at - now));
108126
break;
109127
}

0 commit comments

Comments
 (0)