java.lang.Object
net.i2p.router.tunnel.pool.BuildExecutor
All Implemented Interfaces:
Runnable

public class BuildExecutorextends Object implements Runnable
Single threaded controller of the tunnel creation process, spanning all tunnel pools. Essentially, this loops across the pools, sees which want to build tunnels, and fires off the necessary activities if the load allows. If nothing wants to build any tunnels, it waits for a short period before looping again (or until it is told that something changed, such as a tunnel failed, new client started up, or tunnel creation was aborted).

Note that the default 11 minute tunnel expiration is assumed in here.

As of 0.8.11, inbound request handling is done in a separate thread.

  • Field Details

    • MAX_BUILD_CONCURRENCY

      private static final int MAX_BUILD_CONCURRENCY
      Absolute ceiling on concurrent builds. Bounds elevation.
      See Also:
    • SIMULTANEOUS_COLLAPSED_POOLS

      private static final int SIMULTANEOUS_COLLAPSED_POOLS
      How many pools the global dispatch budget must be able to serve at the same time. Two pools can both reach their MAX_INPROGRESS_PER_POOL_DIR cap at once, and the budget must not be below what that costs.
      See Also:
    • MAX_EXPIRY_BONUS

      private static final int MAX_EXPIRY_BONUS
      Tunnels near expiry at which the urgency bonus saturates. Beyond this, counting more tunnels adds no priority, so one pool with a whole tier about to lapse cannot outrank every other pool indefinitely.
      See Also:
    • EXPIRY_BONUS_PER_TUNNEL

      private static final int EXPIRY_BONUS_PER_TUNNEL
      Score per near-expiry tunnel. Below a near-collapsed pool's boost and above the largest ordinary deficit for a small pool, so expiry lifts a nearly-full pool above a thinly-covered one without inverting the two.
      See Also:
    • SCORE_COMPARATOR

      private static final Comparator<Object[]> SCORE_COMPARATOR
      Comparator for snapshotted Object[2] rows where row[0] is the score (Integer) and row[1] is the pool. Higher score first, then stable tiebreaker. Must never be called with rows from different array instances (identityHashCode is only unique within one snapshot).
    • DISPATCH_COMPARATOR

      static final Comparator<Object[]> DISPATCH_COMPARATOR
      Dispatch comparator for snapshotted Object[3] rows where row[0] is the score (Integer), row[1] the pool and row[2] the zero-hop emergency flag (Boolean): emergency replacements go first regardless of deficit, then higher score first. Deliberately no identity-hash tiebreaker — Arrays.sort is stable, so full ties keep their allocation order from calculatePairedBuilds() instead of an arbitrary per-session order that could starve one of two equally-ranked pools indefinitely.
    • _recentBuildIds

      private final Set<Long> _recentBuildIds
      Build ids recently sent, for duplicate replies.
    • _context

      private final RouterContext _context
      The router context.
    • _log

      private final Log _log
      Logger for tunnel builds.
    • _manager

      private final TunnelPoolManager _manager
      The tunnel pool manager we build for.
    • _ghostPeerManager

      private final GhostPeerManager _ghostPeerManager
      Tracks peers whose builds keep failing.
    • _currentlyBuilding

      private final Object _currentlyBuilding
      Notify lock for the building map.
    • _currentlyBuildingMap

      private final ConcurrentHashMap<Long, PooledTunnelCreatorConfig> _currentlyBuildingMap
      Tunnels currently being built, by reply id.
    • _recentlyBuildingMap

      private final ConcurrentHashMap<Long, PooledTunnelCreatorConfig> _recentlyBuildingMap
      Recently finished builds, by reply id.
    • _isRunning

      private volatile boolean _isRunning
      True while the executor thread runs.
    • _repoll

      private boolean _repoll
      True to force another build pass.
    • _lastBuildPassTime

      private long _lastBuildPassTime
      When the last build pass started.
    • _buildSuccessCount

      private final AtomicInteger _buildSuccessCount
      Successful builds counted in the window.
    • _buildFailureCount

      private final AtomicInteger _buildFailureCount
      Failed builds counted in the window.
    • _lastRebuildTime

      private final ConcurrentHashMap<TunnelPool, Long> _lastRebuildTime
      When each pool last rebuilt its tunnels.
    • _lastConfigureTime

      private final ConcurrentHashMap<TunnelPool, Long> _lastConfigureTime
      Throttle for peer-selection hot path: BldExecutor was pegging at 98% in ClientPeerSelector.selectSingleHop → IBGWExcluder.contains even after endpoint caching, because the tight loop called pool.configureNewTunnel() for every wanted pool without per-pool spacing. Skip the heavy peer selection if this pool built within the throttle window and still has builds in flight. Entries are dropped in shutdown()/removePoolState() so the map cannot leak.
    • CONFIGURE_THROTTLE_MS

      private static final long CONFIGURE_THROTTLE_MS
      Minimum interval between pool reconfigures.
      See Also:
    • _buildTimeoutCount

      private final AtomicInteger _buildTimeoutCount
      Timed-out builds counted in the window.
    • _firstHopResults

      private final byte[] _firstHopResults
      Sliding window of recent first-hop outcomes, the same shape as _buildResults but with its own membership, since the first-hop timeout counts a different set of results than the build timeout does. Replaces two lifetime counters that were never reset: their ratio converged on the router's whole-uptime average, so the "adaptive" first-hop timeout stopped responding to anything happening now.
      See Also:
    • _firstHopWriteIndex

      private final AtomicInteger _firstHopWriteIndex
      Write index into the first hop window.
    • FIRST_HOP_SUCCESS

      private static final byte FIRST_HOP_SUCCESS
      _firstHopResults entry for a first hop that replied.
      See Also:
    • FIRST_HOP_FAILURE

      private static final byte FIRST_HOP_FAILURE
      _firstHopResults entry for a first hop that did not.
      See Also:
    • WINDOW_SIZE

      private static final int WINDOW_SIZE
      Sliding window of recent build results for smooth rate calculation. Replaces the old counter-halving approach which caused sawtooth oscillation in the timeout rate. Each entry is a Result ordinal: SUCCESS(0), BAD_RESPONSE(1), ..., TIMEOUT(10), etc. The window is indexed by a monotonically increasing counter modulo WINDOW_SIZE.
      See Also:
    • _buildResults

      private final byte[] _buildResults
      Ring of recent build results.
    • _windowWriteIndex

      private final AtomicInteger _windowWriteIndex
      Write index into the result ring.
    • ADAPTIVE_TIMEOUT_RECALC_BUILDS

      static final int ADAPTIVE_TIMEOUT_RECALC_BUILDS
      How many recorded builds pass before the adaptive timeout is recalculated from the window. Package-visible so the trigger can be tested without a running router.
      See Also:
    • _timeoutRate

      private volatile double _timeoutRate
      Adaptive concurrency throttle: tracks the timeout rate and adjusts maxConcurrentBuilds dynamically. When timeout rate exceeds 30%, builds are throttled to prevent overwhelming the IB reply path. When timeout rate drops below 15% and success exceeds 80%, concurrency is gradually restored.
    • _adaptiveMaxConcurrentBuilds

      private volatile int _adaptiveMaxConcurrentBuilds
      Current limit on concurrent builds.
    • _urgentMaxConcurrentBuilds

      private volatile int _urgentMaxConcurrentBuilds
      Concurrency ceiling for the current dispatch pass: the adaptive ceiling, lifted when expiry-urgent pools cannot be served at it. Recomputed at the top of every pass, so the lift goes away as soon as the urgency does. Elevation used to be written into the static configured ceiling instead, which nothing ever lowered again - a single urgent burst left the router permanently above its Tuner setting, and the throttle's restore step then ramped back up to the elevated value rather than to the configured one.
      See Also:
    • _firstHopFailureHistory

      private final ConcurrentHashMap<Hash,long[]> _firstHopFailureHistory
      First-hop failure history: tracks recent first-hop failures to avoid repeatedly selecting peers that have recently failed as first hops. Maps: Hash -> [failureCount, lastFailureTimeMs].
    • FIRST_HOP_HISTORY_MAX

      private static final int FIRST_HOP_HISTORY_MAX
      Bound on the first-hop failure history before expired entries are pruned. Well above the number of peers any pool uses as first hops at one time, so the prune only ever reclaims peers that have gone away.
      See Also:
    • _lastDispatchTime

      private final ConcurrentHashMap<Hash,Long> _lastDispatchTime
      Last dispatch time per first-hop peer, for staggering builds. Maps: Hash -> dispatch timestamp in ms.
    • STAGGER_TRACK_MAX

      private static final int STAGGER_TRACK_MAX
      Bound on the first-hop stagger map before dead entries are pruned.
      See Also:
    • _firstHopCooldownMs

      private static volatile long _firstHopCooldownMs
      Maximum age (ms) for first-hop failure history entries. Entries older than this are ignored during lookup. Tunable via Tuner.
    • _firstHopFailureThreshold

      private static volatile int _firstHopFailureThreshold
      Number of failures within _firstHopCooldownMs required to skip a peer as first hop. A single transient failure should not permanently exclude a peer; repeated failures indicate a persistent issue.
    • _staleBuildThresholdPct

      private static volatile int _staleBuildThresholdPct
      Stale build pruning threshold fraction. When the time elapsed since a build was configured exceeds this fraction of the adaptive timeout budget, the build is skipped (it would timeout anyway). Expressed as percentage (e.g. 40 means 40%). Tunable via Tuner.
    • MAX_PER_POOL_DIR

      private static final int MAX_PER_POOL_DIR
      Maximum concurrent in-flight builds per pool per direction (inbound or outbound). Enforces fair build distribution across pools so no single pool monopolizes build slots. Each pool's inbound and outbound directions are tracked independently, so a pool with target 3/2 can have 2 in-flight inbound and 2 in-flight outbound simultaneously. Raised from 2 so incomplete-LeaseSet / depleted pools can stage replacements in parallel without serializing on the configure throttle.
      See Also:
    • _consecutiveFailureThreshold

      private static volatile int _consecutiveFailureThreshold
      Per-pool consecutive build failure tracking for backoff. When a pool exceeds _consecutiveFailureThreshold, builds are paused for _poolBackoffMs to prevent build storms. Maps: TunnelPool -> [consecutiveFailures, backoffUntilMs]

      Threshold of 5 allows transient failures (network hiccups, slow peers) without triggering premature backoff. Previous threshold of 3 caused cascading pool collapses: 3 failures → backoff → tunnels expire → EMERGENCY → more failures → higher backoff counter → death spiral.

      Backoff of 12s is long enough to avoid build storms but short enough that tunnels expiring during backoff can be rebuilt before the pool fully collapses. Previous 8s was too short to prevent repeated storms when the underlying issue was transient.

    • _poolBackoffMs

      private static volatile long _poolBackoffMs
      Current backoff between pool build passes.
    • _poolFailureState

      private final ConcurrentHashMap<TunnelPool, long[]> _poolFailureState
      Per-pool failure tracking state.
    • _lastKeepAliveTime

      private long _lastKeepAliveTime
      When the keepalive pass last ran.
    • _adaptiveTimeout

      private volatile long _adaptiveTimeout
      Current adaptive build timeout.
    • _adaptiveFirstHopTimeout

      private volatile long _adaptiveFirstHopTimeout
      Current adaptive first hop timeout.
    • _tunedTargetBuffer

      private static volatile int _tunedTargetBuffer
      The target build buffer from config or default (0). Extra tunnels to maintain beyond the configured quantity. Tunable via i2p.tunnel.targetBuffer (default: 0). Tuned value overrides config, when set by the Tuner.
    • _tunedGoodDeficitThrottle

      private static volatile long _tunedGoodDeficitThrottle
      The GOOD deficit throttle interval from config or default (30s). Minimum time between GOOD-tunnel deficit rebuilds for non-critical pools. Tunable via i2p.tunnel.goodDeficitThrottle (default: 30000). Tuned value overrides config, when set by the Tuner.
    • _maxConcurrentBuilds

      private static volatile int _maxConcurrentBuilds
      The maximum number of concurrent tunnel builds allowed. Calculated based on CPU cores and configurable multiplier.
    • LOOP_TIME

      private static final int LOOP_TIME
      Interval between executor passes.
      See Also:
    • MIN_BUILD_SPACING_MS

      private static final int MIN_BUILD_SPACING_MS
      Minimum time between consecutive build passes. Fast build completions wake the loop (buildComplete notifyAll), so without a floor the loop re-passes near-continuously during a cascade (~180 builds/min observed) instead of building in fewer, higher-quality bursts.
      See Also:
    • TUNNEL_POOLS

      private static final int TUNNEL_POOLS
      Pools considered per build pass.
      See Also:
    • KEEPALIVE_INTERVAL_MS

      private static final long KEEPALIVE_INTERVAL_MS
      keepAlive() cadence in the executor loop (see run2()).
      See Also:
    • RATES

      private static final long[] RATES
      Rate stat periods for the build statistics.
    • INBOUND_BIT

      private static final int INBOUND_BIT
      Bit marking a window entry as an inbound build.

      Packed into the same byte as the result ordinal so one write records both and a concurrent reader can never see a result attributed to the wrong direction. BuildExecutor.Result has nine values, so ordinals 0-8 leave the top bit free.

      See Also:
    • RESULT_MASK

      private static final int RESULT_MASK
      Mask selecting the result ordinal out of a packed entry.

      An explicit mask rather than ~INBOUND_BIT: the complement of 0x80 keeps every high bit set, and applying it to the sign-extended byte leaves bit 7 in place, so an inbound entry indexed BuildExecutor.Result.values() at a negative ordinal and threw ArrayIndexOutOfBoundsException on the build path.

      See Also:
    • _concurrencyThrottleThreshold

      private static double _concurrencyThrottleThreshold
      Adaptive concurrency throttle thresholds. When timeout rate exceeds THROTTLE_THRESHOLD, concurrent builds are reduced to prevent overwhelming the IB reply path. When timeout rate drops below RESTORE_THRESHOLD and success exceeds RESTORE_SUCCESS_THRESHOLD, concurrency is restored.
    • _concurrencyRestoreThreshold

      private static double _concurrencyRestoreThreshold
      Build success ratio at which concurrency may start restoring.
    • _concurrencyRestoreSuccessThreshold

      private static double _concurrencyRestoreSuccessThreshold
      Build success ratio needed to fully restore concurrency.
    • MAX_ADAPTIVE_TIMEOUT_MS

      private static final long MAX_ADAPTIVE_TIMEOUT_MS
      Safety ceiling for any computed adaptive build timeout, regardless of tunnel length, direction, system load, or measured RTT.
      See Also:
    • LENGTH_STEP_MS

      private static final long LENGTH_STEP_MS
      Ms added per extra hop beyond the 3-hop baseline @since 0.9.71+.
      See Also:
    • LENGTH_BASELINE

      private static final int LENGTH_BASELINE
      Hop count at or below which no length adjustment applies @since 0.9.71+.
      See Also:
    • SLOW_BUILD_MS

      static final long SLOW_BUILD_MS
      Build completion time above which a build counts as slow.

      Half the floor the build reply deadline is clamped to in calculateAdaptiveTimeoutFromSuccess(). Half rather than the deadline itself, so the count stays selective: anything it records is a build that would have been at risk of expiring had the budget been at its floor. If nearly every build were counted the stat would just mirror tunnel.buildClientSuccess and say nothing new, and if none were it would be dead.

      This exists for the same reason as udp.*EstablishSlow. The success stats record a duration but expose only a mean, and build latency is right-skewed, so the mean says little about the builds that nearly timed out - which are exactly the ones a reply budget has to cover.

      See Also:
    • CPU_LOAD_HIGH

      private static final int CPU_LOAD_HIGH
      CPU load percentage above which the +2s load term applies. @since 0.9.71+.
      See Also:
    • CPU_LOAD_CRITICAL

      private static final int CPU_LOAD_CRITICAL
      CPU load percentage above which the +3s load term applies. @since 0.9.71+.
      See Also:
    • OUTBOUND_EXTRA_MS

      private static final long OUTBOUND_EXTRA_MS
      Ms added to the base timeout for the longer outbound reply path @since 0.9.71+.
      See Also:
    • SUCCESS_FAST

      private static final double SUCCESS_FAST
      Success rate above which the network is answering fast enough to shave time off @since 0.9.71+.
      See Also:
    • SUCCESS_GOOD

      private static final double SUCCESS_GOOD
      Success rate above which the base timeout is left alone @since 0.9.71+.
      See Also:
    • SUCCESS_MODERATE

      private static final double SUCCESS_MODERATE
      Success rate above which a modest timeout increase applies @since 0.9.71+.
      See Also:
    • FAST_NETWORK_REDUCTION_MS

      private static final long FAST_NETWORK_REDUCTION_MS
      Reduction applied when the network is fast @since 0.9.71+.
      See Also:
    • MODERATE_RECOVERY_MS

      private static final long MODERATE_RECOVERY_MS
      Increase applied at moderate success @since 0.9.71+.
      See Also:
    • LOW_SUCCESS_RECOVERY_MS

      private static final long LOW_SUCCESS_RECOVERY_MS
      Increase applied at low success driven by rejects rather than timeouts @since 0.9.71+.
      See Also:
    • THROTTLE_FACTOR

      private static final double THROTTLE_FACTOR
      Fraction of the current concurrency removed on each throttle crossing @since 0.9.71+.
      See Also:
    • THROTTLE_FLOOR_RATIO

      private static final double THROTTLE_FLOOR_RATIO
      Fraction of base concurrency the throttle may not go below @since 0.9.71+.
      See Also:
    • THROTTLE_RESTORE_DIVISOR

      private static final int THROTTLE_RESTORE_DIVISOR
      Base concurrency is restored in 1/THROTTLE_RESTORE_DIVISOR steps @since 0.9.71+.
      See Also:
    • BUILD_EXPIRY_LOG_INTERVAL_MS

      private static final long BUILD_EXPIRY_LOG_INTERVAL_MS
      Interval between emitted build-expiry reports.
      See Also:
    • BUDGET_EXHAUSTED_LOG_INTERVAL_MS

      private static final long BUDGET_EXHAUSTED_LOG_INTERVAL_MS
      Interval between build budget log lines.
      See Also:
    • _lastBuildExpiryLog

      private static final AtomicLong _lastBuildExpiryLog
      When build expiry was last logged.
    • _lastBudgetExhaustedLog

      private static final AtomicLong _lastBudgetExhaustedLog
      When budget exhaustion was last logged.
    • HOP_NEVER_ASSIGNED

      static final String HOP_NEVER_ASSIGNED
      Hop was never assigned a peer.
      See Also:
    • HOP_SELF_EXPECTED

      static final String HOP_SELF_EXPECTED
      This router sits at the gateway index it is expected to occupy.
      See Also:
    • HOP_SELF_UNEXPECTED

      static final String HOP_SELF_UNEXPECTED
      This router sits at a hop index other than the gateway slot - never legitimate.
      See Also:
    • HOP_UNREACHABLE

      static final String HOP_UNREACHABLE
      The peer is on record as unreachable: selection admitted a peer we had already written off, so the eligibility filter failed to fire.
      See Also:
    • HOP_ESTABLISHED_SILENT

      static final String HOP_ESTABLISHED_SILENT
      The peer holds an established connection but did not answer the build request.

      This is the signature that recency and reachability filtering cannot address: the peer accepts the connection and then ignores build requests, so it looks healthy to every filter that exists and still consumes a share of the timeout budget.

      See Also:
    • HOP_HANDSHAKE_UNFINISHED

      static final String HOP_HANDSHAKE_UNFINISHED
      The peer never finished a handshake, so it never had a session to answer over.
      See Also:
    • HOP_REACHABLE

      static final String HOP_REACHABLE
      The peer is reachable and has been heard from; nothing distinguishes it.
      See Also:
  • Constructor Details

    • BuildExecutor

      public BuildExecutor(RouterContext ctx, TunnelPoolManager mgr, GhostPeerManager ghostMgr)
      Create a new BuildExecutor.
      Parameters:
      ctx - the router context
      mgr - the tunnel pool manager
      ghostMgr - the ghost peer manager for tracking timeouts
  • Method Details

    • getTunnelTargetMin

      private static int getTunnelTargetMin(RouterContext ctx)
      Returns the configured minimum tunnel target.
      Parameters:
      ctx - the router context
      Returns:
      the minimum target count
    • score

      private static int score(TunnelPool p)
      Priority score for build urgency. Higher = build sooner. Must be a pure function of pool state so the snapshot comparator remains a consistent total order. Zero-hop emergency pools are not ranked here. They are ordered by DISPATCH_COMPARATOR ahead of every scored pool, so this score only arbitrates among ordinary demand.
      Parameters:
      p - the pool to score
      Returns:
      the priority score, higher builds sooner
    • countExpiryUrgent

      private static int countExpiryUrgent(List<TunnelPool> pools)
      Counts wanting pools whose charge is expiry urgency rather than deficit.
      Parameters:
      pools - the pools wanting a build this cycle
      Returns:
      how many hold a lease inside the pre-build window
    • elevateForUrgent

      static int elevateForUrgent(int base, int urgent, int inProgress)
      Elevated ceiling when urgent demand cannot be served at the current one. Pure, so the rule is testable without a router.

      Elevates only for the urgent pool's own build plus headroom, bounded by the ceiling's own maximum, and only when there is not already room under it. An ordinary deficit does not elevate: that demand is served a pool per cycle in score order, which is what keeps a busy router from permanently raising its ceiling to service pools that are merely below target.

      Parameters:
      base - the ceiling now in force
      urgent - pools holding leases about to expire
      inProgress - builds already in flight
      Returns:
      the ceiling to use, which may equal base
    • refreshUrgentCeiling

      int refreshUrgentCeiling(List<TunnelPool> wanted)
      Recomputes the concurrency ceiling for the dispatch pass about to run, from the pools it wants to build, and records it on this instance.

      Recomputed every pass rather than latched: an earlier version wrote the elevated value into the static configured ceiling, so the lift survived the urgency that caused it and the router never came back down.

      Parameters:
      wanted - the pools this pass wants to build
      Returns:
      the ceiling for this pass, which may equal the adaptive one
    • score

      static int score(int active, int target, int pairedActive, int nearExpiry)
      Pure form of the dispatch score, so the ordering can be pinned without building pools.

      Tiers, highest first:

      1. collapsed (no usable tunnel) — a pool with no service at all;
      2. near-collapse (1-2 usable);
      3. leases about to expire;
      4. deficit against target;
      5. paired balance — the direction behind its pair.

      Expiry sits above deficit and below near-collapse deliberately. A pool holding fewer tunnels than target but with hours on its leases can wait a cycle; a pool whose leases lapse in the pre-build window is losing its service in minutes and the replacement needs that same window to build. It stays below near-collapse so a genuinely thin pool is never starved by a merely time-urgent one.

      The expiry bonus is capped at MAX_EXPIRY_BONUS tunnels, so a pool that keeps producing near-expiry tunnels cannot accumulate unbounded priority — the urgency is consumed when the lease lapses.

      Parameters:
      active - usable tunnels in the pool
      target - the pool's target quantity, already floored at 2
      pairedActive - usable tunnels in the paired pool, for balance
      nearExpiry - live tunnels inside the pre-build window
      Returns:
      the score, higher builds sooner
    • isZeroHopEmergency

      static boolean isZeroHopEmergency(TunnelPool pool)
      Whether this pass should treat the pool as a zero-hop emergency: a multi-hop-configured, non-ping pool currently serving traffic through a length-1 bootstrap fallback. Such tunnels provide no anonymity, so their replacement gets an unconditional build floor in calculatePairedBuilds() and first claim on dispatch slots in run2(). Single shared definition so the allocation stage and the dispatch stage can never disagree about which pools are urgent.
      Parameters:
      pool - candidate pool, non-null
      Returns:
      true if the pool carries a zero-hop fallback it did not ask for
    • effectiveTarget

      static int effectiveTarget(RouterContext ctx, TunnelPoolSettings s, boolean keepConfiguredQuantity)
      Effective tunnel-count target a pool builds toward. Pools flagged keepConfiguredQty (ping pools, and pools expressly configured for zero hops where a length-1 tunnel IS the product) keep their configured quantity; all others hold at least targetMin tunnels per direction so a single failure can't empty the pool or starve the LeaseSet.
      Parameters:
      ctx - router context, for the configurable floor and tuner buffer
      s - settings of the pool in question
      keepConfiguredQuantity - true to return the configured quantity verbatim
      Returns:
      the effective target: the configured quantity when flagged, else at least 2
    • getFirstHopFailureCooldownMs

      public static long getFirstHopFailureCooldownMs()
      The first-hop failure cooldown in milliseconds.
      Returns:
      the cooldown in ms
    • setFirstHopFailureCooldownMs

      public static void setFirstHopFailureCooldownMs(long ms)
      Set the first-hop failure cooldown (called by Tuner).
      Parameters:
      ms - cooldown in ms (60000-600000)
    • getFirstHopFailureThreshold

      public static int getFirstHopFailureThreshold()
      The first-hop failure threshold count.
      Returns:
      the failure count, 1-10, that triggers a first-hop cooldown
    • setFirstHopFailureThreshold

      public static void setFirstHopFailureThreshold(int count)
      Set the first-hop failure threshold (called by Tuner).
      Parameters:
      count - threshold count (1-10)
    • getStaleBuildThresholdPct

      public static int getStaleBuildThresholdPct()
      The stale build pruning threshold percentage.
      Returns:
      the threshold percentage (30-80)
    • setStaleBuildThresholdPct

      public static void setStaleBuildThresholdPct(int val)
      Set the stale build pruning threshold (called by Tuner).
      Parameters:
      val - the threshold percentage (30-80)
    • jitteredBackoff

      private long jitteredBackoff()
      Jittered backoff: randomize within ±33% of _poolBackoffMs to prevent synchronized backoff where all pools are skipped in the same cycle. With a 12s base, the effective range is 8-16s.
      Returns:
      a jittered backoff duration in ms
    • getPoolFailureThreshold

      public static int getPoolFailureThreshold()
      The pool failure threshold.
      Returns:
      the consecutive failure count, 1-20, that triggers pool backoff
    • setPoolFailureThreshold

      public static void setPoolFailureThreshold(int val)
      The pool failure threshold.
      Parameters:
      val - the threshold value (1-20)
    • getPoolBackoffMs

      public static long getPoolBackoffMs()
      The pool backoff time in milliseconds.
      Returns:
      the backoff in ms
    • setPoolBackoffMs

      public static void setPoolBackoffMs(long val)
      The pool backoff time in milliseconds.
      Parameters:
      val - the backoff in ms (1000-60000)
    • getTunnelLifetime

      static int getTunnelLifetime(RouterContext ctx)
      The tunnel lifetime from config, delegated to TunnelPool.
      Parameters:
      ctx - the router context
      Returns:
      the tunnel lifetime in milliseconds
    • getTunnelTargetBuffer

      public static int getTunnelTargetBuffer(RouterContext ctx)
      The target build buffer from config or default (0). Extra tunnels to maintain beyond the configured quantity. Tunable via i2p.tunnel.targetBuffer (default: 0).
      Parameters:
      ctx - the router context
      Returns:
      the target buffer count
    • setTunnelTargetBuffer

      public static void setTunnelTargetBuffer(int count)
      Set the target build buffer (called by Tuner).
      Parameters:
      count - the target buffer count
    • getGoodDeficitThrottle

      public static long getGoodDeficitThrottle(RouterContext ctx)
      The GOOD deficit throttle interval from config or default (30s). Minimum time between GOOD-tunnel deficit rebuilds for non-critical pools. Tunable via i2p.tunnel.goodDeficitThrottle (default: 30000).
      Parameters:
      ctx - the router context
      Returns:
      the throttle interval in milliseconds
    • setGoodDeficitThrottle

      public static void setGoodDeficitThrottle(long ms)
      Set the GOOD deficit throttle interval (called by Tuner).
      Parameters:
      ms - the throttle interval in milliseconds
    • getMaxConcurrentBuilds

      public static int getMaxConcurrentBuilds()
      The maximum number of concurrent builds allowed.
      Returns:
      maximum concurrent builds
    • getAdaptiveMaxConcurrentBuilds

      int getAdaptiveMaxConcurrentBuilds()
      The adaptive maximum concurrent builds, adjusted based on the current timeout rate. Returns the throttled value when the timeout rate exceeds _concurrencyThrottleThreshold, otherwise returns the configured maximum.
      Returns:
      adaptive maximum concurrent builds
    • getUrgentMaxConcurrentBuilds

      int getUrgentMaxConcurrentBuilds()
      The concurrency ceiling in force for the current dispatch pass, the adaptive one lifted for expiry-urgent pools.
      Returns:
      the ceiling this pass builds under
    • getTimeoutRate

      double getTimeoutRate()
      The current timeout rate (0.0-1.0) for adaptive throttling.
      Returns:
      the timeout rate
    • getWindowCount

      int getWindowCount()
      Package-visible for tests: returns how many results are in the window.
      Returns:
      number of results recorded so far, saturating at the 100-slot window
    • getFirstHopWindowCount

      int getFirstHopWindowCount()
      Package-visible for tests: returns how many first-hop outcomes are in the window. Saturates at the window size, which is the point - the lifetime counters this replaced kept growing, so the first-hop success rate was diluted by history the window no longer held.
      Returns:
      number of outcomes recorded so far, saturating at the 100-slot window
    • getFirstHopSuccessCount

      int getFirstHopSuccessCount()
      Package-visible for tests: returns the successes among the first-hop outcomes currently in the window.
      Returns:
      number of successes in the window
    • setMaxConcurrentBuilds

      public static void setMaxConcurrentBuilds(int val)
      The maximum number of concurrent builds allowed.
      Parameters:
      val - the maximum concurrent builds
    • countsAsPoolFailure

      static boolean countsAsPoolFailure(BuildExecutor.Result result)
      Pool-backoff counting excludes results that are not peer failures: SUCCESS, DUP_ID (already handled), REJECT (peer said no), NO_TUNNELS (local resource condition), NO_NETDB (local netdb miss), and SKIPPED (local policy, no build dispatched).
      Parameters:
      result - the build result
      Returns:
      true if the result increments the pool consecutive-failure counter
    • spacingDelay

      static long spacingDelay(long lastPassTime, long now)
      Remaining build-pass spacing in ms: 0 when the spacing floor since the last pass is satisfied, otherwise the time still left to wait.
      Parameters:
      lastPassTime - the end of the last build pass in ms
      now - the current time in ms
      Returns:
      ms still to wait, 0 if the floor is met
    • getGracePeriod

      private static long getGracePeriod(RouterContext ctx)
      Returns the startup grace period for builds.
      Parameters:
      ctx - the router context
      Returns:
      the grace period, in ms
    • fullStats

      public boolean fullStats()
      Check if full statistics are enabled.
      Returns:
      true if full statistics are enabled
    • restart

      public void restart()
      Restart the build executor, clearing recent build state.
    • shutdown

      public void shutdown()
      Cannot be restarted.
    • removePoolState

      void removePoolState(TunnelPool pool)
      Remove failure state for a pool that is being removed. Prevents unbounded growth of _poolFailureState, _lastRebuildTime, and _lastConfigureTime across pool lifecycles.
      Parameters:
      pool - the pool to remove state for
    • getOrCreatePoolState

      private long[] getOrCreatePoolState(TunnelPool pool)
      Failure tracking state for a pool, created if absent. Uses get()+putIfAbsent() instead of computeIfAbsent() to avoid per-call allocation of the fallback array and reduce lock contention.
      Parameters:
      pool - the pool whose failure tracking state is wanted
      Returns:
      existing or newly-created long[2]
    • buildDirectionStat

      static String buildDirectionStat(boolean inbound, String event)
      Name of a per-direction build outcome stat, so build success can be compared inbound against outbound. The aggregate tunnel.buildSuccessRate cannot answer that: it mixes both directions, and they do not fail for the same reasons.
      Parameters:
      inbound - true for an inbound tunnel build
      event - Attempted, Succeeded, Failed, TimedOut or SuccessRate
      Returns:
      the stat name
    • recordBuildDirection

      private void recordBuildDirection(PooledTunnelCreatorConfig cfg, BuildExecutor.Result result)
      Record one build outcome against the direction being built. Called for every completed build, including the ones excluded from the adaptive timeout statistics, so the counts are a true per-direction sample.
      Parameters:
      cfg - the tunnel that was being built, may be null
      result - the outcome, bucketed by buildOutcomeEvent(Result)
    • buildOutcomeEvent

      static String buildOutcomeEvent(BuildExecutor.Result result)
      Which outcome bucket a build result belongs to. Everything that is not a success or a timeout counts as failed, so a newly added BuildExecutor.Result is reported rather than silently dropped.
      Parameters:
      result - the build outcome
      Returns:
      the event suffix, e.g. Succeeded
    • windowEntry

      static byte windowEntry(BuildExecutor.Result result, boolean inbound)
      Pack a build outcome and its direction into one window entry.

      Pure decision — no state, safe for unit tests.

      Parameters:
      result - the build outcome
      inbound - whether the build was inbound
      Returns:
      the packed window entry
    • windowResult

      static BuildExecutor.Result windowResult(byte entry)
      The outcome recorded in a window entry.
      Parameters:
      entry - a packed entry from windowEntry(BuildExecutor.Result, boolean)
      Returns:
      the build outcome
    • windowInbound

      static boolean windowInbound(byte entry)
      Whether a window entry is an inbound build.
      Parameters:
      entry - a packed entry from windowEntry(BuildExecutor.Result, boolean)
      Returns:
      true if the build was inbound
    • windowResultCount

      static int windowResultCount(byte[] results, int count, BuildExecutor.Result want)
      How many of the first count window entries recorded want as their outcome.

      A window entry packs the inbound flag into its top bit, so the outcome has to be masked out before it is compared. Comparing the raw byte instead matches nothing for an inbound build, and that is how the adaptive timeout and the concurrency throttle came to be decided on outbound-only evidence while claiming the whole window.

      Parameters:
      results - the window array, oldest entry first
      count - how many leading entries to examine; entries past the array end are ignored, so a count larger than the array is safe
      want - the outcome to count
      Returns:
      the number of entries whose outcome is want
    • recordFirstHopOutcome

      void recordFirstHopOutcome(boolean success)
      Records whether the first hop of a build replied, in the window the adaptive first-hop timeout is computed from. Package-visible for tests.
      Parameters:
      success - true if the first hop accepted the build
    • firstHopWindowCount

      static int firstHopWindowCount(long writeIndex)
      How many first-hop outcomes the window holds for a given write index. Saturates at the window size: the array is a ring, so outcomes older than WINDOW_SIZE have been overwritten and must not stay in the denominator.
      Parameters:
      writeIndex - the window's write index, one past the last entry written
      Returns:
      the number of entries to read back, never more than WINDOW_SIZE
    • firstHopSuccessCount

      static int firstHopSuccessCount(byte[] results, int count)
      How many of the first count first-hop window entries were successes. Pure and static so the input to the adaptive first-hop timeout can be tested without a running build executor.
      Parameters:
      results - the window array, oldest entry first
      count - how many leading entries to examine; entries past the array end are ignored, so a count larger than the array is safe
      Returns:
      the number of successes among the entries examined
    • updateBuildStats

      private void updateBuildStats(BuildExecutor.Result result, boolean inbound)
      Update success/failure/timeout counters and calculate adaptive timeout.

      The per-direction success rates and the aggregate are all computed from this one window in this one pass. That is deliberate: smoothing computed separately would give each rate its own sampling noise, and a per-direction rate that does not reconcile with the aggregate it is meant to be compared against cannot be compared at all. Sharing the window also makes the per-direction rates sum to the aggregate by construction.

      Parameters:
      result - the build outcome
      inbound - whether the build was inbound
    • recalcAdaptiveTimeout

      static boolean recalcAdaptiveTimeout(int seq)
      Whether the build numbered seq should recalculate the adaptive timeout.

      Decided from the caller's own sequence number, never from a fresh read of the counter: builds are recorded concurrently, and a counter read here can land past the boundary that no thread acted on, silently skipping a recalculation for as much as a full period.

      Parameters:
      seq - the zero-based number of this build, as returned by the increment that recorded it
      Returns:
      true if this build completes a period of ADAPTIVE_TIMEOUT_RECALC_BUILDS builds
    • getConcurrencyThrottleThresholdPct

      public static int getConcurrencyThrottleThresholdPct()
      The concurrency throttle threshold as a percentage (0-100).
      Returns:
      the threshold percentage
    • setConcurrencyThrottleThresholdPct

      public static void setConcurrencyThrottleThresholdPct(int pct)
      Set the concurrency throttle threshold (called by Tuner).
      Parameters:
      pct - the threshold percentage (15-50)
    • getConcurrencyRestoreThresholdPct

      public static int getConcurrencyRestoreThresholdPct()
      The concurrency restore threshold as a percentage (0-100).
      Returns:
      the restore threshold percentage
    • calculatePerIterationCap

      static int calculatePerIterationCap(double timeoutRate)
      Calculate the per-iteration build cap from the current timeout rate. Proportional scaling (1-4) avoids the binary oscillation that the old 2-vs-4 threshold caused around the 30% boundary.
      Parameters:
      timeoutRate - the current timeout rate (0.0-1.0)
      Returns:
      cap between 1 and 4
    • calculateAdaptiveTimeoutFromSuccess

      private void calculateAdaptiveTimeoutFromSuccess()
      Calculate adaptive timeouts based on recorded build outcomes. Starts from mainline's base values (13s/10s) and adjusts marginally in either direction based on success rate.

      Adaptive ranges: REQUEST_TIMEOUT: 13s base, 10-18s range FIRST_HOP_TIMEOUT: 10s base, 8-15s range

      Also adjusts concurrent build capacity based on timeout rate to prevent overwhelming the IB reply path.

    • calculateAdaptiveTimeout

      private long calculateAdaptiveTimeout(PooledTunnelCreatorConfig cfg)
      Calculate adaptive timeout based on tunnel characteristics and network conditions.

      Convenience wrapper for single-config callers. Scans that evaluate many configs in one pass should fetch the two invariant inputs once and call calculateAdaptiveTimeout(PooledTunnelCreatorConfig, int, long).

      Parameters:
      cfg - the tunnel configuration
      Returns:
      adaptive timeout in milliseconds
    • calculateAdaptiveTimeout

      private long calculateAdaptiveTimeout(PooledTunnelCreatorConfig cfg, int cpuLoad, long rttFloor)
      Cached-variant of calculateAdaptiveTimeout(PooledTunnelCreatorConfig).

      The CPU load and RTT floor do not vary per config, only the tunnel length and direction do, so a scan over many in-flight builds fetches them once and passes them in. SystemVersion.getCPULoadAvg() is already cached for one second and getRttTimeoutFloor() reads a rate statistic, so both are stable across a single scan.

      Parameters:
      cfg - the tunnel configuration
      cpuLoad - cached value of SystemVersion.getCPULoadAvg()
      rttFloor - cached value of getRttTimeoutFloor()
      Returns:
      adaptive timeout in milliseconds
    • adaptiveTimeoutDelta

      static long adaptiveTimeoutDelta(double successRate)
      The adjustment to add to the base build timeout, from the success rate alone.

      The timeout rate is deliberately not an input. It used to be, and reaching that branch was itself a bug worth recording: the test sat after successRate > 0.50, so the intended +7s was unreachable for every success rate above half - exactly the band where a high timeout rate is most expensive, a timeout burning a whole build slot where a reject costs a round trip. Reordering fixed the reachability and the +7s then did what it said.

      It no longer belongs here. The +7s was justified while an outbound session establishment was abandoning ~117 handshakes a minute at 2250ms: a build waiting on one of those genuinely needed more time. That deadline is now floored at 4000ms and the abandonments are gone, so the timeouts that remain are not premature ones, and buying them 7 extra seconds only holds a build slot longer. Measured across an 11.8 minute window either side of that change, build success fell from 64.5% to 56.0% (-8.5pp, 4.2 sigma) and throughput 12%, with the timeout-to-reject ratio worsening from 1.55:1 to 3.2:1 - the signature of builds waiting rather than failing.

      The timeout rate still drives the concurrency throttle below, which is the lever that reduces occupancy. One signal, one lever: timeouts should shrink the amount of work in flight, not inflate how long each item may hold a slot.

      Parameters:
      successRate - fraction of recent builds that succeeded, 0..1
      Returns:
      milliseconds to add to the base timeout; may be negative
    • combineAdaptiveTimeout

      static long combineAdaptiveTimeout(long baseTimeout, double successRate)
      Combine the Tuner's base timeout with the success-rate adjustment.

      Pure, and deliberately unclamped. The base is already bounded by the Tuner's own range, and the final budget is bounded by computeAdaptiveTimeout(long, int, boolean, int, long) through MAX_ADAPTIVE_TIMEOUT_MS, so clamping here to the same endpoints as the base only destroyed information: with the base at 30s the GOOD (+0), MODERATE (+2s) and LOW (+5s) branches all collapsed onto 30s, and with the base at 10s the FAST (-3s) branch collapsed up to 10s. Logged samples sat pinned at the 30s ceiling 75% of the time, which is what a degenerate ladder looks like from outside.

      Extracted from the adaptive-timeout update so that reachability of every branch across the base range is unit-testable without a router. While the arithmetic lived only inside the instance method, the sole way to test it was to reimplement it in the test -- which passes whether or not the real code clamps, so it proved nothing.

      Parameters:
      baseTimeout - the Tuner's current build request timeout in ms
      successRate - fraction of recent builds that succeeded, 0..1
      Returns:
      the base adjusted for success rate, in ms; not clamped
    • isBuildExpired

      static boolean isBuildExpired(long created, long now, long adaptiveTimeout)
      Decide whether a build sitting in the building map has escaped its deadline.

      An entry with no usable creation timestamp is expired rather than kept: there is nothing to age it against, so keeping it means it occupies a dispatch slot indefinitely. This is the predicate the expiry scan uses in place of the old inline test, which required a positive timestamp and therefore never expired such a build.

      Parameters:
      created - the first hop's creation time in ms, or a non-positive value when no caller ever stamped it
      now - current time in ms
      adaptiveTimeout - the deadline in ms this build was allowed; a non-positive value disables expiry rather than expiring everything at once
      Returns:
      true when the build must be evicted from the building map
    • applyConcurrentReduction

      static int applyConcurrentReduction(int allowed, int concurrent)
      Subtract the builds still holding a slot from the dispatch budget.

      Never returns a negative value: a caller that treated the result as a build count would dispatch none at all, and the pre-emergency path would then spin against a budget that can only recover by builds timing out.

      Parameters:
      allowed - the budget before this reduction, non-negative in practice
      concurrent - builds still in the building map after expiry
      Returns:
      the remaining budget, at least zero
    • countExpiryVeto

      private void countExpiryVeto(PooledTunnelCreatorConfig cfg)
      Record why an expiring build had previously been refused expiry.

      The two reasons the old scan could veto were a first hop with no creation timestamp and a reply id already present in the recently-building map. Both vetoes are gone, so these counts are attribution only: a non-zero value in the next window says which one had been starving the budget.

      Parameters:
      cfg - the build being evicted
    • throttledConcurrency

      static int throttledConcurrency(int current, int base)
      The concurrency ceiling for one throttle crossing: 20% off the current value, never below 60% of base.

      Scaling the current value rather than the base is what makes this a ramp. The previous form took 80% of base and clamped with 60% of base, and since 0.8x is always greater than 0.6x the clamp could never bind: the value was pinned at a single shallow step and the documented floor was unreachable. Repeated crossings now walk 96 to 76 to 60 to 57 and stop.

      Parameters:
      current - current adaptive concurrency ceiling
      base - the unthrottled configured ceiling
      Returns:
      the reduced ceiling, never above current
    • restoredConcurrency

      static int restoredConcurrency(int current, int base)
      The concurrency ceiling for one restore step: a small step toward base.

      Stepping by a sixteenth rather than a quarter keeps recovery slower than the 20% the throttle removes, so a transient spike does not put the router back at full concurrency and straight into another throttle crossing. The previous quarter-step reached base from a single 20% cut in one window, which made the throttle and the restore symmetric and the control oscillatory.

      Parameters:
      current - current adaptive concurrency ceiling
      base - the unthrottled configured ceiling
      Returns:
      the increased ceiling, never above base
    • computeAdaptiveTimeout

      static long computeAdaptiveTimeout(long baseTimeout, int length, boolean isInbound, int cpuLoad, long rttFloor)
      Scale a base build timeout for hop count, system load and direction.

      The length, load and direction terms are added to the base, then the measured RTT floor replaces the sum if it is larger, so a peer that cannot answer within its own round trip still gets its chance.

      Parameters:
      baseTimeout - starting budget in milliseconds
      length - tunnel hop count; each hop past the 3-hop baseline adds 5s
      isInbound - true for inbound pools, which skip the 8s outbound term
      cpuLoad - system load as a percentage; over 80 adds 2s, over 90 adds 3s
      rttFloor - measured RTT floor in milliseconds, overriding the sum when larger
      Returns:
      the adjusted timeout in milliseconds, capped at 45s
    • classifyExpiredHop

      static String classifyExpiredHop(boolean assigned, boolean isUs, boolean isGateway, boolean unreachable, boolean established, boolean connecting)
      Reduce one hop of an expired build to a single classification token.

      Order matters: an unreachable peer is reported as such even if a connection flag is also set, because "we already know this peer is dead" is the more actionable fact and the one that indicts selection rather than the peer.

      Parameters:
      assigned - whether a peer was assigned to this hop
      isUs - whether the assigned peer is this router
      isGateway - whether this hop is the one where we are the gateway
      unreachable - whether the peer is on record as unreachable
      established - whether the peer holds an established connection
      connecting - whether the peer is still handshaking
      Returns:
      one of the HOP_* constants, never null
    • isActionableHop

      static boolean isActionableHop(String classification)
      Whether a classification describes a hop whose state explains, or at least indicts, the expiry - as opposed to a hop that was simply the gateway slot or an ordinary peer.
      Parameters:
      classification - a token returned by classifyExpiredHop(boolean, boolean, boolean, boolean, boolean, boolean)
      Returns:
      true when the classification is actionable for diagnosis
    • shouldLogBuildExpiry

      private boolean shouldLogBuildExpiry(long now)
      Rate-limit the per-expiry report.

      Expired builds arrive at roughly twenty a minute on a degraded router, which is far too often to read individually but not often enough to summarise losslessly. One representative report a minute is enough to identify which pattern dominates; the tunnel.buildTimeout stat counts every expiry.

      Parameters:
      now - current time in milliseconds
      Returns:
      true when this expiry should be reported
    • shouldLogBudgetExhausted

      private boolean shouldLogBudgetExhausted(long now)
      Rate-limit the exhausted-budget report.

      The condition holds on every pass while the building map is full, so an unthrottled line would emit one every dispatch tick. The state is sticky enough that once a minute is more than enough to notice it, and the pass itself already logs the map size and oldest build age for context.

      Parameters:
      now - current time in milliseconds
      Returns:
      true when the exhausted budget should be reported
    • countPreEmergencyExempt

      private int countPreEmergencyExempt()
      How many in-flight builds belong to a pool that dispatched a pre-emergency burst recently. Those builds are governed by the pool's own MAX_INPROGRESS_PER_POOL_DIR cap, so the global budget must not charge itself for them.
      Returns:
      the number of exempt builds, never negative
    • reportExhaustedBudget

      private void reportExhaustedBudget(int allowed, int concurrent, int exemptInFlight, int maxKBps)
      Report a dispatch budget that nothing may start against, or note at DEBUG that builds are simply waiting on in-flight work.

      Two quite different situations produce a zero budget. In-flight above what the per-pool caps legitimately allow is abnormal and worth a warning. In-flight within the caps is correct pacing — the pools self-limit — so it is logged at DEBUG, because a WARN there reads as a fault while describing intended behaviour.

      Parameters:
      allowed - the budget after the in-flight reduction
      concurrent - builds still in the map
      exemptInFlight - builds governed by a pool's own cap
      maxKBps - the outbound bandwidth the budget was derived from
    • buildDemandFloor

      private int buildDemandFloor()
      The smallest global dispatch budget that cannot starve the per-pool caps.

      The bandwidth formula below starts from outbound KB/s and deliberately allows only ~10 concurrent builds, which was tuned to stop flooding peers. It can also land below what two pools in pre-emergency may each legitimately hold (MAX_INPROGRESS_PER_POOL_DIR per direction), which is what starved them: three events were recorded at 31-34 in flight against a budget of 18. Since the per-pool cap is already the fine-grained governor, the global budget never needs to sit below it.

      Returns:
      the minimum global dispatch budget in builds
    • logExpiredBuildWarn

      private void logExpiredBuildWarn(TunnelCreatorConfig cfg)

      Build timeouts are the largest unexplained loss on this router, roughly twice the rejection rate. This WARN reporter is throttled and carries only the identifying facts: tunnel, length, gateway position, request ID. Full per-hop attribution is available at DEBUG via logExpiredPeers(TunnelCreatorConfig).

      Parameters:
      cfg - the expired build's config
    • appendRequestMsgId

      private static void appendRequestMsgId(StringBuilder buf, TunnelCreatorConfig cfg)
      Append the build request's I2NP message ID, when known.

      This is the join key to the [MsgID ...] emitted by BuildRequestor on send. With it, an expiring build can be traced to a specific send and the "hop never answered" and "reply lost" cases become separable; without it both look identical as established, silent. Omitted entirely when unset rather than printed as 0, so a zero is never mistaken for a real message ID.

      Parameters:
      buf - the line being assembled
      cfg - the expired build's config
    • noteExpiredHopAttribution

      private void noteExpiredHopAttribution(TunnelCreatorConfig cfg)
      Record which hop position failed to answer, for every expired build.

      Counts one rate stat per (position, classification) pair, skipping ourselves and the states that are expected rather than failures, so the resulting distribution answers "where do builds die?" without relying on the rate-limited log reporter. Hop1 -- the first remote hop, since Hop0 is us -- was already the worst position in the sampled data, which is what makes the first-hop cooldown the most consequential knob.

      Parameters:
      cfg - the expired build's config
    • classifyHopAt

      private String classifyHopAt(TunnelCreatorConfig cfg, int hop)
      Classify one hop of an expired build, from that hop's current transport state. Shared by the per-hop stat in noteExpiredHopAttribution(TunnelCreatorConfig) and by the ghost-charge decision in shouldChargeGhostMark(PooledTunnelCreatorConfig, Hash) so the two can never disagree about which builds are attributable.
      Parameters:
      cfg - the expired build config
      hop - zero-based hop position
      Returns:
      one of the HOP_* constants, never null
    • logExpiredPeers

      private void logExpiredPeers(TunnelCreatorConfig cfg)
      Emit the detailed per-hop expiry report at DEBUG level.
      Parameters:
      cfg - the expired build's config
    • gatewayHopIndex

      private static int gatewayHopIndex(TunnelCreatorConfig cfg, int length)
      Config index at which the local router sits as this tunnel's gateway.

      TunnelPool.createConfig reverses the selected peer list into the config, so config index 0 is the last selected peer. ClientPeerSelector.finalizeSelection inserts the local router at the front for inbound tunnels and at the end for outbound, which puts us at the highest config index inbound and at index 0 outbound. ExploratoryPeerSelector never selects us at all, so for an exploratory build no index is ours; the returned index is still used only for labelling and any match is reported as unexpected.

      Parameters:
      cfg - the build's config
      length - the hop count
      Returns:
      the config index expected to hold the local router
    • describeAge

      private static String describeAge(long ms)
      Human-readable elapsed time for diagnostic log output.
      Parameters:
      ms - elapsed milliseconds, truncated to whole seconds or minutes
      Returns:
      a compact duration such as "412s", "8m" or "2h"
    • getRttTimeoutFloor

      private long getRttTimeoutFloor()
      Feedforward timeout floor derived from the network's recent baseline RTT (udp.sendConfirmTime, the time to send a message and receive its ACK). Returns a timeout floor of recent RTT plus a fixed margin, so build timeouts track actual network conditions instead of a fixed ceiling.
      Returns:
      timeout floor in ms
    • allowed

      private int allowed()
      Determines allowed number of concurrent tunnel builds based on system status, bandwidth limits, build times, system resources, and current tunnel build activity. Also handles expiration and cleanup of old build requests.
      Returns:
      allowed number of concurrent tunnel builds
    • getOldestBuildingConfiguration

      private PooledTunnelCreatorConfig getOldestBuildingConfiguration()
      The oldest build in the building map, by the creation time of its first hop. Used for diagnostic logging only.

      Returns the config rather than its creation time so callers can apply that build's own deadline to it. Reporting the shared base timeout beside an age is misleading: the expiry scan decides per config, adding tunnel length, outbound direction, CPU load and the RTT floor, so the deadline in force for the oldest build is routinely well above the base.

      Returns:
      the oldest config, or null when the map is empty
    • penalizeTimeout

      void penalizeTimeout(PooledTunnelCreatorConfig cfg)
      Penalize the peers of an expired build. Only the hop the build request was dispatched to — the gateway (peer 0) for inbound, the next hop (peer 1) for outbound, per BuildRequestor.getBuildRequestPeer(PooledTunnelCreatorConfig) — failed to deliver a reply; the other hops may never have received the request, or may be waiting downstream of a silent peer. Blaming them all shrinks the pool on innocent peers during network-wide no-reply events, so the profile penalty and ghost mark go to the contacted hop only. The per-tier expire stat and the didNotReply debug log stay per-hop for accounting and triage.

      Every non-self hop is additionally cooled down out of the immediate retry selection (cooldownFailedPeers(PooledTunnelCreatorConfig)), since any of them may have been the silent one.

      Parameters:
      cfg - the expired build config, non-null
    • shouldChargeGhostMark

      boolean shouldChargeGhostMark(PooledTunnelCreatorConfig cfg, Hash contacted)
      Whether an expired build's contacted hop should be charged a ghost mark.

      A ghost mark is a peer exclusion, so it should rest on evidence that indicts the peer. When the contacted hop is established but silent it is not: the hop accepted the connection, so the peer was reachable, and the missing reply is equally consistent with the request being dropped inside the peer, the reply being lost on the return path, or our own path being the problem. Charging that case excludes peers for a fault they may not own.

      It is also the dominant bucket. On a live router under stress the contacted-hop expiries split roughly 27.7/min established-silent against 4.3/min unreachable and 4.7/min merely reachable, so this single decision governed the majority of all ghost marks taken. Suppressing it stops the exclusion set growing faster than ghostMarksCleared can retire it — the observed 82/min in against 15/min out — which was steadily shrinking the pool selection draws from.

      Every other classification still charges: unreachable, handshake unfinished and plain reachable all leave the peer as the best available explanation for the expiry. The selection-scope cooldown from cooldownFailedPeers(PooledTunnelCreatorConfig) still applies either way, so an ambiguous peer is still not retried immediately.

      Parameters:
      cfg - the expired build config
      contacted - the hop the request was dispatched to
      Returns:
      true if the contacted hop should be charged a ghost mark
    • indexOfPeer

      private static int indexOfPeer(PooledTunnelCreatorConfig cfg, Hash peer)
      Zero-based position of peer in the build path, or -1 when absent.
      Parameters:
      cfg - the build config
      peer - the hash to locate
      Returns:
      the hop index, or -1
    • cooldownFailedPeers

      void cooldownFailedPeers(PooledTunnelCreatorConfig cfg)
      Cool down the contacted hop of a failed build so the immediate retry selects a different peer. Only the hop the build request was dispatched to (the gateway for inbound, the next hop for outbound) is responsible for delivering the reply — cooling the other hops punishes innocent peers and saturates the cooldown map during network-wide no-reply events (observed 247/247 client hops in cooldown on a single-destination router). Selection-scope only. Entries live for TunnelPeerSelector.PEER_SELECTION_COOLDOWN_MS and both peer selectors consult this map; profile penalties are separate and go to the contacted hop only via penalizeTimeout(PooledTunnelCreatorConfig).
      Parameters:
      cfg - the failed build config
    • run

      public void run()
      Starts the tunnel building process in a loop and takes care of error handling. Catches a specific fatal error related to Java versioning and ensures the running state is properly reset on exit.
      Specified by:
      run in interface Runnable
    • run2

      private void run2()
      Main loop that manages tunnel pools and schedules tunnel builds depending on system resource availability, load, and tunnel states. Handles expiration and waiting for new build opportunities with appropriate backoff.
    • buildZeroHopTunnels

      private int buildZeroHopTunnels(List<TunnelPool> wanted, int allowed)
      Iterate over the 0hop tunnels, running them all inline regardless of how many are allowed.
      Parameters:
      wanted - the pools wanting builds; zero-hop pools are removed once processed
      allowed - the number of builds still permitted this cycle
      Returns:
      number of tunnels allowed after processing these zero hop tunnels (almost always the same as before)
    • isRunning

      public boolean isRunning()
      Check if the executor is currently running.
      Returns:
      true if running, false otherwise
    • isPoolInBackoff

      boolean isPoolInBackoff(TunnelPool pool)
      Check if a pool is in backoff due to consecutive build failures. Uses a jittered backoff window (8-16s, centered on _poolBackoffMs) to prevent synchronized backoff where all pools are skipped in the same cycle. During collapse (0 usable tunnels), backoff is skipped entirely so collapsed pools get rebuilt immediately instead of waiting. On backoff expiry, resets the failure counter so the pool gets a fresh window of attempts.
      Parameters:
      pool - the tunnel pool to check
      Returns:
      true if the pool is in backoff
    • buildTunnel

      void buildTunnel(PooledTunnelCreatorConfig cfg)
      Build a tunnel with the given configuration. Burst builds to different peers freely, but never start a second build request to a first-hop peer that already has a build in flight — stacking requests on one peer floods it and makes the first build answer slower. Zero-hop tunnels build inline, unguarded.

      Early ban filtering: checks if the first-hop is banned before dispatching the build, preventing wasted build slots on peers that will reject the request. Saves 12-18 build slots per minute.

      Stale build pruning: skips builds that would timeout before being dispatched (queue wait + expected build time > timeout budget), reducing wasted builds that would timeout anyway.

      Parameters:
      cfg - the tunnel configuration to build
    • hasBuildInFlightToFirstHop

      private boolean hasBuildInFlightToFirstHop(PooledTunnelCreatorConfig cfg)
      Whether another build request is already in flight whose first hop (the peer the request is dispatched to) matches this config's. Prevents stacking multiple build requests onto a single peer while allowing bursts to different peers. Emergency builds bypass this via PooledTunnelCreatorConfig.isBypassPacing().

      Also skips peers that have recently failed as first hops more than _firstHopFailureThreshold times within the cooldown window.

      Parameters:
      cfg - the prospective build
      Returns:
      true if a build to the same first hop is already in flight or the peer has recently failed repeatedly as first hop
    • getFirstHopFailureCount

      private int getFirstHopFailureCount(Hash hash)
      Get the recent first-hop failure count for a peer (for logging).
      Parameters:
      hash - the peer hash
      Returns:
      the failure count, or 0 if no history
    • recordFirstHopFailure

      private void recordFirstHopFailure(Hash hash)
      Record a first-hop failure for a given peer hash. Called from buildComplete(PooledTunnelCreatorConfig, BuildExecutor.Result) when a build fails due to first-hop issues. Uses exponential decay: old failure counts decay over time, so a peer that failed once 10 minutes ago but succeeded since is not penalized.
      Parameters:
      hash - the first-hop peer identity
    • hasRecentlyFailedAsFirstHop

      private boolean hasRecentlyFailedAsFirstHop(Hash hash)
      Check whether a peer has recently failed as a first hop more than _firstHopFailureThreshold times within the cooldown window. Used by hasBuildInFlightToFirstHop(PooledTunnelCreatorConfig) to skip peers with a pattern of repeated first-hop failures.
      Parameters:
      hash - the first-hop peer identity
      Returns:
      true if the peer has exceeded the failure threshold
    • pruneFirstHopFailures

      static int pruneFirstHopFailures(Map<Hash,long[]> history, long now, long cooldown, int cap)
      Prune the first-hop failure history once it has outgrown its cap. An entry older than the cooldown is already ignored by hasRecentlyFailedAsFirstHop(), and the only place it is ever removed is that lookup - so a peer that fails once and then leaves the network is never consulted again and its entry stays for the router's uptime. Within the cap the stale entries are cheap to carry, so nothing is touched and no sweep runs on every pass.
      Parameters:
      history - the map to prune in place
      now - current time in ms
      cooldown - the first-hop failure cooldown in ms; an entry last written more than this long ago carries nothing usable
      cap - the size above which the sweep runs
      Returns:
      the number of entries removed, 0 if the map was within the cap
    • buildComplete

      public void buildComplete(PooledTunnelCreatorConfig cfg, BuildExecutor.Result result)
      Handle a completed tunnel build.
      Parameters:
      cfg - the tunnel configuration to build
      result - the build result (success, failure, etc.)
    • buildComplete

      public void buildComplete(PooledTunnelCreatorConfig cfg, BuildExecutor.Result result, String detail)
      Handle a completed tunnel build with additional detail.
      Parameters:
      cfg - the tunnel configuration that completed
      result - the build result (success, failure, etc.)
      detail - additional detail on the result
    • wasRecentlyBuilding

      public boolean wasRecentlyBuilding(long replyId)
      Check if a tunnel build was recently attempted.
      Parameters:
      replyId - the reply message ID to check
      Returns:
      true if the build was recently attempted, false otherwise
    • trimFifo

      static void trimFifo(Set<Long> ids, int max)
      Trim a FIFO id set to at most max entries, dropping the oldest (head) entries first. Insertion order is preserved by the caller.
      Parameters:
      ids - the id set, held by the caller's monitor
      max - the maximum size to keep
    • repoll

      public void repoll()
      Signal the executor to repoll for tunnel building opportunities.
    • didNotReply

      private void didNotReply(long tunnel, Hash peer)
      Log that a peer did not reply to a tunnel build request.
      Parameters:
      tunnel - reply message ID of the unanswered build request
      peer - hop that was contacted and stayed silent
    • addToBuilding

      private boolean addToBuilding(PooledTunnelCreatorConfig cfg)
      Only do this for non-fallback tunnels.
      Parameters:
      cfg - the new tunnel configuration to reserve a building slot for
      Returns:
      true if refused because of a duplicate key
    • removeFromBuilding

      PooledTunnelCreatorConfig removeFromBuilding(long id)
      This returns the PTCC up to a minute after it 'expired', thus allowing us to still use a tunnel if it was accepted, and to update peer stats. This means that manager.buildComplete(PooledTunnelCreatorConfig, BuildExecutor.Result) could be called more than once, and a build can be failed or successful after it was timed out, which will affect the stats and profiles. But that's ok. A peer that rejects slowly gets penalized twice, for example.
      Parameters:
      id - the build message ID
      Returns:
      ptcc or null
    • calculatePairedBuilds

      private void calculatePairedBuilds(List<TunnelPool> pools, List<TunnelPool> wanted)
      Graduated-urgency replenishment - replaces simple deficit-based approach. Uses expiry-window urgency multipliers from mainline I2P: 1x at 210-270s, 2x at 150-210s, 4x at 90-150s, 6x at <90s. This ensures tunnels are rebuilt predictively before they expire, rather than only reactively after failure.
      Parameters:
      pools - all pools examined this cycle
      wanted - the pools wanting builds; pools whose urgency warrants a build are added here
    • collectPairTargets

      static Map<Hash,int[]> collectPairTargets(List<TunnelPool> pools)
      Collect per-direction build quantity targets for paired destinations, used by the proportional per-direction cap in calculatePairedBuilds(). For each destination the inbound and outbound quantities are the maxima of the quantities configured on the pools for that direction, so a pair with asymmetric quantities (e.g. 4 inbound / 2 outbound) splits the build budget proportionally instead of 50/50.
      Parameters:
      pools - pools to scan; dead pools and pools without a destination are skipped
      Returns:
      destination -> int[2] {inboundQty, outboundQty}, never null
    • computeUrgencyScore

      static int computeUrgencyScore(int usableCount, int target)
      Urgency score used to sort pools before build allocation, so a collapsed or near-collapse pool earlier in the list cannot consume build slots (or trigger the proportional cap) before an urgent pool later in the list gets any. Scores are tiered far apart on purpose: 0 usable tunnels (1 << 20) outrank 1-2 usable (1 << 16) which outrank any numerical deficit, so the sort order is stable regardless of configured quantities.
      Parameters:
      usableCount - current usable tunnel count of the pool
      target - minimum desired tunnel count (already clamped to >= 2)
      Returns:
      urgency score; larger sorts earlier
    • countExpiryBuckets

      static BuildExecutor.ExpiryBuckets countExpiryBuckets(List<TunnelInfo> tunnels, long now)
      Count a pool's tunnels into cumulative expiry-window buckets. Tunnels at or below one window boundary fall into that window (and no later one), so expire30s is "expires within 30s", expire90s is "within 90s but not 30s", and so on. ExpireLater covers everything beyond 330s. Each bucket has a GOOD-only twin (good status and at most 1 consecutive failure) used for proactive replacement, plus aggregates: zero-hop fallback count, GOOD count and their total latency.

      Length-1 tunnels always land in fallbackCount and never fill the expiry buckets, so pools that don't explicitly request zero-hop tunnels keep building replacements until the fallback is gone.

      Parameters:
      tunnels - tunnels of one pool, never null
      now - current time, used to compute time-to-expiry
      Returns:
      bucket counts, never null