ScheduledThreadPoolExecutor. Replaces the legacy SimpleTimer (deleted; had lock contention issues). Supports cancel and reschedule. Events must NOT block. All timer events should extend TimedEvent and use schedule(SimpleTimer2.TimedEvent, long)/cancel() directly.
A saturation watchdog samples this pool from a thread of its own and warns when every worker has stayed occupied with a backlog behind it: a ScheduledThreadPoolExecutor never grows past DEFAULT_THREADS, so two blocked events starve every periodic event on this timer without throwing anything or writing anything to the log.
Nested Class Summary
Modifier and TypeClassDescriptionprivate static classExecutor that rejects work after shutdown.private classFactory naming the timer threads.private classSamples this timer on its own daemon thread and reports a pool that has stopped keeping up.static classBase class for timer events.private static enumState of a given TimedEvent.(package private) static enumWhat a single saturation-watchdog sample means.private static classThread factory for the saturation watchdog.Field Summary
FieldsModifier and TypeFieldDescriptionprivate final AtomicLongEvents completed since startup.private final I2PAppContextThe app context, for the clock and stats.private final AtomicIntegerCounter numbering the timer threads.private intNext slot in_recentDelayMs.private final ScheduledThreadPoolExecutorThe pool running the scheduled events.private final AtomicLongSchedules requested with a zero or negative delay since the last watchdog report.private final StringName prefix for the timer threads.private final RunnableHook stopping the timer on JVM exit.private final long[]Recent execution delays, newest overwriting oldest.private intHow many workers this timer runs.private final SimpleTimer2.SaturationWatchdogWatchdog watching for a saturated timer.(package private) static final intDefault worker count.private static final intHow many execution delays are kept for the backlog reading.private static final intCeiling on automatic worker growth.private static final intThe floor on worker count.(package private) static final intHow often the saturation watchdog samples this pool.(package private) static final intQueue depth that turns full occupancy into a backlog rather than a coincidence.(package private) static final intConsecutive samples that must show progress before a quiet episode is called over.(package private) static final intConsecutive saturated samples tolerated before a WARN.(package private) static final intConsecutive samples with no completed task before a WARN.Constructor Summary
ConstructorsModifierConstructorDescriptionSimpleTimer2(I2PAppContext context) To be instantiated by the context.SimpleTimer2(I2PAppContext context, String name) To be instantiated by the context.protectedSimpleTimer2(I2PAppContext context, String name, boolean prestartAllThreads) To be instantiated by the context.Method Summary
Modifier and TypeMethodDescriptionvoidaddEvent(SimpleTimer2.TimedEvent event, long timeoutMs) Queue up the given event to be fired no sooner than timeoutMs from now.voidaddPeriodicEvent(SimpleTimer2.TimedEvent event, long timeoutMs) Schedule a periodic event backed by SimpleTimer2.TimedEvent.voidaddPeriodicEvent(SimpleTimer2.TimedEvent event, long delay, long timeoutMs) Schedule a periodic event backed by SimpleTimer2.TimedEvent.private Stringdebug()Warning - slow.static intdesiredThreads(int backlogMs, int current, int max, int growMs, int shrinkMs) How many workers to run for a given backlog.(package private) static longepisodeForMs(int samples) Lower bound on how long a run ofsamplesconsecutive watchdog samples of the same thing spans: consecutive samples are at least one interval apart, so N samples cover at least N-1 intervals.(package private) static SimpleTimer2.WatchdogDecisionevaluateSaturation(int activeCount, int poolSize, int queueSize, int consecutiveSaturatedSamples, int threshold, boolean episodeReported) Decide what one saturation sample means.(package private) static SimpleTimer2.WatchdogDecisionevaluateStall(long completed, long lastCompleted, int consecutiveStalledSamples, int threshold, boolean episodeReported, long maxObservedGapMs) Decide what one "nothing finished" sample means.(package private) static SimpleTimer2.WatchdogDecisionevaluateStall(long completed, long lastCompleted, int consecutiveStalledSamples, int threshold, boolean episodeReported, long maxObservedGapMs, boolean workDue) Decide what one "nothing finished" sample means, given whether work was actually due.longThe worst recent execution delay.static SimpleTimer2If you have a context, use context.simpleTimer2() instead.intThe worker count now in effect.private voidrecordDelay(long delayMs) Record one execution delay for the backlog reading.(package private) static intresolveThreads(int configured) Resolve the worker count for a context.private ScheduledFuture<?> schedule(SimpleTimer2.TimedEvent t, long timeoutMs) Schedules one event on the executor.voidsetThreadCount(int threads) Move the worker count.voidstop()Stops the timer.private voidstop(boolean removeTask) Stops the timer.toString()Timer name.
Field Details
DEFAULT_THREADS
static final int DEFAULT_THREADSDefault worker count. Two was the original value and is enough only while the timer's total event volume stays below what two threads can service: the pool never grows past this figure, so a persistent backlog shows up as execution latency rather than as a rejected task or a warning anywhere else.A live router measured a 16s median / 30s p90 execution delay across many event types - ACKTimer, KillTimer, BodyWatchdog, DelayedCloser, Cleaner, RemoveExpired - which is the signature of a volume-saturated pool rather than of the two-blocked-events wedge the saturation watchdog exists to catch. That watchdog samples occupancy and queue depth at an instant, so it reports a wedge but is blind to a backlog whose events merely run back-to-back.
- See Also:
_threads
private int _threadsHow many workers this timer runs. Configurable because the right value is a property of total event volume, which varies with the router's tunnels, peers and services, and is not knowable where the class is written.MIN_THREADS
private static final int MIN_THREADSThe floor on worker count. Below two, one blocked event starves every periodic event on this timer, which is a worse failure than any backlog.- See Also:
MAX_THREADS
private static final int MAX_THREADSCeiling on automatic worker growth. The timer is shared by everything, so an unbounded count would let one busy subsystem take the process with it. This leaves ample headroom over the observed need without letting a runaway grow without limit.- See Also:
WATCHDOG_INTERVAL_MS
static final int WATCHDOG_INTERVAL_MSHow often the saturation watchdog samples this pool. Long enough to be free (four counter reads), short enough that a wedge is reported within a couple of minutes instead of never.- See Also:
WATCHDOG_SATURATION_SAMPLES
static final int WATCHDOG_SATURATION_SAMPLESConsecutive saturated samples tolerated before a WARN. The pool is briefly busy all the time; a pool that is still fully occupied with a backlog after this many samples is wedged.- See Also:
WATCHDOG_MIN_QUEUE
static final int WATCHDOG_MIN_QUEUEQueue depth that turns full occupancy into a backlog rather than a coincidence. WithDEFAULT_THREADSworkers, this means at least this many events are waiting behind running ones. Not zero: the queue also holds delayed events that are not due yet, so depth alone proves nothing.- See Also:
WATCHDOG_STALL_SAMPLES
static final int WATCHDOG_STALL_SAMPLESConsecutive samples with no completed task before a WARN. Longer thanWATCHDOG_SATURATION_SAMPLESso the broader, less specific signal follows the narrow one rather than pre-empting it: a wedged pool reports saturation first, and only reaches "nothing is running at all" if it stays wedged twice as long.- See Also:
WATCHDOG_RECOVERY_SAMPLES
static final int WATCHDOG_RECOVERY_SAMPLESConsecutive samples that must show progress before a quiet episode is called over.Without it a single completing task ends the episode, so a timer that finishes something every few minutes reports the same condition on every threshold window instead of once.
- See Also:
_executor
The pool running the scheduled events._recentDelayMs
private final long[] _recentDelayMsRecent execution delays, newest overwriting oldest. The control signal for worker count is how far behind the timer is running, not how busy it is at an instant: an executor whose events merely run back-to-back looks idle between them while every event is late. That is the failure mode a fixed worker count cannot fix and an instantaneous check cannot see._delayIdx
private int _delayIdxNext slot in_recentDelayMs.DELAY_SAMPLES
private static final int DELAY_SAMPLESHow many execution delays are kept for the backlog reading.- See Also:
_name
Name prefix for the timer threads._count
Counter numbering the timer threads._context
The app context, for the clock and stats._onShutdown
Hook stopping the timer on JVM exit._completed
Events completed since startup._watchdog
Watchdog watching for a saturated timer._immediateReschedules
Schedules requested with a zero or negative delay since the last watchdog report. Counted rather than logged per event: an immediate reschedule is legitimate occasionally, but a connection whose head-of-line packet is already older than one RTO asks for one on every ACK, and a population of those will bury a two-thread pool. The count is what tells the saturation report the queue is being flooded by immediate reschedules rather than by ordinary periodic work.Incremented only on the zero-delay path, so the steady-state cost is nil.
Constructor Details
SimpleTimer2
To be instantiated by the context. Others should use context.simpleTimer2() instead.- Parameters:
context- the I2P application context
SimpleTimer2
To be instantiated by the context. Others should use context.simpleTimer2() instead, except for dedicated timers that need a distinct thread name.- Parameters:
context- the I2P application contextname- the timer name, used for the timer thread name
SimpleTimer2
To be instantiated by the context. Others should use context.simpleTimer2() instead.- Parameters:
context- the I2P application contextname- the timer nameprestartAllThreads- whether to prestart all threads
Method Details
getInstance
If you have a context, use context.simpleTimer2() instead.- Returns:
- the global SimpleTimer2 instance
resolveThreads
static int resolveThreads(int configured) Resolve the worker count for a context. Pure decision helper for unit tests.- Parameters:
configured- a requested worker count, non-positive meaning unset- Returns:
- the worker count, never below the original two
desiredThreads
public static int desiredThreads(int backlogMs, int current, int max, int growMs, int shrinkMs) How many workers to run for a given backlog. Pure decision helper for unit tests.The trigger and the release are deliberately far apart. A timer that grows at 2s of backlog and shrinks at 1.9s would oscillate between two thread counts for as long as the load sat near the threshold, and every step reallocates threads. Shrinking only on a genuinely clean reading costs a little recovery time and removes the oscillation entirely.
- Parameters:
backlogMs- the worst recent execution delay in ms, 0 when nothing is latecurrent- the worker count now in effectmax- the ceiling; non-positive falls back toMAX_THREADSrather than being read as "no ceiling", so a caller passing a bad value cannot disable the boundgrowMs- backlog at or above which a worker is addedshrinkMs- backlog at or below which a worker is released- Returns:
- the worker count to move to, never below
MIN_THREADSand never above the ceiling in force
recordDelay
private void recordDelay(long delayMs) Record one execution delay for the backlog reading.- Parameters:
delayMs- how late the event ran in ms; non-positive is not recorded
getBacklogMs
public long getBacklogMs()The worst recent execution delay. This is the control signal for the worker count: an executor whose events run back-to-back looks idle between them, so occupancy and queue depth at an instant prove nothing, while the delay is what the router actually experiences.- Returns:
- the worst recorded delay in ms, or 0 when nothing has been late
setThreadCount
public void setThreadCount(int threads) Move the worker count. Grows and shrinks: shrinking relies on the pool being allowed to reap idle workers, which a scheduled executor does not do by default.- Parameters:
threads- the worker count, below the floor is raised to it
getThreadCount
public int getThreadCount()The worker count now in effect.- Returns:
- the current worker count
stop
public void stop()Stops the timer. Subsequent executions will not throw RejectedExecutionException. Cannot be restarted.stop
private void stop(boolean removeTask) Stops the timer. Subsequent executions will not throw RejectedExecutionException. Cannot be restarted.- Parameters:
removeTask- true to unregister the shutdown hook
evaluateSaturation
static SimpleTimer2.WatchdogDecision evaluateSaturation(int activeCount, int poolSize, int queueSize, int consecutiveSaturatedSamples, int threshold, boolean episodeReported) Decide what one saturation sample means.The pool counts as saturated only when both halves hold: every worker is occupied (
activeCount >= poolSize) and at leastWATCHDOG_MIN_QUEUEevents are waiting behind them. Either half alone is noise - the queue also holds delayed events that are not due yet, and both workers being busy is ordinary whenever two events happen to run at once.consecutiveSaturatedSamplescounts the samples before this one, so a threshold of N reports on the (N+1)th consecutive saturated sample: "more than N samples", not "N".episodeReportedis the throttle, which is what turns a permanently-wedged pool into one WARN instead of one everyWATCHDOG_INTERVAL_MS.- Parameters:
activeCount- executor active thread countpoolSize- executor pool size, zero once it is shut downqueueSize- executor queue depthconsecutiveSaturatedSamples- immediately preceding saturated samplesthreshold- consecutive saturated samples tolerated before reportingepisodeReported- whether this episode has already been reported- Returns:
- REPORT once per episode, OK when not saturated (ending the episode), PENDING otherwise
evaluateStall
static SimpleTimer2.WatchdogDecision evaluateStall(long completed, long lastCompleted, int consecutiveStalledSamples, int threshold, boolean episodeReported, long maxObservedGapMs) Decide what one "nothing finished" sample means.This is the cheap form of "a periodic event stopped firing": the executor's own completed-task counter not advancing across samples means nothing this timer runs is finishing, regardless of what the pool size says. It deliberately does not track individual events - that would mean bookkeeping on every scheduling and every run.
An executor that has never completed a task is treated as idle, not wedged: a timer with nothing scheduled on it is not a failure, and warning about one would bury the real reports. A wedge that starts before the first completion is still caught by
evaluateSaturation(int, int, int, int, int, boolean).The quiet period is calibrated against the timer's own worst observed gap rather than its mean interval. A mean is the wrong statistic here: a coalescing timer runs in dense bursts, so its mean interval is dominated by the busy stretches while the real gaps between bursts run far longer. Scaling the bar by a multiple of the mean therefore trips on healthy bursty timers, which trains the reader to ignore the one report that matters. The longest gap a timer has actually come back from is self-calibrating, and it reports only a quiet period that exceeds what that timer has done before.
- Parameters:
completed- executor completed task count, nowlastCompleted- executor completed task count, at the previous sampleconsecutiveStalledSamples- immediately preceding samples with no progressthreshold- such samples tolerated before reportingepisodeReported- whether this episode has already been reportedmaxObservedGapMs- longest quiet period this timer has previously come back from; zero when no baseline has been established- Returns:
- REPORT once per episode, OK on progress or an idle timer (ending the episode), PENDING otherwise
evaluateStall
static SimpleTimer2.WatchdogDecision evaluateStall(long completed, long lastCompleted, int consecutiveStalledSamples, int threshold, boolean episodeReported, long maxObservedGapMs, boolean workDue) Decide what one "nothing finished" sample means, given whether work was actually due.Added parameter
workDue: when nothing is due there is nothing to be late for, so the sample isSimpleTimer2.WatchdogDecision.OKregardless of history. The calibration below then only judges timers that genuinely had work and did not run it, which is the case worth a report.- Parameters:
completed- executor completed task count, nowlastCompleted- executor completed task count, at the previous sampleconsecutiveStalledSamples- immediately preceding samples with no progressthreshold- such samples tolerated before reportingepisodeReported- whether this episode has already been reportedmaxObservedGapMs- longest quiet period this timer has come back from; zero if unmeasuredworkDue- true when the timer had a task whose delay had elapsed- Returns:
- OK when work was not due or progress was made, PENDING below the threshold, REPORT once per episode
- See Also:
episodeForMs
static long episodeForMs(int samples) Lower bound on how long a run ofsamplesconsecutive watchdog samples of the same thing spans: consecutive samples are at least one interval apart, so N samples cover at least N-1 intervals. The bound is deliberately low, since a sample can be late.- Parameters:
samples- number of consecutive samples of one episode- Returns:
- conservative duration in ms, zero for fewer than two samples
schedule
Schedules one event on the executor.- Parameters:
t- the event to runtimeoutMs- the delay before it runs- Returns:
- the scheduled future
addEvent
Queue up the given event to be fired no sooner than timeoutMs from now. Pool is set automatically from this timer.- Parameters:
event- to be run oncetimeoutMs- run after this delay
addPeriodicEvent
Schedule a periodic event backed by SimpleTimer2.TimedEvent. The event self-reschedules via schedule() in timeReached(); this method only sets the initial delay and the minimum-period check. The period the event actually runs at is whatever its timeReached() passes to schedule()/reschedule().- Parameters:
event- the TimedEvent to run, which reschedules itself from its own timeReached()timeoutMs- delay to the first run, in ms, and lower bound for the period the event chooses for itself; the value is not enforced after the first run- Throws:
IllegalArgumentException- if timeoutMs less than 5000
addPeriodicEvent
Schedule a periodic event backed by SimpleTimer2.TimedEvent. The event self-reschedules via schedule() in timeReached(); the period this method validates is not passed to the executor as a repetition rate.- Parameters:
event- the TimedEvent to run, which reschedules itself from its own timeReached()delay- run the first iteration after delay mstimeoutMs- lower bound in ms for the period the event picks for itself, enforced only by the check below- Throws:
IllegalArgumentException- if timeoutMs less than 5000
toString
debug