All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.craft.atom.util.schedule.TimingWheel Maven / Gradle / Ivy

There is a newer version: 3.1.2
Show newest version
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(); } } }





    © 2015 - 2024 Weber Informatics LLC | Privacy Policy