java.lang.Object
net.i2p.router.tunnel.pool.GhostPeerManager

public class GhostPeerManagerextends Object
Temporarily excludes unresponsive peers from tunnel build selection.

A peer is excluded only after i2p.tunnel.ghostPeer.timeoutThreshold consecutive timeouts (strikes); earlier timeouts are tracked but do not exclude the peer on their own.

The exclusion is flat and short — DEFAULT_COOLDOWN_MS, with no escalation for repeat timeouts.

Escalation was removed because it bought nothing. The cooldown this mark governs is not the one that keeps a failed peer out of selection: TunnelPeerSelector.FIRST_HOP_FAIL_COOLDOWN_MS is five minutes, and ProfileOrganizer.passesBasicGates tests it directly, so a peer that failed as a first hop is already excluded from tier promotion for five minutes regardless. Every ghost cooldown the router can express — 120s, 300s, or the 240s an escalation produced — sat inside that window. Doubling it on a repeat timeout therefore excluded the peer from more than it already was, while making the mark outlive the build burst it was written to cover, which is the only thing it is uniquely positioned to do: the mark also gates promotion in roles the first-hop cooldown does not cover, and a recovered peer should be back in those roles within a minute or two.

What a silently dead peer is protected by is eviction, not escalation. BuildExecutor.penalizeTimeout demotes the contacted hop from the fast and high-cap tiers on the same failure, and the three-strike demoteIfUnreachable still runs for the failures that do not indict the peer. A peer that is dead leaves the candidate set on evidence, not on a timer.

Tracked marks are hard-bounded at MAX_TRACKED_PEERS (expired marks evicted first, then those nearest to expiry), and the *active* exclusion set is separately hard-bounded at MAX_ACTIVE_GHOSTS by deactivating the exclusions closest to lapsing. Without that second bound a burst of timeouts can exclude enough of the reachable set that tier selection returns no candidates at all — the exclusion list starves the very selection it is meant to protect. Deactivation keeps offenses and markedAt, so the strike history survives and a peer that times out again still counts toward timeoutThreshold.

