FundsMover: fast-fail rebalance attempts the destination fee already prices out
Some checks are pending
Code Base Sanity Check / tests (push) Waiting to run
Code Base Sanity Check / coverage (push) Waiting to run
Code Base Sanity Check / build-clang (push) Waiting to run

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.
This commit is contained in:
Ken Sedgwick 2026-06-24 12:52:58 -07:00
parent 37434046ef
commit 17ac085ef1
No known key found for this signature in database
GPG key ID: DBD2AF0849D711A9

View file

@ -468,6 +468,38 @@ private:
});
}
Ev::Io<void> 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.