io.craft.atom.util.schedule.TimingWheel Maven / Gradle / Ivy
package io.craft.atom.util.schedule;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A timing wheel data structures to efficiently implement a timer facility, such as I/O timeout scheduling.
* {@link TimingWheel} creates a new thread whenever it is instantiated and started, so don't create many instances.
*
* The classic usage as follows:
*
using timing-wheel manage any object timeout
*
* // Create a timing-wheel with 60 ticks, and every tick is 1 second.
* private static final TimingWheel TIMING_WHEEL = new TimingWheel(1, 60, TimeUnit.SECONDS);
*
* // Add expiration listener and start the timing-wheel.
* static {
* TIMING_WHEEL.addExpirationListener(new YourExpirationListener());
* TIMING_WHEEL.start();
* }
*
* // Add one element to be timeout approximated after 60 seconds
* TIMING_WHEEL.add(e);
*
* // Anytime you can cancel count down timer for element e like this
* TIMING_WHEEL.remove(e);
*
*
* After expiration occurs, the {@link ExpirationListener} interface will be invoked and the expired object will be
* the argument for callback method {@link ExpirationListener#expired(Object)}
*
* As timing-wheel use map structure internal, so any element added to timing-wheel should implement
* its own equals(o) and hashCode() method.
*
* {@link TimingWheel} is based on George Varghese and Tony Lauck's paper,
* 'Hashed and Hierarchical Timing Wheels: data structures
* to efficiently implement a timer facility'. More comprehensive slides are located here.
*
* @author mindwind
* @version 1.0, Sep 20, 2012
*/
@ToString(of = { "tickDuration", "ticksPerWheel", "currentTickIndex", "wheel", "indicator"})
public class TimingWheel {
private static final Logger LOG = LoggerFactory.getLogger(TimingWheel.class);
private final long tickDuration ;
private final int ticksPerWheel ;
private final ArrayList> wheel ;
private final Map> indicator = new ConcurrentHashMap>() ;
private final AtomicBoolean shutdown = new AtomicBoolean(false) ;
private final ReadWriteLock lock = new ReentrantReadWriteLock() ;
private final CopyOnWriteArrayList> expirationListeners = new CopyOnWriteArrayList>();
private volatile int currentTickIndex = 0 ;
private Thread workerThread ;
// ~ -------------------------------------------------------------------------------------------------------------
/**
* Construct a timing wheel.
*
* @param tickDuration tick duration with specified time unit.
* @param ticksPerWheel
* @param timeUnit
*/
public TimingWheel(int tickDuration, int ticksPerWheel, TimeUnit timeUnit) {
if (timeUnit == null) {
throw new NullPointerException("unit");
}
if (tickDuration <= 0) {
throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
}
if (ticksPerWheel <= 0) {
throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
}
this.wheel = new ArrayList>();
this.tickDuration = TimeUnit.MILLISECONDS.convert(tickDuration, timeUnit);
this.ticksPerWheel = ticksPerWheel + 1;
for (int i = 0; i < this.ticksPerWheel; i++) {
wheel.add(new Slot(i));
}
wheel.trimToSize();
workerThread = new Thread(new TickWorker(), "Timing-Wheel");
}
// ~ -------------------------------------------------------------------------------------------------------------
public void start() {
if (shutdown.get()) {
throw new IllegalStateException("Cannot be started once stopped");
}
if (!workerThread.isAlive()) {
workerThread.start();
}
}
public boolean stop() {
if (!shutdown.compareAndSet(false, true)) {
return false;
}
boolean interrupted = false;
while (workerThread.isAlive()) {
workerThread.interrupt();
try {
workerThread.join(100);
} catch (InterruptedException e) {
interrupted = true;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
return true;
}
public void addExpirationListener(ExpirationListener listener) {
expirationListeners.add(listener);
}
public void removeExpirationListener(ExpirationListener listener) {
expirationListeners.remove(listener);
}
/**
* Add a element to {@link TimingWheel} and start to count down its life-time.
*
* @param e
* @return remain time to be expired in millisecond.
*/
public long add(E e) {
// at any time just only one e(element) in the timing-wheel, all operations(add,remove,put) on this element should be synchronized.
synchronized(e) {
checkAdd(e);
int previousTickIndex = getPreviousTickIndex();
Slot slot = wheel.get(previousTickIndex);
slot.add(e);
indicator.put(e, slot);
return (ticksPerWheel - 1) * tickDuration;
}
}
private void checkAdd(E e) {
Slot slot = indicator.get(e);
if (slot != null) {
slot.remove(e);
}
}
private int getPreviousTickIndex() {
lock.readLock().lock();
try {
int cti = currentTickIndex;
if (cti == 0) {
return ticksPerWheel - 1;
}
return cti - 1;
} finally {
lock.readLock().unlock();
}
}
/**
* Removes the specified element from timing wheel.
*
* @param e
* @return true if this timing wheel contained the specified
* element
*/
public boolean remove(E e) {
synchronized (e) {
Slot slot = indicator.get(e);
if (slot == null) {
return false;
}
indicator.remove(e);
return slot.remove(e) != null;
}
}
private void notifyExpired(int idx) {
Slot slot = wheel.get(idx);
Set elements = slot.elements();
for (E e : elements) {
slot.remove(e);
synchronized (e) {
Slot latestSlot = indicator.get(e);
if (slot.equals(latestSlot)) {
indicator.remove(e);
}
}
for (ExpirationListener listener : expirationListeners) {
listener.expired(e);
}
}
}
/**
* @return the number of elements within timing wheel.
*/
public int size() {
return indicator.size();
}
/**
* @return the elements within timing wheel.
*/
public Set elements() {
return indicator.keySet();
}
// ~ -------------------------------------------------------------------------------------------------------------
private class TickWorker implements Runnable {
private long startTime;
private long tick;
@Override
public void run() {
startTime = System.currentTimeMillis();
tick = 1;
for (int i = 0; !shutdown.get(); i++) {
if (i == wheel.size()) {
i = 0;
}
lock.writeLock().lock();
try {
currentTickIndex = i;
} finally {
lock.writeLock().unlock();
}
notifyExpired(currentTickIndex);
waitForNextTick();
}
}
private void waitForNextTick() {
for (;;) {
long currentTime = System.currentTimeMillis();
long sleepTime = tickDuration * tick - (currentTime - startTime);
LOG.debug("[CRAFT-ATOM-UTIL] Wait for next tick sleep |sleepTime={}|", sleepTime);
if (sleepTime <= 0) {
break;
}
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
return;
}
}
tick++;
}
}
@ToString
@EqualsAndHashCode(of = "id")
private static class Slot {
private int id;
private Map elements = new ConcurrentHashMap();
public Slot(int id) {
this.id = id;
}
public void add(E e) {
elements.put(e, e);
}
public E remove(E e) {
return elements.remove(e);
}
public Set elements() {
return elements.keySet();
}
}
}