/** * */ package edu.washington.cs.oneswarm.f2f.network; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.PriorityBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; public class DelayedExecutorService { /* * the time accuracy isn't that great anyway and we use slack that makes it * even worse, the buckets used are delay mod BUCKET_SIZE */ private static final int BUCKET_SIZE = 10; private static final int CHECK_PERIOD = 60 * 1000; protected final static Logger logger; private final static DelayedExecutorService instance; static { logger = Logger.getLogger(DelayedExecutorService.class.getName()); instance = new DelayedExecutorService(); } private final HashMap<Long, DelayedExecutor> fixedDelayExecutors = new HashMap<Long, DelayedExecutor>(); private final VariableDelayExecutor variableDelayExecutor = new VariableDelayExecutor(); private DelayedExecutorService() { Timer executorStopTimer = new Timer("DelayedExecutorCheckTimer", true); executorStopTimer.schedule(new TimerTask() { @Override public void run() { if (variableDelayExecutor.running && variableDelayExecutor.isIdle()) { variableDelayExecutor.stop(); } synchronized (fixedDelayExecutors) { int running = 0; for (Iterator<DelayedExecutor> iterator = fixedDelayExecutors.values() .iterator(); iterator.hasNext();) { DelayedExecutor e = iterator.next(); if (e.running) { if (e.isIdle()) { e.stop(); /* * don't remove it, killing the thread is enough * for now, we could potentially remove it from * the list when we are sure that no overlay * transports will use it anymore */ // iterator.remove(); } else { running++; } } } logger.finer("cleaning up threads, running=" + running + " total=" + fixedDelayExecutors.size()); } } }, CHECK_PERIOD, CHECK_PERIOD); } public DelayedExecutor getFixedDelayExecutor(long delay) { synchronized (fixedDelayExecutors) { long bucket = Math.round(delay / (double) BUCKET_SIZE); DelayedExecutor e = fixedDelayExecutors.get(bucket); if (e == null) { e = new FixedDelayExecutor(bucket * BUCKET_SIZE); fixedDelayExecutors.put(bucket, e); logger.finer("creating fixed delay executor: asked_delay=" + delay + " bucket=" + bucket + " size=" + fixedDelayExecutors.size()); } return e; } } public DelayedExecutor getVariableDelayExecutor() { return variableDelayExecutor; } public static DelayedExecutorService getInstance() { return instance; } public static class DelayedExecutionEntry { final long createdAt; final long executeAt; final long slack; final TimerTask task; public DelayedExecutionEntry(long executeAt, long slack, TimerTask task) { this.task = task; this.executeAt = executeAt; this.slack = slack; this.createdAt = System.nanoTime(); } } public abstract static class DelayedExecutor { protected Thread executorThread; protected volatile long lastExecutionTime = 0; protected final BlockingQueue<DelayedExecutionEntry> queue = createQueue(); protected volatile boolean running = false; protected volatile long sleepingUntil = 0; protected abstract BlockingQueue<DelayedExecutionEntry> createQueue(); public abstract String getDescription(); public boolean isEmpty() { return queue.isEmpty(); } public boolean isIdle() { return System.currentTimeMillis() > lastExecutionTime + CHECK_PERIOD && queue.isEmpty(); } public abstract void queue(List<DelayedExecutionEntry> batch); /** * Queue a task for later execution, if the delay is 0 it will run it * instantly in the urrent thread * * @param delay * @param slack * Allow task to be executed up to slack ms earlier than the * deadline if that avoids a call to Thread.sleep * * @param task */ public void queue(long delay, long slack, TimerTask task) { if (delay <= 0) { task.run(); return; } DelayedExecutionEntry entry = new DelayedExecutionEntry(System.currentTimeMillis() + delay, slack, task); List<DelayedExecutionEntry> batch = new LinkedList<DelayedExecutionEntry>(); batch.add(entry); queue(batch); } public void queue(long delay, TimerTask task) { queue(delay, 0, task); } void start() { synchronized (this) { if (!running) { logger.fine("starting thread: " + getDescription()); running = true; lastExecutionTime = System.currentTimeMillis(); DelayedExecutionRunner r = new DelayedExecutionRunner(); executorThread = new Thread(r); executorThread.setDaemon(true); executorThread.setName(getDescription()); executorThread.start(); } else { logger.warning("start called but thread already running: " + getDescription()); } } } void stop() { synchronized (this) { if (running && queue.peek() == null) { logger.fine("stopping delay executor thread: " + getDescription()); running = false; if (executorThread != null) { executorThread.interrupt(); } } else { logger.warning("stop called but thread is either no running or non-empty: " + getDescription()); } } } private class DelayedExecutionRunner implements Runnable { public void run() { logger.fine("executor thread started"); while (running) { DelayedExecutionEntry entry = null; try { entry = queue.take(); long currentTime = System.currentTimeMillis(); long sleepMs = entry.executeAt - currentTime; /* * sleep at least "slack" ms, if we are to close to the * time run the task anyway even though it can be a bit * early */ if (sleepMs > entry.slack) { if (logger.isLoggable(Level.FINEST)) { logger.finest(getDescription() + ": sleeping " + sleepMs + "ms"); } sleepingUntil = entry.executeAt; Thread.sleep(sleepMs); } else { if (logger.isLoggable(Level.FINEST)) { logger.finest(getDescription() + ": skipping sleep this time, sleepMs=" + sleepMs + ", slack=" + entry.slack); } } } catch (InterruptedException e) { logger.finest(getDescription() + ": executor thread interupted"); if (!running) { logger.finer(getDescription() + ": stopping executor thread"); continue; } // this is expected if we add anything to the head of // the // queue if (entry != null) { logger.finer(getDescription() + ": interrupted, returing current entry to queue"); queue.add(entry); } continue; } long startTime = System.currentTimeMillis(); entry.task.run(); long elapsed = System.currentTimeMillis() - startTime; if (elapsed > 20) { logger.warning(getDescription() + ": took " + elapsed + "ms to run task! (parent=" + getDescription() + ")"); } if (logger.isLoggable(Level.FINEST)) { logger.finest(getDescription() + ": executed task in: " + elapsed + " ms"); } lastExecutionTime = System.currentTimeMillis(); } logger.fine(getDescription() + ":executor thread stopped"); } } } private static class FixedDelayExecutor extends DelayedExecutor { private final long delay; private final String desc; private FixedDelayExecutor(long delay) { this.delay = delay; this.desc = "FixedDelayExecutor:" + delay; } @Override protected BlockingQueue<DelayedExecutionEntry> createQueue() { return new LinkedBlockingQueue<DelayedExecutionEntry>(); } @Override public String getDescription() { return desc; } @Override public void queue(List<DelayedExecutionEntry> batch) { throw new RuntimeException("batch adding not supported"); } @Override public void queue(long delay, long slack, TimerTask task) { if (delay + BUCKET_SIZE < this.delay || delay - BUCKET_SIZE > this.delay) { throw new RuntimeException("delay must be " + this.delay + "+/- " + BUCKET_SIZE); } delay = this.delay; if (delay <= 0) { task.run(); return; } DelayedExecutionEntry entry = new DelayedExecutionEntry(System.currentTimeMillis() + delay, slack, task); if (logger.isLoggable(Level.FINEST)) { logger.finest("queuing task: delay=" + delay + " slack=" + slack); } queue.add(entry); synchronized (this) { if (!running) { start(); } } } } private static class VariableDelayExecutor extends DelayedExecutor { private VariableDelayExecutor() { logger.fine("DelayedExecutor created"); } @Override protected BlockingQueue<DelayedExecutionEntry> createQueue() { return new PriorityBlockingQueue<DelayedExecutionEntry>(10000, new Comparator<DelayedExecutionEntry>() { public int compare(DelayedExecutionEntry o1, DelayedExecutionEntry o2) { if (o1.executeAt < o2.executeAt) { return -1; } else if (o1.executeAt == o2.executeAt) { if (o1.createdAt < o2.createdAt) { return -1; } else if (o1.createdAt == o2.createdAt) { return 0; } else { return 1; } } else { return 1; } } }); } @Override public String getDescription() { return "VariableDelayExecutor"; } public void queue(List<DelayedExecutionEntry> batch) { List<DelayedExecutionEntry> clone = new LinkedList<DelayedExecutionEntry>(batch); long earliestRun = Long.MAX_VALUE; long currentTime = System.currentTimeMillis(); /* * first, check if any of these are expired already, in that case * run then straight up */ for (Iterator<DelayedExecutionEntry> iterator = clone.iterator(); iterator.hasNext();) { DelayedExecutionEntry e = iterator.next(); if (e.executeAt <= currentTime) { e.task.run(); iterator.remove(); } else { if (e.executeAt < earliestRun) { earliestRun = e.executeAt; } } } /* * if we have anything left, add to queue */ if (clone.size() > 0) { synchronized (this) { if (!running) { start(); } } queue.addAll(clone); if (sleepingUntil > earliestRun) { logger.finer("interrupting queue executor thread"); executorThread.interrupt(); } } } } }