From 17ac085ef1fd436786ebc6d943462a2c8684058e Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Wed, 24 Jun 2026 12:52:58 -0700 Subject: [PATCH] FundsMover: fast-fail rebalance attempts the destination fee already prices out When the destination channel's own last-hop fee meets or exceeds the prorated move budget, askrene's maxfee_msat clamps to 0 and no route can fit -- yet askrene still grinds the flow about a second to reach the same 205, and the Runner then splits and repeats that roughly 30 times. That futile splitting is the bulk of the rebalance storm. Detect the clamp locally in getroute (it mirrors the maxfee_msat computation already in the getroutes branch) and skip the doomed call. Smaller splits only make it worse -- the prorated budget shrinks toward zero while the destination base fee does not -- so every split re-enters and fast-fails with no RPC, and the move finishes in milliseconds with zero askrene load instead of ~14 seconds of saturation. The verdict is still recorded via Msg::FundsMoverUnaffordable, identical to the getroutes 205/206 path, so detecting it locally does not change what the Unaffordable memory sees. On prod1 the storming sinks were 100% maxfee=0 (the destination fee ate the budget), so this removes essentially the whole storm at its root; the Unaffordable cache remains for the distinct, source-specific maxfee>0 over-budget case. --- Boss/Mod/FundsMover/Attempter.cpp | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Boss/Mod/FundsMover/Attempter.cpp b/Boss/Mod/FundsMover/Attempter.cpp index ddaa025..6d953d4 100644 --- a/Boss/Mod/FundsMover/Attempter.cpp +++ b/Boss/Mod/FundsMover/Attempter.cpp @@ -468,6 +468,38 @@ private: }); } Ev::Io getroute() { + /* FAST-FAIL: if the destination's last-hop fee alone meets + * or exceeds our prorated budget, the maxfee_msat computed + * below clamps to 0 and no route can fit. askrene still + * spends ~1s grinding the flow to the same 205, and the + * Runner then splits and repeats that ~30 times -- the bulk + * of the rebalance storm. Smaller splits only make it worse + * (the budget shrinks toward 0 while the dest base fee does + * not), so every split re-enters here and fast-fails with no + * RPC. Detect the clamp locally and skip the doomed call. + * + * Record the verdict via Msg::FundsMoverUnaffordable, the + * same as the getroutes 205/206 path, so how we detected it + * does not change what the Unaffordable memory sees. The + * three lines below mirror the maxfee_msat computation in the + * getroutes branch; keep them in sync. + */ + { + auto prorata = amount / *remaining_amount; + auto prorated_fee_budget = *fee_budget * prorata; + auto dest_hop_fee = dest_amount - amount; + if (!(prorated_fee_budget > dest_hop_fee)) + return Boss::log( bus, Debug + , "FundsMover[%s]: dest hop fee " + ">= budget; skipping getroutes " + "(no route can fit)." + , attempt_tag().c_str() + ) + + bus.raise(Msg::FundsMoverUnaffordable{ + source, destination, amount, + orig_budget * (amount / orig_amount) + }); + } return Ev::yield().then([this]() { /* Prorate our share of the global fee budget and * pass it to askrene as a hard maxfee_msat cap.