org.eclipse.jetty.io.CyclicTimeouts Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.io;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jetty.util.component.Destroyable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.Scheduler;
/**
* An implementation of a timeout that manages many {@link Expirable expirable} entities whose
* timeouts are mostly cancelled or re-scheduled.
* A typical scenario is for a parent entity to manage the timeouts of many children entities.
* When a new entity is created, call {@link #schedule(Expirable)} with the new entity so that
* this instance can be aware and manage the timeout of the new entity.
* Eventually, this instance wakes up and iterates over the entities provided by {@link #iterator()}.
* During the iteration, each entity:
*
* - may never expire (see {@link Expirable#getExpireNanoTime()}; the entity is ignored
* - may be expired; {@link #onExpired(Expirable)} is called with that entity as parameter
* - may expire at a future time; the iteration records the earliest expiration time among
* all non-expired entities
*
* When the iteration is complete, this instance is re-scheduled with the earliest expiration time
* calculated during the iteration.
*
* @param the {@link Expirable} entity type
* @see CyclicTimeout
*/
public abstract class CyclicTimeouts implements Destroyable
{
private static final Logger LOG = Log.getLogger(CyclicTimeouts.class);
private final AtomicLong earliestTimeout = new AtomicLong(Long.MAX_VALUE);
private final CyclicTimeout cyclicTimeout;
public CyclicTimeouts(Scheduler scheduler)
{
cyclicTimeout = new Timeouts(scheduler);
}
/**
* @return the entities to iterate over when this instance expires
*/
protected abstract Iterator iterator();
/**
* Invoked during the iteration when the given entity is expired.
* This method may be invoked multiple times, and even concurrently,
* for the same expirable entity and therefore the expiration of the
* entity, if any, should be an idempotent action.
*
* @param expirable the entity that is expired
* @return whether the entity should be removed from the iterator via {@link Iterator#remove()}
*/
protected abstract boolean onExpired(T expirable);
private void onTimeoutExpired()
{
if (LOG.isDebugEnabled())
LOG.debug("Timeouts check for {}", this);
long now = System.nanoTime();
long earliest = Long.MAX_VALUE;
// Reset the earliest timeout so we can expire again.
// A concurrent call to schedule(long) may lose an
// earliest value, but the corresponding entity will
// be seen during the iteration below.
earliestTimeout.set(earliest);
Iterator iterator = iterator();
if (iterator == null)
return;
// Scan the entities to abort expired entities
// and to find the entity that expires the earliest.
while (iterator.hasNext())
{
T expirable = iterator.next();
long expiresAt = expirable.getExpireNanoTime();
if (LOG.isDebugEnabled())
LOG.debug("Entity {} expires in {} ms for {}", expirable, TimeUnit.NANOSECONDS.toMillis(expiresAt - now), this);
if (expiresAt == -1)
continue;
if (expiresAt <= now)
{
boolean remove = onExpired(expirable);
if (LOG.isDebugEnabled())
LOG.debug("Entity {} expired, remove={} for {}", expirable, remove, this);
if (remove)
iterator.remove();
continue;
}
earliest = Math.min(earliest, expiresAt);
}
if (earliest < Long.MAX_VALUE)
schedule(earliest);
}
/**
* Manages the timeout of a new entity.
*
* @param expirable the new entity to manage the timeout for
*/
public void schedule(T expirable)
{
long expiresAt = expirable.getExpireNanoTime();
if (expiresAt < Long.MAX_VALUE)
schedule(expiresAt);
}
private void schedule(long expiresAt)
{
// Schedule a timeout for the earliest entity that may expire.
// When the timeout expires, scan the entities for the next
// earliest entity that may expire, and reschedule a new timeout.
long prevEarliest = earliestTimeout.getAndUpdate(t -> Math.min(t, expiresAt));
long expires = expiresAt;
while (expires < prevEarliest)
{
// A new entity expires earlier than previous entities, schedule it.
long delay = Math.max(0, expires - System.nanoTime());
if (LOG.isDebugEnabled())
LOG.debug("Scheduling timeout in {} ms for {}", TimeUnit.NANOSECONDS.toMillis(delay), this);
schedule(cyclicTimeout, delay, TimeUnit.NANOSECONDS);
// If we lost a race and overwrote a schedule() with an earlier time, then that earlier time
// is remembered by earliestTimeout, in which case we will loop and set it again ourselves.
prevEarliest = expires;
expires = earliestTimeout.get();
}
}
@Override
public void destroy()
{
cyclicTimeout.destroy();
}
boolean schedule(CyclicTimeout cyclicTimeout, long delay, TimeUnit unit)
{
return cyclicTimeout.schedule(delay, unit);
}
/**
* An entity that may expire.
*/
public interface Expirable
{
/**
* Returns the expiration time in nanoseconds.
* The value to return must be calculated taking into account {@link System#nanoTime()},
* for example:
* {@code expireNanoTime = System.nanoTime() + timeoutNanos}
* Returning {@link Long#MAX_VALUE} indicates that this entity does not expire.
*
* @return the expiration time in nanoseconds, or {@link Long#MAX_VALUE} if this entity does not expire
*/
public long getExpireNanoTime();
}
private class Timeouts extends CyclicTimeout
{
private Timeouts(Scheduler scheduler)
{
super(scheduler);
}
@Override
public void onTimeoutExpired()
{
CyclicTimeouts.this.onTimeoutExpired();
}
}
}