java.lang.Object
net.i2p.util.SimpleTimer2

public class SimpleTimer2extends Object
Scheduled event executor backed by 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.

  • Field Details

    • DEFAULT_THREADS

      static final int DEFAULT_THREADS
      Default 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 _threads
      How 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_THREADS
      The 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_THREADS
      Ceiling 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_MS
      How 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_SAMPLES
      Consecutive 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_QUEUE
      Queue depth that turns full occupancy into a backlog rather than a coincidence. With DEFAULT_THREADS workers, 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_SAMPLES
      Consecutive samples with no completed task before a WARN. Longer than WATCHDOG_SATURATION_SAMPLES so 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_SAMPLES
      Consecutive 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

      private final ScheduledThreadPoolExecutor _executor
      The pool running the scheduled events.
    • _recentDelayMs

      private final long[] _recentDelayMs
      Recent 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 _delayIdx
      Next slot in _recentDelayMs.
    • DELAY_SAMPLES

      private static final int DELAY_SAMPLES
      How many execution delays are kept for the backlog reading.
      See Also:
    • _name

      private final String _name
      Name prefix for the timer threads.
    • _count

      private final AtomicInteger _count
      Counter numbering the timer threads.
    • _context

      private final I2PAppContext _context
      The app context, for the clock and stats.
    • _onShutdown

      private final Runnable _onShutdown
      Hook stopping the timer on JVM exit.
    • _completed

      private final AtomicLong _completed
      Events completed since startup.
    • _watchdog

      private final SimpleTimer2.SaturationWatchdog _watchdog
      Watchdog watching for a saturated timer.
    • _immediateReschedules

      private final AtomicLong _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

      public SimpleTimer2(I2PAppContext context)
      To be instantiated by the context. Others should use context.simpleTimer2() instead.
      Parameters:
      context - the I2P application context
    • SimpleTimer2

      public SimpleTimer2(I2PAppContext context, String name)
      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 context
      name - the timer name, used for the timer thread name
    • SimpleTimer2

      protected SimpleTimer2(I2PAppContext context, String name, boolean prestartAllThreads)
      To be instantiated by the context. Others should use context.simpleTimer2() instead.
      Parameters:
      context - the I2P application context
      name - the timer name
      prestartAllThreads - whether to prestart all threads
  • Method Details

    • getInstance

      public static SimpleTimer2 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 late
      current - the worker count now in effect
      max - the ceiling; non-positive falls back to MAX_THREADS rather than being read as "no ceiling", so a caller passing a bad value cannot disable the bound
      growMs - backlog at or above which a worker is added
      shrinkMs - backlog at or below which a worker is released
      Returns:
      the worker count to move to, never below MIN_THREADS and 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 least WATCHDOG_MIN_QUEUE events 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.

      consecutiveSaturatedSamples counts 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". episodeReported is the throttle, which is what turns a permanently-wedged pool into one WARN instead of one every WATCHDOG_INTERVAL_MS.

      Parameters:
      activeCount - executor active thread count
      poolSize - executor pool size, zero once it is shut down
      queueSize - executor queue depth
      consecutiveSaturatedSamples - immediately preceding saturated samples
      threshold - consecutive saturated samples tolerated before reporting
      episodeReported - 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, now
      lastCompleted - executor completed task count, at the previous sample
      consecutiveStalledSamples - immediately preceding samples with no progress
      threshold - such samples tolerated before reporting
      episodeReported - whether this episode has already been reported
      maxObservedGapMs - 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 is SimpleTimer2.WatchdogDecision.OK regardless 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, now
      lastCompleted - executor completed task count, at the previous sample
      consecutiveStalledSamples - immediately preceding samples with no progress
      threshold - such samples tolerated before reporting
      episodeReported - whether this episode has already been reported
      maxObservedGapMs - longest quiet period this timer has come back from; zero if unmeasured
      workDue - 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 of samples consecutive 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

      private ScheduledFuture<?> schedule(SimpleTimer2.TimedEvent t, long timeoutMs)
      Schedules one event on the executor.
      Parameters:
      t - the event to run
      timeoutMs - the delay before it runs
      Returns:
      the scheduled future
    • addEvent

      public void addEvent(SimpleTimer2.TimedEvent event, long timeoutMs)
      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 once
      timeoutMs - run after this delay
    • addPeriodicEvent

      public void addPeriodicEvent(SimpleTimer2.TimedEvent event, long timeoutMs)
      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

      public void addPeriodicEvent(SimpleTimer2.TimedEvent event, long delay, long timeoutMs)
      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 ms
      timeoutMs - 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

      public String toString()
      Timer name.
      Overrides:
      toString in class Object
      Returns:
      the name prefix of this timer's threads
    • debug

      private String debug()
      Warning - slow.
      Returns:
      a multi-line string with pool, queue and event state