- All Implemented Interfaces:
Runnable
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.
Nested Class Summary
Nested ClassesModifier and TypeClassDescription(package private) static classImmutable result ofcountExpiryBuckets(List, long).(package private) static enumBuild result enumeration.Field Summary
FieldsModifier and TypeFieldDescriptionprivate longCurrent adaptive first hop timeout.private intCurrent limit on concurrent builds.private longCurrent adaptive build timeout.private final AtomicIntegerFailed builds counted in the window.private final byte[]Ring of recent build results.private final AtomicIntegerSuccessful builds counted in the window.private final AtomicIntegerTimed-out builds counted in the window.private static doubleBuild success ratio needed to fully restore concurrency.private static doubleBuild success ratio at which concurrency may start restoring.private static doubleAdaptive concurrency throttle thresholds.private static intPer-pool consecutive build failure tracking for backoff.private final RouterContextThe router context.private final ObjectNotify lock for the building map.private final ConcurrentHashMap<Long, PooledTunnelCreatorConfig> Tunnels currently being built, by reply id.private static longMaximum age (ms) for first-hop failure history entries.private final ConcurrentHashMap<Hash, long[]> First-hop failure history: tracks recent first-hop failures to avoid repeatedly selecting peers that have recently failed as first hops.private static intNumber of failures within_firstHopCooldownMsrequired to skip a peer as first hop.private final byte[]Sliding window of recent first-hop outcomes, the same shape as_buildResultsbut with its own membership, since the first-hop timeout counts a different set of results than the build timeout does.private final AtomicIntegerWrite index into the first hop window.private final GhostPeerManagerTracks peers whose builds keep failing.private booleanTrue while the executor thread runs.private static final AtomicLongWhen budget exhaustion was last logged.private static final AtomicLongWhen build expiry was last logged.private longWhen the last build pass started.private final ConcurrentHashMap<TunnelPool, Long> Throttle for peer-selection hot path: BldExecutor was pegging at 98% inClientPeerSelector.selectSingleHop → IBGWExcluder.contains even after endpoint caching, because the tight loop called pool.configureNewTunnel() for every wanted pool without per-pool spacing.private final ConcurrentHashMap<Hash, Long> Last dispatch time per first-hop peer, for staggering builds.private longWhen the keepalive pass last ran.private final ConcurrentHashMap<TunnelPool, Long> When each pool last rebuilt its tunnels.private final LogLogger for tunnel builds.private final TunnelPoolManagerThe tunnel pool manager we build for.private static intThe maximum number of concurrent tunnel builds allowed.private static longCurrent backoff between pool build passes.private final ConcurrentHashMap<TunnelPool, long[]> Per-pool failure tracking state.Build ids recently sent, for duplicate replies.private final ConcurrentHashMap<Long, PooledTunnelCreatorConfig> Recently finished builds, by reply id.private booleanTrue to force another build pass.private static intStale build pruning threshold fraction.private doubleAdaptive concurrency throttle: tracks the timeout rate and adjusts maxConcurrentBuilds dynamically.private static longThe GOOD deficit throttle interval from config or default (30s).private static intThe target build buffer from config or default (0).private intConcurrency ceiling for the current dispatch pass: the adaptive ceiling, lifted when expiry-urgent pools cannot be served at it.private final AtomicIntegerWrite index into the result ring.(package private) static final intHow many recorded builds pass before the adaptive timeout is recalculated from the window.private static final longInterval between build budget log lines.private static final longInterval between emitted build-expiry reports.private static final longMinimum interval between pool reconfigures.private static final intCPU load percentage above which the +3s load term applies.private static final intCPU load percentage above which the +2s load term applies.(package private) static final Comparator<Object[]> Dispatch comparator for snapshottedObject[3]rows whererow[0]is the score (Integer),row[1]the pool androw[2]the zero-hop emergency flag (Boolean): emergency replacements go first regardless of deficit, then higher score first.private static final intScore per near-expiry tunnel.private static final longReduction applied when the network is fast @since 0.9.71+.private static final byte_firstHopResultsentry for a first hop that did not.private static final intBound on the first-hop failure history before expired entries are pruned.private static final byte_firstHopResultsentry for a first hop that replied.(package private) static final StringThe peer holds an established connection but did not answer the build request.(package private) static final StringThe peer never finished a handshake, so it never had a session to answer over.(package private) static final StringHop was never assigned a peer.(package private) static final StringThe peer is reachable and has been heard from; nothing distinguishes it.(package private) static final StringThis router sits at the gateway index it is expected to occupy.(package private) static final StringThis router sits at a hop index other than the gateway slot - never legitimate.(package private) static final StringThe peer is on record as unreachable: selection admitted a peer we had already written off, so the eligibility filter failed to fire.private static final intBit marking a window entry as an inbound build.private static final longkeepAlive() cadence in the executor loop (see run2()).private static final intHop count at or below which no length adjustment applies @since 0.9.71+.private static final longMs added per extra hop beyond the 3-hop baseline @since 0.9.71+.private static final intInterval between executor passes.private static final longIncrease applied at low success driven by rejects rather than timeouts @since 0.9.71+.private static final longSafety ceiling for any computed adaptive build timeout, regardless of tunnel length, direction, system load, or measured RTT.private static final intAbsolute ceiling on concurrent builds.private static final intTunnels near expiry at which the urgency bonus saturates.private static final intMaximum concurrent in-flight builds per pool per direction (inbound or outbound).private static final intMinimum time between consecutive build passes.private static final longIncrease applied at moderate success @since 0.9.71+.private static final longMs added to the base timeout for the longer outbound reply path @since 0.9.71+.private static final long[]Rate stat periods for the build statistics.private static final intMask selecting the result ordinal out of a packed entry.private static final Comparator<Object[]> Comparator for snapshottedObject[2]rows whererow[0]is the score (Integer) androw[1]is the pool.private static final intHow many pools the global dispatch budget must be able to serve at the same time.(package private) static final longBuild completion time above which a build counts as slow.private static final intBound on the first-hop stagger map before dead entries are pruned.private static final doubleSuccess rate above which the network is answering fast enough to shave time off @since 0.9.71+.private static final doubleSuccess rate above which the base timeout is left alone @since 0.9.71+.private static final doubleSuccess rate above which a modest timeout increase applies @since 0.9.71+.private static final doubleFraction of the current concurrency removed on each throttle crossing @since 0.9.71+.private static final doubleFraction of base concurrency the throttle may not go below @since 0.9.71+.private static final intBase concurrency is restored in 1/THROTTLE_RESTORE_DIVISOR steps @since 0.9.71+.private static final intPools considered per build pass.private static final intSliding window of recent build results for smooth rate calculation.Constructor Summary
ConstructorsConstructorDescriptionBuildExecutor(RouterContext ctx, TunnelPoolManager mgr, GhostPeerManager ghostMgr) Create a new BuildExecutor.Method Summary
Modifier and TypeMethodDescription(package private) static longadaptiveTimeoutDelta(double successRate) The adjustment to add to the base build timeout, from the success rate alone.private booleanOnly do this for non-fallback tunnels.private intallowed()Determines allowed number of concurrent tunnel builds based on system status, bandwidth limits, build times, system resources, and current tunnel build activity.private static voidAppend the build request's I2NP message ID, when known.(package private) static intapplyConcurrentReduction(int allowed, int concurrent) Subtract the builds still holding a slot from the dispatch budget.voidbuildComplete(PooledTunnelCreatorConfig cfg, BuildExecutor.Result result) Handle a completed tunnel build.voidbuildComplete(PooledTunnelCreatorConfig cfg, BuildExecutor.Result result, String detail) Handle a completed tunnel build with additional detail.private intThe smallest global dispatch budget that cannot starve the per-pool caps.(package private) static StringbuildDirectionStat(boolean inbound, String event) Name of a per-direction build outcome stat, so build success can be compared inbound against outbound.(package private) static StringWhich outcome bucket a build result belongs to.(package private) voidBuild a tunnel with the given configuration.private intbuildZeroHopTunnels(List<TunnelPool> wanted, int allowed) Iterate over the 0hop tunnels, running them all inline regardless of how many are allowed.private longCalculate adaptive timeout based on tunnel characteristics and network conditions.private longcalculateAdaptiveTimeout(PooledTunnelCreatorConfig cfg, int cpuLoad, long rttFloor) Cached-variant ofcalculateAdaptiveTimeout(PooledTunnelCreatorConfig).private voidCalculate adaptive timeouts based on recorded build outcomes.private voidcalculatePairedBuilds(List<TunnelPool> pools, List<TunnelPool> wanted) Graduated-urgency replenishment - replaces simple deficit-based approach.(package private) static intcalculatePerIterationCap(double timeoutRate) Calculate the per-iteration build cap from the current timeout rate.(package private) static StringclassifyExpiredHop(boolean assigned, boolean isUs, boolean isGateway, boolean unreachable, boolean established, boolean connecting) Reduce one hop of an expired build to a single classification token.private StringclassifyHopAt(TunnelCreatorConfig cfg, int hop) Classify one hop of an expired build, from that hop's current transport state.collectPairTargets(List<TunnelPool> pools) Collect per-direction build quantity targets for paired destinations, used by the proportional per-direction cap in calculatePairedBuilds().(package private) static longcombineAdaptiveTimeout(long baseTimeout, double successRate) Combine the Tuner's base timeout with the success-rate adjustment.(package private) static longcomputeAdaptiveTimeout(long baseTimeout, int length, boolean isInbound, int cpuLoad, long rttFloor) Scale a base build timeout for hop count, system load and direction.(package private) static intcomputeUrgencyScore(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.(package private) voidCool down the contacted hop of a failed build so the immediate retry selects a different peer.(package private) static BuildExecutor.ExpiryBucketscountExpiryBuckets(List<TunnelInfo> tunnels, long now) Count a pool's tunnels into cumulative expiry-window buckets.private static intcountExpiryUrgent(List<TunnelPool> pools) Counts wanting pools whose charge is expiry urgency rather than deficit.private voidRecord why an expiring build had previously been refused expiry.private intHow many in-flight builds belong to a pool that dispatched a pre-emergency burst recently.(package private) static booleanPool-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).private static StringdescribeAge(long ms) Human-readable elapsed time for diagnostic log output.private voiddidNotReply(long tunnel, Hash peer) Log that a peer did not reply to a tunnel build request.(package private) static inteffectiveTarget(RouterContext ctx, TunnelPoolSettings s, boolean keepConfiguredQuantity) Effective tunnel-count target a pool builds toward.(package private) static intelevateForUrgent(int base, int urgent, int inProgress) Elevated ceiling when urgent demand cannot be served at the current one.(package private) static intfirstHopSuccessCount(byte[] results, int count) How many of the firstcountfirst-hop window entries were successes.(package private) static intfirstHopWindowCount(long writeIndex) How many first-hop outcomes the window holds for a given write index.booleanCheck if full statistics are enabled.private static intgatewayHopIndex(TunnelCreatorConfig cfg, int length) Config index at which the local router sits as this tunnel's gateway.(package private) intThe adaptive maximum concurrent builds, adjusted based on the current timeout rate.static intThe concurrency restore threshold as a percentage (0-100).static intThe concurrency throttle threshold as a percentage (0-100).static longThe first-hop failure cooldown in milliseconds.private intgetFirstHopFailureCount(Hash hash) Get the recent first-hop failure count for a peer (for logging).static intThe first-hop failure threshold count.(package private) intPackage-visible for tests: returns the successes among the first-hop outcomes currently in the window.(package private) intPackage-visible for tests: returns how many first-hop outcomes are in the window.static longThe GOOD deficit throttle interval from config or default (30s).private static longReturns the startup grace period for builds.static intThe maximum number of concurrent builds allowed.private PooledTunnelCreatorConfigThe oldest build in the building map, by the creation time of its first hop.private long[]Failure tracking state for a pool, created if absent.static longThe pool backoff time in milliseconds.static intThe pool failure threshold.private longFeedforward timeout floor derived from the network's recent baseline RTT (udp.sendConfirmTime, the time to send a message and receive its ACK).static intThe stale build pruning threshold percentage.(package private) doubleThe current timeout rate (0.0-1.0) for adaptive throttling.(package private) static intThe tunnel lifetime from config, delegated to TunnelPool.static intThe target build buffer from config or default (0).private static intReturns the configured minimum tunnel target.(package private) intThe concurrency ceiling in force for the current dispatch pass, the adaptive one lifted for expiry-urgent pools.(package private) intPackage-visible for tests: returns how many results are in the window.private booleanWhether another build request is already in flight whose first hop (the peer the request is dispatched to) matches this config's.private booleanCheck whether a peer has recently failed as a first hop more than_firstHopFailureThresholdtimes within the cooldown window.private static intindexOfPeer(PooledTunnelCreatorConfig cfg, Hash peer) Zero-based position ofpeerin the build path, or -1 when absent.(package private) static booleanisActionableHop(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.(package private) static booleanisBuildExpired(long created, long now, long adaptiveTimeout) Decide whether a build sitting in the building map has escaped its deadline.(package private) booleanisPoolInBackoff(TunnelPool pool) Check if a pool is in backoff due to consecutive build failures.booleanCheck if the executor is currently running.(package private) static booleanisZeroHopEmergency(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.private longJittered backoff: randomize within ±33% of_poolBackoffMsto prevent synchronized backoff where all pools are skipped in the same cycle.private voidBuild timeouts are the largest unexplained loss on this router, roughly twice the rejection rate.private voidEmit the detailed per-hop expiry report at DEBUG level.private voidRecord which hop position failed to answer, for every expired build.(package private) voidPenalize the peers of an expired build.(package private) static intpruneFirstHopFailures(Map<Hash, long[]> history, long now, long cooldown, int cap) Prune the first-hop failure history once it has outgrown its cap.(package private) static booleanrecalcAdaptiveTimeout(int seq) Whether the build numbered seq should recalculate the adaptive timeout.private voidRecord one build outcome against the direction being built.private voidrecordFirstHopFailure(Hash hash) Record a first-hop failure for a given peer hash.(package private) voidrecordFirstHopOutcome(boolean success) Records whether the first hop of a build replied, in the window the adaptive first-hop timeout is computed from.(package private) intrefreshUrgentCeiling(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.(package private) PooledTunnelCreatorConfigremoveFromBuilding(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.(package private) voidremovePoolState(TunnelPool pool) Remove failure state for a pool that is being removed.voidrepoll()Signal the executor to repoll for tunnel building opportunities.private voidreportExhaustedBudget(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.voidrestart()Restart the build executor, clearing recent build state.(package private) static intrestoredConcurrency(int current, int base) The concurrency ceiling for one restore step: a small step toward base.voidrun()Starts the tunnel building process in a loop and takes care of error handling.private voidrun2()Main loop that manages tunnel pools and schedules tunnel builds depending on system resource availability, load, and tunnel states.(package private) static intscore(int active, int target, int pairedActive, int nearExpiry) Pure form of the dispatch score, so the ordering can be pinned without building pools.private static intscore(TunnelPool p) Priority score for build urgency.static voidsetConcurrencyThrottleThresholdPct(int pct) Set the concurrency throttle threshold (called by Tuner).static voidsetFirstHopFailureCooldownMs(long ms) Set the first-hop failure cooldown (called by Tuner).static voidsetFirstHopFailureThreshold(int count) Set the first-hop failure threshold (called by Tuner).static voidsetGoodDeficitThrottle(long ms) Set the GOOD deficit throttle interval (called by Tuner).static voidsetMaxConcurrentBuilds(int val) The maximum number of concurrent builds allowed.static voidsetPoolBackoffMs(long val) The pool backoff time in milliseconds.static voidsetPoolFailureThreshold(int val) The pool failure threshold.static voidsetStaleBuildThresholdPct(int val) Set the stale build pruning threshold (called by Tuner).static voidsetTunnelTargetBuffer(int count) Set the target build buffer (called by Tuner).(package private) booleanshouldChargeGhostMark(PooledTunnelCreatorConfig cfg, Hash contacted) Whether an expired build's contacted hop should be charged a ghost mark.private booleanshouldLogBudgetExhausted(long now) Rate-limit the exhausted-budget report.private booleanshouldLogBuildExpiry(long now) Rate-limit the per-expiry report.voidshutdown()Cannot be restarted.(package private) static longspacingDelay(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.(package private) static intthrottledConcurrency(int current, int base) The concurrency ceiling for one throttle crossing: 20% off the current value, never below 60% of base.(package private) static voidTrim a FIFO id set to at mostmaxentries, dropping the oldest (head) entries first.private voidupdateBuildStats(BuildExecutor.Result result, boolean inbound) Update success/failure/timeout counters and calculate adaptive timeout.booleanwasRecentlyBuilding(long replyId) Check if a tunnel build was recently attempted.(package private) static bytewindowEntry(BuildExecutor.Result result, boolean inbound) Pack a build outcome and its direction into one window entry.(package private) static booleanwindowInbound(byte entry) Whether a window entry is an inbound build.(package private) static BuildExecutor.ResultwindowResult(byte entry) The outcome recorded in a window entry.(package private) static intwindowResultCount(byte[] results, int count, BuildExecutor.Result want) How many of the firstcountwindow entries recordedwantas their outcome.
Field Details
MAX_BUILD_CONCURRENCY
private static final int MAX_BUILD_CONCURRENCYAbsolute ceiling on concurrent builds. Bounds elevation.- See Also:
SIMULTANEOUS_COLLAPSED_POOLS
private static final int SIMULTANEOUS_COLLAPSED_POOLSHow many pools the global dispatch budget must be able to serve at the same time. Two pools can both reach theirMAX_INPROGRESS_PER_POOL_DIRcap at once, and the budget must not be below what that costs.- See Also:
MAX_EXPIRY_BONUS
private static final int MAX_EXPIRY_BONUSTunnels 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_TUNNELScore 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
Comparator for snapshottedObject[2]rows whererow[0]is the score (Integer) androw[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
Dispatch comparator for snapshottedObject[3]rows whererow[0]is the score (Integer),row[1]the pool androw[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
_context
The router context._log
Logger for tunnel builds._manager
The tunnel pool manager we build for._ghostPeerManager
Tracks peers whose builds keep failing._currentlyBuilding
Notify lock for the building map._currentlyBuildingMap
Tunnels currently being built, by reply id._recentlyBuildingMap
Recently finished builds, by reply id._isRunning
private volatile boolean _isRunningTrue while the executor thread runs._repoll
private boolean _repollTrue to force another build pass._lastBuildPassTime
private long _lastBuildPassTimeWhen the last build pass started._buildSuccessCount
Successful builds counted in the window._buildFailureCount
Failed builds counted in the window._lastRebuildTime
When each pool last rebuilt its tunnels._lastConfigureTime
Throttle for peer-selection hot path: BldExecutor was pegging at 98% inClientPeerSelector.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 inshutdown()/removePoolState() so the map cannot leak.CONFIGURE_THROTTLE_MS
private static final long CONFIGURE_THROTTLE_MSMinimum interval between pool reconfigures.- See Also:
_buildTimeoutCount
Timed-out builds counted in the window._firstHopResults
private final byte[] _firstHopResultsSliding window of recent first-hop outcomes, the same shape as_buildResultsbut 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
Write index into the first hop window.FIRST_HOP_SUCCESS
private static final byte FIRST_HOP_SUCCESS_firstHopResultsentry for a first hop that replied.- See Also:
FIRST_HOP_FAILURE
private static final byte FIRST_HOP_FAILURE_firstHopResultsentry for a first hop that did not.- See Also:
WINDOW_SIZE
private static final int WINDOW_SIZESliding 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[] _buildResultsRing of recent build results._windowWriteIndex
Write index into the result ring.ADAPTIVE_TIMEOUT_RECALC_BUILDS
static final int ADAPTIVE_TIMEOUT_RECALC_BUILDSHow 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 _timeoutRateAdaptive 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 _adaptiveMaxConcurrentBuildsCurrent limit on concurrent builds._urgentMaxConcurrentBuilds
private volatile int _urgentMaxConcurrentBuildsConcurrency 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
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_MAXBound 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
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_MAXBound on the first-hop stagger map before dead entries are pruned.- See Also:
_firstHopCooldownMs
private static volatile long _firstHopCooldownMsMaximum age (ms) for first-hop failure history entries. Entries older than this are ignored during lookup. Tunable viaTuner._firstHopFailureThreshold
private static volatile int _firstHopFailureThresholdNumber of failures within_firstHopCooldownMsrequired 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 _staleBuildThresholdPctStale 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 viaTuner.MAX_PER_POOL_DIR
private static final int MAX_PER_POOL_DIRMaximum 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 _consecutiveFailureThresholdPer-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 _poolBackoffMsCurrent backoff between pool build passes._poolFailureState
Per-pool failure tracking state._lastKeepAliveTime
private long _lastKeepAliveTimeWhen the keepalive pass last ran._adaptiveTimeout
private volatile long _adaptiveTimeoutCurrent adaptive build timeout._adaptiveFirstHopTimeout
private volatile long _adaptiveFirstHopTimeoutCurrent adaptive first hop timeout._tunedTargetBuffer
private static volatile int _tunedTargetBufferThe target build buffer from config or default (0). Extra tunnels to maintain beyond the configured quantity. Tunable viai2p.tunnel.targetBuffer(default: 0). Tuned value overrides config, when set by the Tuner._tunedGoodDeficitThrottle
private static volatile long _tunedGoodDeficitThrottleThe GOOD deficit throttle interval from config or default (30s). Minimum time between GOOD-tunnel deficit rebuilds for non-critical pools. Tunable viai2p.tunnel.goodDeficitThrottle(default: 30000). Tuned value overrides config, when set by the Tuner._maxConcurrentBuilds
private static volatile int _maxConcurrentBuildsThe maximum number of concurrent tunnel builds allowed. Calculated based on CPU cores and configurable multiplier.LOOP_TIME
MIN_BUILD_SPACING_MS
private static final int MIN_BUILD_SPACING_MSMinimum 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
KEEPALIVE_INTERVAL_MS
private static final long KEEPALIVE_INTERVAL_MSkeepAlive() cadence in the executor loop (see run2()).- See Also:
RATES
private static final long[] RATESRate stat periods for the build statistics.INBOUND_BIT
private static final int INBOUND_BITBit 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.Resulthas nine values, so ordinals 0-8 leave the top bit free.- See Also:
RESULT_MASK
private static final int RESULT_MASKMask selecting the result ordinal out of a packed entry.An explicit mask rather than
~INBOUND_BIT: the complement of0x80keeps every high bit set, and applying it to the sign-extendedbyteleaves bit 7 in place, so an inbound entry indexedBuildExecutor.Result.values()at a negative ordinal and threwArrayIndexOutOfBoundsExceptionon the build path.- See Also:
_concurrencyThrottleThreshold
private static double _concurrencyThrottleThresholdAdaptive concurrency throttle thresholds. When timeout rate exceedsTHROTTLE_THRESHOLD, concurrent builds are reduced to prevent overwhelming the IB reply path. When timeout rate drops belowRESTORE_THRESHOLDand success exceedsRESTORE_SUCCESS_THRESHOLD, concurrency is restored._concurrencyRestoreThreshold
private static double _concurrencyRestoreThresholdBuild success ratio at which concurrency may start restoring._concurrencyRestoreSuccessThreshold
private static double _concurrencyRestoreSuccessThresholdBuild success ratio needed to fully restore concurrency.MAX_ADAPTIVE_TIMEOUT_MS
private static final long MAX_ADAPTIVE_TIMEOUT_MSSafety 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_MSMs added per extra hop beyond the 3-hop baseline @since 0.9.71+.- See Also:
LENGTH_BASELINE
private static final int LENGTH_BASELINEHop count at or below which no length adjustment applies @since 0.9.71+.- See Also:
SLOW_BUILD_MS
static final long SLOW_BUILD_MSBuild 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 mirrortunnel.buildClientSuccessand 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_HIGHCPU load percentage above which the +2s load term applies. @since 0.9.71+.- See Also:
CPU_LOAD_CRITICAL
private static final int CPU_LOAD_CRITICALCPU load percentage above which the +3s load term applies. @since 0.9.71+.- See Also:
OUTBOUND_EXTRA_MS
private static final long OUTBOUND_EXTRA_MSMs added to the base timeout for the longer outbound reply path @since 0.9.71+.- See Also:
SUCCESS_FAST
private static final double SUCCESS_FASTSuccess 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_GOODSuccess rate above which the base timeout is left alone @since 0.9.71+.- See Also:
SUCCESS_MODERATE
private static final double SUCCESS_MODERATESuccess 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_MSReduction applied when the network is fast @since 0.9.71+.- See Also:
MODERATE_RECOVERY_MS
private static final long MODERATE_RECOVERY_MSIncrease applied at moderate success @since 0.9.71+.- See Also:
LOW_SUCCESS_RECOVERY_MS
private static final long LOW_SUCCESS_RECOVERY_MSIncrease applied at low success driven by rejects rather than timeouts @since 0.9.71+.- See Also:
THROTTLE_FACTOR
private static final double THROTTLE_FACTORFraction of the current concurrency removed on each throttle crossing @since 0.9.71+.- See Also:
THROTTLE_FLOOR_RATIO
private static final double THROTTLE_FLOOR_RATIOFraction of base concurrency the throttle may not go below @since 0.9.71+.- See Also:
THROTTLE_RESTORE_DIVISOR
private static final int THROTTLE_RESTORE_DIVISORBase 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_MSInterval between emitted build-expiry reports.- See Also:
BUDGET_EXHAUSTED_LOG_INTERVAL_MS
private static final long BUDGET_EXHAUSTED_LOG_INTERVAL_MSInterval between build budget log lines.- See Also:
_lastBuildExpiryLog
When build expiry was last logged._lastBudgetExhaustedLog
When budget exhaustion was last logged.HOP_NEVER_ASSIGNED
HOP_SELF_EXPECTED
This router sits at the gateway index it is expected to occupy.- See Also:
HOP_SELF_UNEXPECTED
This router sits at a hop index other than the gateway slot - never legitimate.- See Also:
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
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
The peer never finished a handshake, so it never had a session to answer over.- See Also:
HOP_REACHABLE
The peer is reachable and has been heard from; nothing distinguishes it.- See Also:
Constructor Details
BuildExecutor
Create a new BuildExecutor.- Parameters:
ctx- the router contextmgr- the tunnel pool managerghostMgr- the ghost peer manager for tracking timeouts
Method Details
getTunnelTargetMin
Returns the configured minimum tunnel target.- Parameters:
ctx- the router context- Returns:
- the minimum target count
score
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 byDISPATCH_COMPARATORahead 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
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 forceurgent- pools holding leases about to expireinProgress- builds already in flight- Returns:
- the ceiling to use, which may equal
base
refreshUrgentCeiling
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:
- collapsed (no usable tunnel) — a pool with no service at all;
- near-collapse (1-2 usable);
- leases about to expire;
- deficit against target;
- 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_BONUStunnels, 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 pooltarget- the pool's target quantity, already floored at 2pairedActive- usable tunnels in the paired pool, for balancenearExpiry- live tunnels inside the pre-build window- Returns:
- the score, higher builds sooner
isZeroHopEmergency
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 inrun2(). 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
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 buffers- settings of the pool in questionkeepConfiguredQuantity- 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_poolBackoffMsto 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
The tunnel lifetime from config, delegated to TunnelPool.- Parameters:
ctx- the router context- Returns:
- the tunnel lifetime in milliseconds
getTunnelTargetBuffer
The target build buffer from config or default (0). Extra tunnels to maintain beyond the configured quantity. Tunable viai2p.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
The GOOD deficit throttle interval from config or default (30s). Minimum time between GOOD-tunnel deficit rebuilds for non-critical pools. Tunable viai2p.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
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 msnow- the current time in ms- Returns:
- ms still to wait, 0 if the floor is met
getGracePeriod
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
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
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
Name of a per-direction build outcome stat, so build success can be compared inbound against outbound. The aggregatetunnel.buildSuccessRatecannot answer that: it mixes both directions, and they do not fail for the same reasons.- Parameters:
inbound- true for an inbound tunnel buildevent- Attempted, Succeeded, Failed, TimedOut or SuccessRate- Returns:
- the stat name
recordBuildDirection
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 nullresult- the outcome, bucketed bybuildOutcomeEvent(Result)
buildOutcomeEvent
Which outcome bucket a build result belongs to. Everything that is not a success or a timeout counts as failed, so a newly addedBuildExecutor.Resultis reported rather than silently dropped.- Parameters:
result- the build outcome- Returns:
- the event suffix, e.g. Succeeded
windowEntry
Pack a build outcome and its direction into one window entry.Pure decision — no state, safe for unit tests.
- Parameters:
result- the build outcomeinbound- whether the build was inbound- Returns:
- the packed window entry
windowResult
The outcome recorded in a window entry.- Parameters:
entry- a packed entry fromwindowEntry(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 fromwindowEntry(BuildExecutor.Result, boolean)- Returns:
- true if the build was inbound
windowResultCount
How many of the firstcountwindow entries recordedwantas 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 firstcount- how many leading entries to examine; entries past the array end are ignored, so a count larger than the array is safewant- 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 firstcountfirst-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 firstcount- 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
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 outcomeinbound- 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_BUILDSbuilds
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
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
Cached-variant ofcalculateAdaptiveTimeout(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 andgetRttTimeoutFloor()reads a rate statistic, so both are stable across a single scan.- Parameters:
cfg- the tunnel configurationcpuLoad- cached value ofSystemVersion.getCPULoadAvg()rttFloor- cached value ofgetRttTimeoutFloor()- 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)throughMAX_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 mssuccessRate- 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 itnow- current time in msadaptiveTimeout- 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 practiceconcurrent- builds still in the building map after expiry- Returns:
- the remaining budget, at least zero
countExpiryVeto
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 ceilingbase- 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 ceilingbase- 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 millisecondslength- tunnel hop count; each hop past the 3-hop baseline adds 5sisInbound- true for inbound pools, which skip the 8s outbound termcpuLoad- system load as a percentage; over 80 adds 2s, over 90 adds 3srttFloor- 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 hopisUs- whether the assigned peer is this routerisGateway- whether this hop is the one where we are the gatewayunreachable- whether the peer is on record as unreachableestablished- whether the peer holds an established connectionconnecting- whether the peer is still handshaking- Returns:
- one of the
HOP_*constants, never null
isActionableHop
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 byclassifyExpiredHop(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.buildTimeoutstat 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 ownMAX_INPROGRESS_PER_POOL_DIRcap, 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 reductionconcurrent- builds still in the mapexemptInFlight- builds governed by a pool's own capmaxKBps- 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_DIRper 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
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
Append the build request's I2NP message ID, when known.This is the join key to the
[MsgID ...]emitted byBuildRequestoron 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 asestablished, 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 assembledcfg- the expired build's config
noteExpiredHopAttribution
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
Classify one hop of an expired build, from that hop's current transport state. Shared by the per-hop stat innoteExpiredHopAttribution(TunnelCreatorConfig)and by the ghost-charge decision inshouldChargeGhostMark(PooledTunnelCreatorConfig, Hash)so the two can never disagree about which builds are attributable.- Parameters:
cfg- the expired build confighop- zero-based hop position- Returns:
- one of the
HOP_*constants, never null
logExpiredPeers
Emit the detailed per-hop expiry report at DEBUG level.- Parameters:
cfg- the expired build's config
gatewayHopIndex
Config index at which the local router sits as this tunnel's gateway.TunnelPool.createConfigreverses the selected peer list into the config, so config index 0 is the last selected peer.ClientPeerSelector.finalizeSelectioninserts 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.ExploratoryPeerSelectornever 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 configlength- the hop count- Returns:
- the config index expected to hold the local router
describeAge
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
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
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, perBuildRequestor.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
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 silentit 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
ghostMarksClearedcan 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 configcontacted- the hop the request was dispatched to- Returns:
- true if the contacted hop should be charged a ghost mark
indexOfPeer
Zero-based position ofpeerin the build path, or -1 when absent.- Parameters:
cfg- the build configpeer- the hash to locate- Returns:
- the hop index, or -1
cooldownFailedPeers
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 forTunnelPeerSelector.PEER_SELECTION_COOLDOWN_MSand both peer selectors consult this map; profile penalties are separate and go to the contacted hop only viapenalizeTimeout(PooledTunnelCreatorConfig).- Parameters:
cfg- the failed build config
run
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
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 processedallowed- 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
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
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
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 viaPooledTunnelCreatorConfig.isBypassPacing().Also skips peers that have recently failed as first hops more than
_firstHopFailureThresholdtimes 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
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
Record a first-hop failure for a given peer hash. Called frombuildComplete(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
Check whether a peer has recently failed as a first hop more than_firstHopFailureThresholdtimes within the cooldown window. Used byhasBuildInFlightToFirstHop(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
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 placenow- current time in mscooldown- the first-hop failure cooldown in ms; an entry last written more than this long ago carries nothing usablecap- the size above which the sweep runs- Returns:
- the number of entries removed, 0 if the map was within the cap
buildComplete
Handle a completed tunnel build.- Parameters:
cfg- the tunnel configuration to buildresult- 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 completedresult- 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
repoll
public void repoll()Signal the executor to repoll for tunnel building opportunities.didNotReply
Log that a peer did not reply to a tunnel build request.- Parameters:
tunnel- reply message ID of the unanswered build requestpeer- hop that was contacted and stayed silent
addToBuilding
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
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
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 cyclewanted- the pools wanting builds; pools whose urgency warrants a build are added here
collectPairTargets
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 pooltarget- minimum desired tunnel count (already clamped to >= 2)- Returns:
- urgency score; larger sorts earlier
countExpiryBuckets
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 nullnow- current time, used to compute time-to-expiry- Returns:
- bucket counts, never null