package net.i2p.util;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.I2PAppContext;
/**
* Simple event scheduler - toss an event on the queue and it gets fired at the
* appropriate time. The method that is fired however should NOT block (otherwise
* they b0rk the timer).
*
* This rewrites the old SimpleTimer to use the java.util.concurrent.ScheduledThreadPoolExecutor.
* SimpleTimer has problems with lock contention;
* this should work a lot better.
*
* This supports cancelling and arbitrary rescheduling.
* If you don't need that, use SimpleScheduler instead.
*
* SimpleTimer is deprecated, use this or SimpleScheduler.
*
* @author zzz
*/
public class SimpleTimer2 {
/**
* If you have a context, use context.simpleTimer2() instead
*/
public static SimpleTimer2 getInstance() {
return I2PAppContext.getGlobalContext().simpleTimer2();
}
private static final int MIN_THREADS = 2;
private static final int MAX_THREADS = 4;
private final ScheduledThreadPoolExecutor _executor;
private final String _name;
private final AtomicInteger _count = new AtomicInteger();
private final int _threads;
/**
* To be instantiated by the context.
* Others should use context.simpleTimer2() instead
*/
public SimpleTimer2(I2PAppContext context) {
this(context, "SimpleTimer2");
}
/**
* To be instantiated by the context.
* Others should use context.simpleTimer2() instead
*/
protected SimpleTimer2(I2PAppContext context, String name) {
this(context, name, true);
}
/**
* To be instantiated by the context.
* Others should use context.simpleTimer2() instead
* @since 0.9
*/
protected SimpleTimer2(I2PAppContext context, String name, boolean prestartAllThreads) {
_name = name;
long maxMemory = SystemVersion.getMaxMemory();
_threads = (int) Math.max(MIN_THREADS, Math.min(MAX_THREADS, 1 + (maxMemory / (32*1024*1024))));
_executor = new CustomScheduledThreadPoolExecutor(_threads, new CustomThreadFactory());
if (prestartAllThreads)
_executor.prestartAllCoreThreads();
// don't bother saving ref to remove hook if somebody else calls stop
context.addShutdownTask(new Shutdown());
}
/**
* @since 0.8.8
*/
private class Shutdown implements Runnable {
public void run() {
stop();
}
}
/**
* Stops the SimpleTimer.
* Subsequent executions should not throw a RejectedExecutionException.
* Cannot be restarted.
*/
public void stop() {
_executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
_executor.shutdownNow();
}
private static class CustomScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
public CustomScheduledThreadPoolExecutor(int threads, ThreadFactory factory) {
super(threads, factory);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) { // shoudn't happen, caught in RunnableEvent.run()
Log log = I2PAppContext.getGlobalContext().logManager().getLog(SimpleTimer2.class);
log.log(Log.CRIT, "event borked: " + r, t);
}
}
}
private class CustomThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread rv = Executors.defaultThreadFactory().newThread(r);
rv.setName(_name + ' ' + _count.incrementAndGet() + '/' + _threads);
// Uncomment this to test threadgrouping, but we should be all safe now that the constructor preallocates!
// String name = rv.getThreadGroup().getName();
// if(!name.equals("main")) {
// (new Exception("OWCH! DAMN! Wrong ThreadGroup `" + name +"', `" + rv.getName() + "'")).printStackTrace();
// }
rv.setDaemon(true);
rv.setPriority(Thread.NORM_PRIORITY + 1);
return rv;
}
}
private ScheduledFuture<?> schedule(TimedEvent t, long timeoutMs) {
return _executor.schedule(t, timeoutMs, TimeUnit.MILLISECONDS);
}
/**
* Queue up the given event to be fired no sooner than timeoutMs from now.
*
* For transition from SimpleScheduler. Uncancellable.
* New code should use SimpleTimer2.TimedEvent.
*
* @param event to be run once
* @param timeoutMs run after this delay
* @since 0.9.20
*/
public void addEvent(final SimpleTimer.TimedEvent event, final long timeoutMs) {
if (event == null)
throw new IllegalArgumentException("addEvent null");
new TimedEvent(this, timeoutMs) {
@Override
public void timeReached() {
event.timeReached();
}
@Override
public String toString() {
return event.toString();
}
};
}
/**
* Schedule periodic event
*
* The TimedEvent must not do its own rescheduling.
* As all Exceptions are caught in run(), these will not prevent
* subsequent executions (unlike SimpleTimer, where the TimedEvent does
* its own rescheduling).
*
* For transition from SimpleScheduler. Uncancellable.
* New code should use SimpleTimer2.TimedEvent.
*
* @since 0.9.20
* @param timeoutMs run subsequent iterations of this event every timeoutMs ms, 5000 minimum
* @throws IllegalArgumentException if timeoutMs less than 5000
*/
public void addPeriodicEvent(final SimpleTimer.TimedEvent event, final long timeoutMs) {
addPeriodicEvent(event, timeoutMs, timeoutMs);
}
/**
* Schedule periodic event
*
* The TimedEvent must not do its own rescheduling.
* As all Exceptions are caught in run(), these will not prevent
* subsequent executions (unlike SimpleTimer, where the TimedEvent does
* its own rescheduling).
*
* For transition from SimpleScheduler. Uncancellable.
* New code should use SimpleTimer2.TimedEvent.
*
* @since 0.9.20
* @param delay run the first iteration of this event after delay ms
* @param timeoutMs run subsequent iterations of this event every timeoutMs ms, 5000 minimum
* @throws IllegalArgumentException if timeoutMs less than 5000
*/
public void addPeriodicEvent(final SimpleTimer.TimedEvent event, final long delay, final long timeoutMs) {
new PeriodicTimedEvent(this, delay, timeoutMs) {
@Override
public void timeReached() {
event.timeReached();
}
@Override
public String toString() {
return event.toString();
}
};
}
/**
* state of a given TimedEvent
*
* valid transitions:
* {IDLE,CANCELLED,RUNNING} -> SCHEDULED [ -> SCHEDULED ]* -> RUNNING -> {IDLE,CANCELLED,SCHEDULED}
* {IDLE,CANCELLED,RUNNING} -> SCHEDULED [ -> SCHEDULED ]* -> CANCELLED
*
* anything else is invalid.
*/
private enum TimedEventState {
IDLE,
SCHEDULED,
RUNNING,
CANCELLED
};
/**
* Similar to SimpleTimer.TimedEvent but users must extend instead of implement,
* and all schedule and cancel methods are through this class rather than SimpleTimer2.
*
* To convert over, change implements SimpleTimer.TimedEvent to extends SimpleTimer2.TimedEvent,
* and be sure to call super(SimpleTimer2.getInstance(), timeoutMs) in the constructor
* (or super(SimpleTimer2.getInstance()); .... schedule(timeoutMs); if there is other stuff
* in your constructor)
*
* Other porting:
* SimpleTimer.getInstance().addEvent(new foo(), timeout) => new foo(SimpleTimer2.getInstance(), timeout)
* SimpleTimer.getInstance().addEvent(this, timeout) => schedule(timeout)
* SimpleTimer.getInstance().addEvent(foo, timeout) => foo.reschedule(timeout)
* SimpleTimer.getInstance().removeEvent(foo) => foo.cancel()
*
* There's no global locking, but for scheduling, we synchronize on this
* to reduce the chance of duplicates on the queue.
*
* schedule(ms) can get create duplicates
* reschedule(ms) and reschedule(ms, true) can lose the timer
* reschedule(ms, false) and forceReschedule(ms) are relatively safe from either
*
*/
public static abstract class TimedEvent implements Runnable {
private final Log _log;
private final SimpleTimer2 _pool;
private int _fuzz;
protected static final int DEFAULT_FUZZ = 3;
private ScheduledFuture<?> _future; // _executor.remove() doesn't work so we have to use this
// ... and I expect cancelling this way is more efficient
/** state of the current event. All access should be under lock. */
protected TimedEventState _state;
/** absolute time this event should run next time. LOCKING: this */
private long _nextRun;
/** whether this was scheduled during RUNNING state. LOCKING: this */
private boolean _rescheduleAfterRun;
/** whether this was cancelled during RUNNING state. LOCKING: this */
private boolean _cancelAfterRun;
/** must call schedule() later */
public TimedEvent(SimpleTimer2 pool) {
_pool = pool;
_fuzz = DEFAULT_FUZZ;
_log = I2PAppContext.getGlobalContext().logManager().getLog(SimpleTimer2.class);
_state = TimedEventState.IDLE;
}
/** automatically schedules, don't use this one if you have other things to do first */
public TimedEvent(SimpleTimer2 pool, long timeoutMs) {
this(pool);
schedule(timeoutMs);
}
/**
* Don't bother rescheduling if +/- this many ms or less.
* Use this to reduce timer queue and object churn for a sloppy timer like
* an inactivity timer.
* Default 3 ms.
*/
public synchronized void setFuzz(int fuzz) {
_fuzz = fuzz;
}
/**
* Slightly more efficient than reschedule().
* Does nothing if already scheduled.
*/
public synchronized void schedule(long timeoutMs) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Scheduling: " + this + " timeout = " + timeoutMs + " state: " + _state);
if (timeoutMs <= 0) {
// streaming timers do call with timeoutMs == 0
if (timeoutMs < 0 && _log.shouldLog(Log.WARN))
_log.warn("Sched. timeout < 0: " + this + " timeout = " + timeoutMs + " state: " + _state);
timeoutMs = 1; // otherwise we may execute before _future is updated, which is fine
// except it triggers 'early execution' warning logging
}
// always set absolute time of execution
_nextRun = timeoutMs + System.currentTimeMillis();
_cancelAfterRun = false;
switch(_state) {
case RUNNING:
_rescheduleAfterRun = true; // signal that we need rescheduling.
break;
case IDLE: // fall through
case CANCELLED:
_future = _pool.schedule(this, timeoutMs);
_state = TimedEventState.SCHEDULED;
break;
case SCHEDULED: // nothing
}
}
/**
* Use the earliest of the new time and the old time
* May be called from within timeReached(), but schedule() is
* better there.
*
* @param timeoutMs
*/
public void reschedule(long timeoutMs) {
reschedule(timeoutMs, true);
}
/**
* May be called from within timeReached(), but schedule() is
* better there.
*
* @param timeoutMs
* @param useEarliestTime if its already scheduled, use the earlier of the
* two timeouts, else use the later
*/
public synchronized void reschedule(long timeoutMs, boolean useEarliestTime) {
if (timeoutMs <= 0) {
if (timeoutMs < 0 && _log.shouldInfo())
_log.info("Resched. timeout < 0: " + this + " timeout = " + timeoutMs + " state: " + _state);
timeoutMs = 1;
}
final long now = System.currentTimeMillis();
long oldTimeout;
boolean scheduled = _state == TimedEventState.SCHEDULED;
if (scheduled)
oldTimeout = _nextRun - now;
else
oldTimeout = timeoutMs;
// don't bother rescheduling if within _fuzz ms
if ((oldTimeout - _fuzz > timeoutMs && useEarliestTime) ||
(oldTimeout + _fuzz < timeoutMs && !useEarliestTime)||
(!scheduled)) {
if (scheduled && oldTimeout <= 5) {
// don't reschedule to avoid race
if (_log.shouldWarn())
_log.warn("not rescheduling to " + timeoutMs + ", about to execute " + this + " in " + oldTimeout);
return;
}
if (scheduled && (now + timeoutMs) < _nextRun) {
if (_log.shouldLog(Log.INFO))
_log.info("Re-scheduling: " + this + " timeout = " + timeoutMs + " old timeout was " + oldTimeout + " state: " + _state);
cancel();
}
schedule(timeoutMs);
}
}
/**
* Always use the new time - ignores fuzz
* @param timeoutMs
*/
public synchronized void forceReschedule(long timeoutMs) {
// don't cancel while running!
if (_state == TimedEventState.SCHEDULED)
cancel();
schedule(timeoutMs);
}
/** @return true if cancelled */
public synchronized boolean cancel() {
// always clear
_rescheduleAfterRun = false;
switch(_state) {
case CANCELLED: // fall through
case IDLE:
break; // my preference is to throw IllegalState here, but let it be.
case RUNNING:
_cancelAfterRun = true;
return true;
case SCHEDULED:
// There's probably a race here, where it's cancelled after it's running
// The result (if rescheduled) is a dup on the queue, see tickets 1694, 1705
// Mitigated by close-to-execution check in reschedule()
boolean cancelled = _future.cancel(true);
if (cancelled)
_state = TimedEventState.CANCELLED;
else
_log.error("could not cancel " + this + " to run in " + (_nextRun - System.currentTimeMillis()), new Exception());
return cancelled;
}
return false;
}
public void run() {
try {
run2();
} catch (RuntimeException re) {
_log.error("timer error", re);
throw re;
}
}
private void run2() {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Running: " + this);
long before = System.currentTimeMillis();
long delay = 0;
synchronized(this) {
if (Thread.currentThread().isInterrupted()) {
if (_log.shouldWarn())
_log.warn("I was interrupted in run, state "+_state+" event "+this);
return;
}
if (_rescheduleAfterRun)
throw new IllegalStateException(this + " rescheduleAfterRun cannot be true here");
switch(_state) {
case CANCELLED:
if (_log.shouldInfo())
_log.info("Not actually running: CANCELLED " + this);
return; // goodbye
case IDLE: // fall through
case RUNNING:
throw new IllegalStateException(this + " not possible to be in " + _state);
case SCHEDULED:
// proceed, will switch to IDLE to reschedule
}
// if I was rescheduled by the user, re-submit myself to the executor.
long difference = _nextRun - before; // careful with long uptimes
if (difference > _fuzz) {
// proceed, switch to IDLE to reschedule
_state = TimedEventState.IDLE;
if (_log.shouldInfo())
_log.info("Early execution, Rescheduling for " + difference + " later: " + this);
schedule(difference);
return;
}
// else proceed to run
_state = TimedEventState.RUNNING;
}
// cancel()-ing after this point only works if the event supports it explicitly
// none of these _future checks should be necessary anymore
if (_future != null)
delay = _future.getDelay(TimeUnit.MILLISECONDS);
else if (_log.shouldLog(Log.WARN))
_log.warn(_pool + " no _future " + this);
// This can be an incorrect warning especially after a schedule(0)
if (_log.shouldWarn()) {
if (delay > 100)
_log.warn(_pool + " early execution " + delay + ": " + this);
else if (delay < -1000)
_log.warn(" late execution " + (0 - delay) + ": " + this + _pool.debug());
}
try {
timeReached();
} catch (Throwable t) {
_log.log(Log.CRIT, _pool + ": Timed task " + this + " exited unexpectedly, please report", t);
} finally { // must be in finally
synchronized(this) {
switch(_state) {
case SCHEDULED: // fall through
case IDLE:
throw new IllegalStateException(this + " can't be " + _state);
case CANCELLED:
break; // nothing
case RUNNING:
if (_cancelAfterRun) {
_cancelAfterRun = false;
_state = TimedEventState.CANCELLED;
} else {
_state = TimedEventState.IDLE;
// do we need to reschedule?
if (_rescheduleAfterRun) {
_rescheduleAfterRun = false;
if (_log.shouldInfo())
_log.info("Reschedule after run: " + this);
schedule(_nextRun - System.currentTimeMillis());
}
}
}
}
}
long time = System.currentTimeMillis() - before;
if (time > 500 && _log.shouldLog(Log.WARN))
_log.warn(_pool + " event execution took " + time + ": " + this);
else if (_log.shouldDebug())
_log.debug("Execution finished in " + time + ": " + this);
if (_log.shouldLog(Log.INFO)) {
// this call is slow - iterates through a HashMap -
// would be better to have a local AtomicLong if we care
long completed = _pool.getCompletedTaskCount();
if (completed % 250 == 0)
_log.info(_pool.debug());
}
}
/**
* Simple interface for events to be queued up and notified on expiration
* the time requested has been reached (this call should NOT block,
* otherwise the whole SimpleTimer gets backed up)
*
*/
public abstract void timeReached();
}
@Override
public String toString() {
return _name;
}
/** warning - slow */
private long getCompletedTaskCount() {
return _executor.getCompletedTaskCount();
}
/** warning - slow */
private String debug() {
_executor.purge(); // Remove cancelled tasks from the queue so we get a good queue size stat
return
" Pool: " + _name +
" Active: " + _executor.getActiveCount() + '/' + _executor.getPoolSize() +
" Completed: " + _executor.getCompletedTaskCount() +
" Queued: " + _executor.getQueue().size();
}
/**
* For transition from SimpleScheduler.
* @since 0.9.20
*/
private static abstract class PeriodicTimedEvent extends TimedEvent {
private final long _timeoutMs;
/**
* Schedule periodic event
*
* @param delay run the first iteration of this event after delay ms
* @param timeoutMs run subsequent iterations of this event every timeoutMs ms, 5000 minimum
* @throws IllegalArgumentException if timeoutMs less than 5000
*/
public PeriodicTimedEvent(SimpleTimer2 pool, long delay, long timeoutMs) {
super(pool, delay);
if (timeoutMs < 5000)
throw new IllegalArgumentException("timeout minimum 5000");
_timeoutMs = timeoutMs;
}
@Override
public void run() {
super.run();
synchronized(this) {
// Task may have rescheduled itself without actually running.
// If we schedule again, it will be stuck in a scheduling loop.
// This happens after a backwards clock shift.
if (_state == TimedEventState.IDLE)
schedule(_timeoutMs);
}
}
}
}