The active exclusion count is cached so getGhostCount() is a volatile read on the hot path instead of a full map scan; its sweep deadline is bucketed to COUNT_GRANULARITY_MS so scattered expiry times cost at most one rescan per bucket rather than one per mark.

  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    private static final class 
    Counts captured at one point in time.
    private static final class 
    One ghost mark: exclusion expiry (snapshotted at mark time), the repeat offense count that produced it, and when it was written so the count can decay.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    Immutable (active count, sweep deadline) pair written under synchronized (this) and read lock-free, so the two fields can never be observed torn.
    private final RouterContext
    The router context.
    The marked peers, by hash.
    private long
    When the ghost peer warning was last logged.
    private final Log
    Logger for the ghost peer manager.
    private static final int
    Active exclusions are evicted down to this headroom, mirroring EVICTION_HEADROOM, so a sustained timeout storm pays for a full count rescan every few marks rather than on every mark.
    Eviction order by expiry ascending: expired marks sort first (oldest expiry first, so they go in the order they lapsed), followed by active marks nearest to lapsing.
    (package private) static final long
    Sweep deadlines are rounded up to this many milliseconds.
    (package private) static final int
    Default exclusion length.
    private static final int
    Marks evicted above MAX_TRACKED_PEERS go down to this headroom so a burst of new timeouts does not force a full evict pass per mark.
    private static final long
    Silence between the ghost peer warning logs.
    (package private) static final int
    Hard bound on *active* exclusions.
    (package private) static final int
    Hard bound on tracked marks; package-visible so tests can pin the eviction bound against it.
    (package private) static final long
    Window in which repeat timeouts keep counting toward i2p.tunnel.ghostPeer.timeoutThreshold.
  • Constructor Summary

    Constructors
    Constructor
    Description
    Create a ghost peer manager.
  • Method Summary

    Modifier and Type
    Method
    Description
    private void
    adjustActiveLocked(boolean wasActive, boolean isActive, long until)
    Apply one mark insertion to the cached active count: +1 when the peer becomes newly excluded, −1 when it stops being excluded, and no change when it was and still is (or was not and still is not).
    (package private) static long
    bucketDue(long due)
    Round a sweep deadline up to the next COUNT_GRANULARITY_MS boundary, so the cache is recomputed at most once per bucket instead of once per distinct expiry millisecond.
    private void
    capActiveLocked(long now)
    Enforce MAX_ACTIVE_GHOSTS: a burst of timeouts that excludes enough of the reachable set makes tier selection return nothing.
    void
    Clear ghost status for a peer (manual intervention).
    private static long
    Reads the ghost peer cooldown time.
    int
    Count of active ghost exclusions whose peer sits in the fast or high-capacity tier, a peer in both tiers counted once.
    int
    Count of currently excluded ghost peers (active marks only — expired or sub-threshold entries retained for offense history are not counted).
    int
    The configured timeout threshold: consecutive strikes required before a peer is excluded (values below 1 are treated as 1 by recordTimeout(Hash)).
    private static int
    Reads the timeout count that marks a peer.
    (package private) int
    Number of tracked marks, active or not (exposed for tests).
    boolean
    isGhost(Hash peer)
    Check if a peer should be excluded from tunnel selection.
    private static boolean
    isUnderAttack(double buildSuccess)
    Checks whether the build success rate means an attack.
    private void
    logGhostMarkLocked(Hash peer, boolean underAttack, long cooldownMs, int offenses, long now)
    Debug detail plus the once-per-minute warn summary.
    (package private) static int
    nextOffenses(int currentOffenses, long lastMarkAt, long now, long decayMs)
    Offense count for a repeat timeout: previous count plus one, reset to zero when the previous mark is older than the decay window (or the clock moved backwards).
    private void
    Enforce MAX_TRACKED_PEERS as a hard bound.
    void
    Record successful tunnel participation by a peer.
    void
    Record that a peer timed out during tunnel build.
    private void
    Recompute and publish the active count; caller must hold this.

    Methods inherited from class Object

    clone, equals, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • _log

      private final Log _log
      Logger for the ghost peer manager.
    • _context

      private final RouterContext _context
      The router context.
    • _ghostMarks

      private final ConcurrentHashMap<Hash, GhostPeerManager.GhostMark> _ghostMarks
      The marked peers, by hash.
    • DEFAULT_COOLDOWN_MS

      static final int DEFAULT_COOLDOWN_MS
      Default exclusion length. Long enough to sit out the burst of builds that follows one failure, short enough that a peer proven live by a later success is back in tier rotation within a minute or two.
      See Also:
    • MAX_TRACKED_PEERS

      static final int MAX_TRACKED_PEERS
      Hard bound on tracked marks; package-visible so tests can pin the eviction bound against it.
      See Also:
    • OFFENSE_DECAY_MS

      static final long OFFENSE_DECAY_MS
      Window in which repeat timeouts keep counting toward i2p.tunnel.ghostPeer.timeoutThreshold. After this long without a fresh timeout the strike count resets, so a peer that recovered and later has one bad day starts the count over rather than being excluded on its first strike.

      This no longer governs a cooldown — the exclusion is flat (DEFAULT_COOLDOWN_MS) — but it still governs whether a timeout is a first strike or a repeat one when the threshold is above 1.

      See Also:
    • GHOST_WARN_INTERVAL_MS

      private static final long GHOST_WARN_INTERVAL_MS
      Silence between the ghost peer warning logs.
      See Also:
    • _lastGhostWarnTime

      private volatile long _lastGhostWarnTime
      When the ghost peer warning was last logged.
    • _activeSnapshot

      private volatile GhostPeerManager.CountSnapshot _activeSnapshot
      Immutable (active count, sweep deadline) pair written under synchronized (this) and read lock-free, so the two fields can never be observed torn. The deadline is the earliest expiry among active marks (Long.MAX_VALUE when none), which makes it the validity bound of the cached count: once the clock passes it the count is stale and the next reader or writer recomputes it.
    • EVICTION_HEADROOM

      private static final int EVICTION_HEADROOM
      Marks evicted above MAX_TRACKED_PEERS go down to this headroom so a burst of new timeouts does not force a full evict pass per mark.
      See Also:
    • MAX_ACTIVE_GHOSTS

      static final int MAX_ACTIVE_GHOSTS
      Hard bound on *active* exclusions. Excluded peers are also gated out of tier selection, so an unbounded active set starves the candidate pool: observed as hundreds of "Skipping ghost peer" lines and "All selected peers were ghosts" build failures. Keeping the active set well below the reachable peer count preserves the exclusion's value without ever letting it empty the tier. Package-visible so tests can pin the bound.
      See Also:
    • ACTIVE_EVICTION_HEADROOM

      private static final int ACTIVE_EVICTION_HEADROOM
      Active exclusions are evicted down to this headroom, mirroring EVICTION_HEADROOM, so a sustained timeout storm pays for a full count rescan every few marks rather than on every mark.
      See Also:
    • COUNT_GRANULARITY_MS

      static final long COUNT_GRANULARITY_MS
      Sweep deadlines are rounded up to this many milliseconds. Marks expire at whatever offset the timeout landed on; without bucketing, a hundred staggered expiries would force a full map rescan per distinct millisecond as getGhostCount() is polled. Bucketing costs at most this much staleness in the *count* only — isGhost(Hash) always reads the map directly, so selection is never affected.
      See Also:
    • BY_EXPIRY

      private static final Comparator<Map.Entry<Hash, GhostPeerManager.GhostMark>> BY_EXPIRY
      Eviction order by expiry ascending: expired marks sort first (oldest expiry first, so they go in the order they lapsed), followed by active marks nearest to lapsing. Keying on the expiry rather than the mark time keeps whichever exclusions have the most protection left. A 4× escalated mark still ten minutes from lapsing outlives a base mark about to lapse, which markedAt ordering would get backwards.
  • Constructor Details

    • GhostPeerManager

      public GhostPeerManager(RouterContext context)
      Create a ghost peer manager.
      Parameters:
      context - the router context
  • Method Details

    • getTimeoutThreshold

      private static int getTimeoutThreshold(RouterContext ctx)
      Reads the timeout count that marks a peer.
      Parameters:
      ctx - the router context
      Returns:
      the threshold, clamped to a sane range
    • getCooldownMs

      private static long getCooldownMs(RouterContext ctx)
      Reads the ghost peer cooldown time.
      Parameters:
      ctx - the router context
      Returns:
      the cooldown, in ms, clamped to a sane range
    • isUnderAttack

      private static boolean isUnderAttack(double buildSuccess)
      Checks whether the build success rate means an attack.
      Parameters:
      buildSuccess - the cached tunnel build success rate
      Returns:
      true if the rate is below the attack threshold
    • recordTimeout

      public void recordTimeout(Hash peer)
      Record that a peer timed out during tunnel build.

      The peer is excluded only once its strike count reaches the configured threshold (i2p.tunnel.ghostPeer.timeoutThreshold, at least 1). Earlier strikes are recorded without excluding it, so the configured sensitivity actually gates selection. The exclusion expiry is snapshotted at mark time so a later state change doesn't extend or shorten an active exclusion; a fresh timeout is new evidence and re-marks the peer for a fresh DEFAULT_COOLDOWN_MS. Re-marking also repairs the old stale-entry hole where an expired, never-pruned entry made putIfAbsent silently no-op and left the peer unmarked.

      Parameters:
      peer - the router hash of the peer that timed out; our own hash is ignored
    • nextOffenses

      static int nextOffenses(int currentOffenses, long lastMarkAt, long now, long decayMs)
      Offense count for a repeat timeout: previous count plus one, reset to zero when the previous mark is older than the decay window (or the clock moved backwards). Pure decision helper.
      Parameters:
      currentOffenses - offense count on the existing mark, < 0 treated as 0
      lastMarkAt - when the existing mark was written (ms), 0 if none
      now - current router time (ms)
      decayMs - window in which repeat offenses count (ms)
      Returns:
      offense count to apply to the new mark
    • logGhostMarkLocked

      private void logGhostMarkLocked(Hash peer, boolean underAttack, long cooldownMs, int offenses, long now)
      Debug detail plus the once-per-minute warn summary. Caller must hold this — the warn path both reads and writes the rate-limit stamp, and callers are already inside the section that owns the map mutation, so folding it in avoids a second acquisition per marked peer.
      Parameters:
      peer - the peer just marked as a ghost, named in the debug log
      underAttack - true if the network is under stress, noted in the debug log
      cooldownMs - the exclusion duration applied to the peer, in milliseconds
      offenses - the peer's repeat offense count, 0 for a first mark
      now - the caller's view of the clock, reused instead of re-reading it
    • adjustActiveLocked

      private void adjustActiveLocked(boolean wasActive, boolean isActive, long until)
      Apply one mark insertion to the cached active count: +1 when the peer becomes newly excluded, −1 when it stops being excluded, and no change when it was and still is (or was not and still is not). The deadline only ever moves earlier — pushing it out would risk serving a stale count past a real expiry, whereas an early deadline just costs one extra exact rescan. Caller must hold this.
      Parameters:
      wasActive - whether the peer's previous mark was still excluding it
      isActive - whether the new mark excludes it
      until - new mark's expiry (ms), irrelevant when not active
    • bucketDue

      static long bucketDue(long due)
      Round a sweep deadline up to the next COUNT_GRANULARITY_MS boundary, so the cache is recomputed at most once per bucket instead of once per distinct expiry millisecond. Idempotent; Long.MAX_VALUE (no active marks) passes through untouched.
      Parameters:
      due - earliest active expiry, or Long.MAX_VALUE
      Returns:
      the bucketed deadline
    • capActiveLocked

      private void capActiveLocked(long now)
      Enforce MAX_ACTIVE_GHOSTS: a burst of timeouts that excludes enough of the reachable set makes tier selection return nothing. The exclusion intended to protect the build instead starves it (seen as "All selected peers were ghosts"). Deactivate the excess, dropping the exclusions nearest to lapsing so the set keeps the marks with the most time left. Deactivation zeroes until only: the offense count and mark time survive, so a peer that keeps timing out re-escalates instead of starting over. No-op — and so free — while the set is within bounds. Caller must hold this.
      Parameters:
      now - current router time (ms)
    • pruneToLimitLocked

      private void pruneToLimitLocked(long now)
      Enforce MAX_TRACKED_PEERS as a hard bound. Triggered only once the bound is actually reached, then evicted down to the headroom, so the headroom really is the number of inserts absorbed before the next pass rather than the point the pass starts at. Eviction order is BY_EXPIRY — expired marks first, then actives nearest to lapsing. O(n log n), once per headroom's worth of inserts. Rescans the active count only when it actually evicted, so the caller's single delta isn't undone by an unconditional recompute. Caller must hold this.
      Parameters:
      now - current router time (ms)
    • recordSuccess

      public void recordSuccess(Hash peer)
      Record successful tunnel participation by a peer. Clears ghost status immediately, including its offense history. A peer that just routed a tunnel is proven live.

      Called for every peer on every successful build, so the overwhelming majority of calls find no mark present. Only a call that actually removed a mark is counted: a success from a peer that was never excluded is not a recovery, and counting those inflated this stat to roughly 3.4x the number of marks added, which made the two counters impossible to reconcile.

      Parameters:
      peer - the router hash of the peer that just routed a tunnel; our own hash is ignored
    • isGhost

      public boolean isGhost(Hash peer)
      Check if a peer should be excluded from tunnel selection. Expired marks are left in place (not evicted) so their strike count survives toward i2p.tunnel.ghostPeer.timeoutThreshold; they report false here and count as inactive in getGhostCount().
      Parameters:
      peer - the router hash to test
      Returns:
      true if the peer is a ghost and should be skipped
    • getThreshold

      public int getThreshold()
      The configured timeout threshold: consecutive strikes required before a peer is excluded (values below 1 are treated as 1 by recordTimeout(Hash)).
      Returns:
      threshold number of timeouts before exclusion
    • clearGhost

      public void clearGhost(Hash peer)
      Clear ghost status for a peer (manual intervention).
      Parameters:
      peer - the router hash whose ghost mark should be removed
    • getGhostCount

      public int getGhostCount()
      Count of currently excluded ghost peers (active marks only — expired or sub-threshold entries retained for offense history are not counted). A single volatile read while the cached count is within its deadline; an exact rescan only once the bucketed deadline passes. Deliberately capped by MAX_ACTIVE_GHOSTS, which is what keeps a timeout storm from emptying the candidate tier.
      Returns:
      number of ghost peers
    • getFastOrHighCapGhostCount

      public int getFastOrHighCapGhostCount()
      Count of active ghost exclusions whose peer sits in the fast or high-capacity tier, a peer in both tiers counted once.

      Needed to pair with ProfileOrganizer.getFastOrHighCapCount(): an all-tier ghost count divided by a tier size overstates, because most marks land on peers that were never in the preferred tier and would not have competed for those slots.

      Iterates the tracked marks, bounded by MAX_TRACKED_PEERS and filtered to active ones, so it is not a hot-path call — the health scorer polls it once per tuning cycle.

      Returns:
      active ghost exclusions within the fast or high-capacity tier
    • getTrackedCount

      int getTrackedCount()
      Number of tracked marks, active or not (exposed for tests). Bounded by MAX_TRACKED_PEERS plus in-flight inserts.
      Returns:
      the size of the mark map
    • rescanActiveLocked

      private void rescanActiveLocked(long now)
      Recompute and publish the active count; caller must hold this. The deadline is the earliest expiry among active marks, bucketed up to COUNT_GRANULARITY_MS, so the cache is self-invalidating within one bucket of the soonest exclusion lapsing without forcing a rescan per staggered expiry.
      Parameters:
      now - current router time (ms)