org.eclipse.jetty.io.CyclicTimeout Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.io;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.component.Destroyable;
import org.eclipse.jetty.util.thread.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.lang.Long.MAX_VALUE;
/**
* An abstract implementation of a timeout.
* Subclasses should implement {@link #onTimeoutExpired()}.
* This implementation is optimised assuming that the timeout
* will mostly be cancelled and then reused with a similar value.
* The typical scenario to use this class is when you have events
* that postpone (by re-scheduling), or cancel then re-schedule, a
* timeout for a single entity.
* For example: connection idleness, where for each connection there
* is a CyclicTimeout and a read/write postpones the timeout; when
* the timeout expires, the implementation checks against a timestamp
* if the connection is really idle.
* Another example: HTTP session expiration, where for each HTTP
* session there is a CyclicTimeout and at the beginning of the
* request processing the timeout is canceled (via cancel()), but at
* the end of the request processing the timeout is re-scheduled.
* This implementation has a {@link Timeout} holding the time
* at which the scheduled task should fire, and a linked list of
* {@link Wakeup}, each holding the actual scheduled task.
* Calling {@link #schedule(long, TimeUnit)} the first time will
* create a Timeout with an associated Wakeup and submit a task to
* the scheduler.
* Calling {@link #schedule(long, TimeUnit)} again with the same or
* a larger delay will cancel the previous Timeout, but keep the
* previous Wakeup without submitting a new task to the scheduler,
* therefore reducing the pressure on the scheduler and avoid it
* becomes a bottleneck.
* When the Wakeup task fires, it will see that the Timeout is now
* in the future and will attach a new Wakeup with the future time
* to the Timeout, and submit a scheduler task for the new Wakeup.
*
* @see CyclicTimeouts
*/
public abstract class CyclicTimeout implements Destroyable
{
private static final Logger LOG = LoggerFactory.getLogger(CyclicTimeout.class);
private static final Timeout NOT_SET = new Timeout(MAX_VALUE, null);
private static final Scheduler.Task DESTROYED = () -> false;
/* The underlying scheduler to use */
private final Scheduler _scheduler;
/* Reference to the current Timeout and chain of Wakeup */
private final AtomicReference _timeout = new AtomicReference<>(NOT_SET);
/**
* @param scheduler A scheduler used to schedule wakeups
*/
public CyclicTimeout(Scheduler scheduler)
{
_scheduler = scheduler;
}
public Scheduler getScheduler()
{
return _scheduler;
}
/**
* Schedules a timeout, even if already set, cancelled or expired.
* If a timeout is already set, it will be cancelled and replaced
* by the new one.
*
* @param delay The period of time before the timeout expires.
* @param units The unit of time of the period.
* @return true if the timeout was already set.
*/
public boolean schedule(long delay, TimeUnit units)
{
long now = NanoTime.now();
long newTimeoutAt = now + units.toNanos(delay);
Wakeup newWakeup = null;
boolean result;
while (true)
{
Timeout timeout = _timeout.get();
result = timeout._at != MAX_VALUE;
// Is the current wakeup good to use? ie before our timeout time?
Wakeup wakeup = timeout._wakeup;
if (wakeup == null || NanoTime.isBefore(newTimeoutAt, wakeup._at))
// No, we need an earlier wakeup.
wakeup = newWakeup = new Wakeup(newTimeoutAt, wakeup);
if (_timeout.compareAndSet(timeout, new Timeout(newTimeoutAt, wakeup)))
{
if (LOG.isDebugEnabled())
{
LOG.debug("Installed timeout in {} ms, {} wake up in {} ms",
units.toMillis(delay),
newWakeup != null ? "new" : "existing",
NanoTime.millisElapsed(now, wakeup._at));
}
break;
}
}
// If we created a new wakeup, we need to actually schedule it.
// Any wakeup that is created and discarded by the failed CAS will not be
// in the wakeup chain, will not have a scheduler task set and will be GC'd.
if (newWakeup != null)
newWakeup.schedule(now);
return result;
}
/**
* Cancels this CyclicTimeout so that it won't expire.
* After being cancelled, this CyclicTimeout can be scheduled again.
*
* @return true if this CyclicTimeout was scheduled to expire
* @see #destroy()
*/
public boolean cancel()
{
boolean result;
while (true)
{
Timeout timeout = _timeout.get();
result = timeout._at != MAX_VALUE;
Wakeup wakeup = timeout._wakeup;
Timeout newTimeout = wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup);
if (_timeout.compareAndSet(timeout, newTimeout))
break;
}
return result;
}
/**
* Invoked when the timeout expires.
*/
public abstract void onTimeoutExpired();
/**
* Destroys this CyclicTimeout.
* After being destroyed, this CyclicTimeout is not used anymore.
*/
@Override
public void destroy()
{
Timeout timeout = _timeout.getAndSet(NOT_SET);
Wakeup wakeup = timeout == null ? null : timeout._wakeup;
while (wakeup != null)
{
wakeup.destroy();
wakeup = wakeup._next;
}
}
/**
* A timeout time with a link to a Wakeup chain.
*/
private static class Timeout
{
private final long _at;
private final Wakeup _wakeup;
private Timeout(long timeoutAt, Wakeup wakeup)
{
_at = timeoutAt;
_wakeup = wakeup;
}
@Override
public String toString()
{
return String.format("%s@%x:%dms,%s",
getClass().getSimpleName(),
hashCode(),
NanoTime.millisUntil(_at),
_wakeup);
}
}
/**
* A Wakeup chain of real scheduler tasks.
*/
private class Wakeup implements Runnable
{
private final AtomicReference _task = new AtomicReference<>();
private final long _at;
private final Wakeup _next;
private Wakeup(long wakeupAt, Wakeup next)
{
_at = wakeupAt;
_next = next;
}
private void schedule(long now)
{
_task.compareAndSet(null, _scheduler.schedule(this, NanoTime.elapsed(now, _at), TimeUnit.NANOSECONDS));
}
private void destroy()
{
Scheduler.Task task = _task.getAndSet(DESTROYED);
if (task != null)
task.cancel();
}
@Override
public void run()
{
long now = NanoTime.now();
Wakeup newWakeup = null;
boolean hasExpired = false;
while (true)
{
Timeout timeout = _timeout.get();
// We must look for ourselves in the current wakeup list.
// If we find ourselves, then we act and we use our tail for any new
// wakeup list, effectively removing any wakeup before us in the list (and making them no-ops).
// If we don't find ourselves, then a wakeup that should have expired after us has already run
// and removed us from the list, so we become a noop.
Wakeup wakeup = timeout._wakeup;
while (wakeup != null)
{
if (wakeup == this)
break;
// Not us, so look at next wakeup in the list.
wakeup = wakeup._next;
}
if (wakeup == null)
// Not found, we become a noop.
return;
// We are in the wakeup list! So we have to act and we know our
// tail has not expired (else it would have removed us from the list).
// Remove ourselves (and any prior Wakeup) from the wakeup list.
wakeup = wakeup._next;
Timeout newTimeout;
if (NanoTime.isBeforeOrSame(timeout._at, now))
{
// We have timed out!
hasExpired = true;
newTimeout = wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup);
}
else if (timeout._at != MAX_VALUE)
{
// We have not timed out, but we are set to!
// Is the current wakeup good to use? ie before our timeout time?
if (wakeup == null || NanoTime.isBefore(timeout._at, wakeup._at))
// No, we need an earlier wakeup.
wakeup = newWakeup = new Wakeup(timeout._at, wakeup);
newTimeout = new Timeout(timeout._at, wakeup);
}
else
{
// We don't timeout, preserve scheduled chain.
newTimeout = wakeup == null ? NOT_SET : new Timeout(MAX_VALUE, wakeup);
}
// Loop until we succeed in changing state or we are a noop!
if (_timeout.compareAndSet(timeout, newTimeout))
break;
}
// If we created a new wakeup, we need to actually schedule it.
if (newWakeup != null)
newWakeup.schedule(now);
// If we expired, then do the callback.
if (hasExpired)
onTimeoutExpired();
}
@Override
public String toString()
{
return String.format("%s@%x:%dms->%s",
getClass().getSimpleName(),
hashCode(),
_at == MAX_VALUE ? _at : NanoTime.millisUntil(_at),
_next);
}
}
}