mirror of
https://github.com/ZmnSCPxj/clboss.git
synced 2026-08-13 12:33:20 +02:00
Add AskreneVolatileLayer: a wiped layer so disable_node/channel_update blocks can heal
askrene-age only trims timestamped inform-channel constraints; it never removes disabled_nodes or channel_update overrides, which carry no timestamp. So the blocks the rebalancers write accumulate forever and a node or channel can never recover -- a disabled peer stays unroutable for the life of the layer. Separate those blocks from the aged constraints into their own shared, non-persistent layer, clboss-volatile, wiped wholesale on a timer (default 3h, clboss-volatile-layer-wipe-secs, dynamic). Blocks re-accumulate from fresh failures, so a recovered node or channel becomes routable again within one wipe interval. The constraint layers (clboss / clboss-xrebalance) are untouched and still aged by askrene-age. The new AskreneVolatileLayer module owns the layer: it creates it (non-persistent) at startup and removes+recreates it once the interval elapses on a TimerRandomHourly tick. A getroutes landing in the brief wipe gap just misses the blocks for that one call -- benign and self-correcting. Non-persistent means a CLN restart is itself a free wipe. Classic wiring (FundsMover/Attempter): the two block writes -- disable_node for NODE-level failures and update_channel for policy overrides -- now go to clboss-volatile, which is added to the classic getroutes layers array. The inform-channel constraints stay in clboss. The classic self-exclude also stays in clboss; the shared volatile layer carries no self entry, since xrebalance must never disable self for its circular routing. The xrebalance engine (XMoveFunds) is intentionally not redirected here; that is a follow-on change.
This commit is contained in:
parent
55b8f67ede
commit
6f4eccf0b2
7 changed files with 304 additions and 15 deletions
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
namespace Boss { namespace Mod { namespace AskreneLayer {
|
||||
|
||||
std::string const clboss_volatile_layer_name = "clboss-volatile";
|
||||
std::string const clboss_layer_name = "clboss";
|
||||
|
||||
namespace {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,24 @@ namespace Boss { namespace Mod { class Rpc; } }
|
|||
|
||||
namespace Boss { namespace Mod { namespace AskreneLayer {
|
||||
|
||||
/* Name of the shared, non-persistent askrene layer that holds the
|
||||
* "blocks" both rebalancers learn -- node disables (NODE-level
|
||||
* failures) and channel_update policy overrides. These are not
|
||||
* timestamped and askrene-age never removes them, so unlike the
|
||||
* inform-channel constraints they cannot heal by aging. Instead this
|
||||
* layer is wiped wholesale on a timer (see Boss::Mod::AskreneVolatileLayer)
|
||||
* and re-accumulates from fresh failures, so a recovered node/channel
|
||||
* becomes routable again within one wipe interval.
|
||||
*
|
||||
* Shared by both modes because a down node / a forwarder's policy is a
|
||||
* mode-independent fact; both classic and xrebalance getroutes include
|
||||
* this layer alongside their per-mode constraint layer. Deliberately
|
||||
* carries NO self_id entry -- the classic self-exclude lives in
|
||||
* clboss_layer_name (xrebalance must never disable self for its circular
|
||||
* routing).
|
||||
*/
|
||||
extern std::string const clboss_volatile_layer_name;
|
||||
|
||||
/* Name of the persistent askrene layer that CLBOSS subsystems
|
||||
* write failure-feedback and (optionally) success-observations
|
||||
* into. Following the xpay convention -- the layer is named
|
||||
|
|
|
|||
216
Boss/Mod/AskreneVolatileLayer.cpp
Normal file
216
Boss/Mod/AskreneVolatileLayer.cpp
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
#include"Boss/Mod/AskreneVolatileLayer.hpp"
|
||||
#include"Boss/Mod/AskreneLayer.hpp"
|
||||
#include"Boss/Mod/Rpc.hpp"
|
||||
#include"Boss/Msg/Init.hpp"
|
||||
#include"Boss/Msg/ManifestOption.hpp"
|
||||
#include"Boss/Msg/Manifestation.hpp"
|
||||
#include"Boss/Msg/Option.hpp"
|
||||
#include"Boss/Msg/OptionType.hpp"
|
||||
#include"Boss/Msg/TimerRandomHourly.hpp"
|
||||
#include"Boss/concurrent.hpp"
|
||||
#include"Boss/log.hpp"
|
||||
#include"Ev/Io.hpp"
|
||||
#include"Ev/now.hpp"
|
||||
#include"Jsmn/Object.hpp"
|
||||
#include"Json/Out.hpp"
|
||||
#include"S/Bus.hpp"
|
||||
#include"Util/make_unique.hpp"
|
||||
#include"Util/stringify.hpp"
|
||||
#include<cinttypes>
|
||||
|
||||
namespace {
|
||||
|
||||
/* Default seconds between wholesale wipes of the clboss-volatile layer. */
|
||||
auto constexpr default_wipe_secs = std::uint64_t(10800); /* 3h */
|
||||
|
||||
}
|
||||
|
||||
namespace Boss { namespace Mod {
|
||||
|
||||
class AskreneVolatileLayer::Impl {
|
||||
private:
|
||||
S::Bus& bus;
|
||||
Boss::Mod::Rpc* rpc;
|
||||
|
||||
/* Seconds between wipes; dynamic via clboss-volatile-layer-wipe-secs. */
|
||||
std::uint64_t wipe_secs;
|
||||
/* Ev::now() at the last (re)create. The first wipe happens one
|
||||
* interval after startup, so the freshly-created layer is given time
|
||||
* to accumulate before being wiped. */
|
||||
double last_wipe;
|
||||
|
||||
void start() {
|
||||
bus.subscribe<Msg::Init>([this](Msg::Init const& init) {
|
||||
rpc = &init.rpc;
|
||||
last_wipe = Ev::now();
|
||||
return Boss::concurrent(create_layer());
|
||||
});
|
||||
|
||||
bus.subscribe<Msg::Manifestation
|
||||
>([this](Msg::Manifestation const&) {
|
||||
return bus.raise(Msg::ManifestOption{
|
||||
"clboss-volatile-layer-wipe-secs",
|
||||
Msg::OptionType_Int,
|
||||
Json::Out::direct(wipe_secs),
|
||||
"Seconds between wholesale wipes of the shared "
|
||||
"clboss-volatile askrene layer (the never-aged "
|
||||
"node disables and channel-update overrides that "
|
||||
"the rebalancers write). Once the interval "
|
||||
"elapses the layer is removed and recreated on a "
|
||||
"TimerRandomHourly tick, so blocks re-accumulate "
|
||||
"from fresh failures and a recovered node or "
|
||||
"channel becomes routable again within one "
|
||||
"interval. Dynamic: settable at runtime via "
|
||||
"`lightning-cli setconfig "
|
||||
"clboss-volatile-layer-wipe-secs <secs>`. "
|
||||
"Default 10800 (3h).",
|
||||
/* dynamic = */ true
|
||||
});
|
||||
});
|
||||
bus.subscribe<Msg::Option>([this](Msg::Option const& o) {
|
||||
if (o.name != "clboss-volatile-layer-wipe-secs")
|
||||
return Ev::lift();
|
||||
/* Number at startup, string via setconfig -- the same
|
||||
* dual encoding the other dynamic options handle.
|
||||
* Signed so a negative value is rejected below rather
|
||||
* than wrapping to a huge unsigned. */
|
||||
long long secs = 0;
|
||||
try {
|
||||
if (o.value.is_number()) {
|
||||
secs = static_cast<long long>(double(o.value));
|
||||
} else if (o.value.is_string()) {
|
||||
secs = std::stoll(std::string(o.value));
|
||||
} else {
|
||||
return Boss::log( bus, Warn
|
||||
, "AskreneVolatileLayer: "
|
||||
"clboss-volatile-layer-wipe-"
|
||||
"secs: unsupported value "
|
||||
"type; keeping %" PRIu64 "."
|
||||
, wipe_secs
|
||||
);
|
||||
}
|
||||
} catch (std::exception const& e) {
|
||||
return Boss::log( bus, Warn
|
||||
, "AskreneVolatileLayer: "
|
||||
"clboss-volatile-layer-wipe-secs: "
|
||||
"parse error '%s'; keeping "
|
||||
"%" PRIu64 "."
|
||||
, e.what()
|
||||
, wipe_secs
|
||||
);
|
||||
}
|
||||
if (secs <= 0) {
|
||||
return Boss::log( bus, Warn
|
||||
, "AskreneVolatileLayer: "
|
||||
"clboss-volatile-layer-wipe-secs: "
|
||||
"must be > 0; keeping %" PRIu64 "."
|
||||
, wipe_secs
|
||||
);
|
||||
}
|
||||
wipe_secs = std::uint64_t(secs);
|
||||
return Boss::log( bus, Info
|
||||
, "AskreneVolatileLayer: wipe interval "
|
||||
"set to %" PRIu64 "s."
|
||||
, wipe_secs
|
||||
);
|
||||
});
|
||||
|
||||
bus.subscribe<Msg::TimerRandomHourly
|
||||
>([this](Msg::TimerRandomHourly const&) {
|
||||
if (!rpc)
|
||||
return Ev::lift();
|
||||
auto now = Ev::now();
|
||||
if (now - last_wipe < double(wipe_secs))
|
||||
return Ev::lift();
|
||||
last_wipe = now;
|
||||
return Boss::concurrent(wipe_layer());
|
||||
});
|
||||
}
|
||||
|
||||
/* (Re)create the non-persistent volatile layer. Non-fatal on
|
||||
* RpcError: on CLN < v24.11 (no askrene) the layer simply does not
|
||||
* exist and the rebalancers' block-writes silently no-op. */
|
||||
Ev::Io<void> create_layer() {
|
||||
auto parms = Json::Out()
|
||||
.start_object()
|
||||
.field( "layer"
|
||||
, Boss::Mod::AskreneLayer::clboss_volatile_layer_name
|
||||
)
|
||||
.field("persistent", false)
|
||||
.end_object()
|
||||
;
|
||||
return rpc->command( "askrene-create-layer"
|
||||
, std::move(parms)
|
||||
).then([](Jsmn::Object) {
|
||||
return Ev::lift();
|
||||
}).catching<RpcError>([this](RpcError const& e) {
|
||||
auto code = int(0);
|
||||
if (e.error.has("code") && e.error["code"].is_number())
|
||||
code = int(double(e.error["code"]));
|
||||
auto is_method_missing = (code == -32601);
|
||||
return Boss::log( bus
|
||||
, is_method_missing ? Debug : Warn
|
||||
, "AskreneVolatileLayer: "
|
||||
"askrene-create-layer (%s) failed: "
|
||||
"%s%s"
|
||||
, Boss::Mod::AskreneLayer::clboss_volatile_layer_name
|
||||
.c_str()
|
||||
, Util::stringify(e.error).c_str()
|
||||
, is_method_missing
|
||||
? " (RPC missing; volatile layer "
|
||||
"unavailable on this CLN)."
|
||||
: " (unexpected)."
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/* Wipe = remove + recreate. Single fixed-name layer, so a getroutes
|
||||
* landing in the brief gap simply misses the blocks for that one call
|
||||
* -- benign and self-correcting (the next failure re-records them).
|
||||
* The remove is allowed to fail (the layer may not exist yet). */
|
||||
Ev::Io<void> wipe_layer() {
|
||||
auto parms = Json::Out()
|
||||
.start_object()
|
||||
.field( "layer"
|
||||
, Boss::Mod::AskreneLayer::clboss_volatile_layer_name
|
||||
)
|
||||
.end_object()
|
||||
;
|
||||
return rpc->command( "askrene-remove-layer"
|
||||
, std::move(parms)
|
||||
).then([](Jsmn::Object) {
|
||||
return Ev::lift();
|
||||
}).catching<RpcError>([](RpcError const&) {
|
||||
/* Layer absent (first wipe, or a prior create failed);
|
||||
* the recreate below establishes it regardless. */
|
||||
return Ev::lift();
|
||||
}).then([this]() {
|
||||
return create_layer();
|
||||
}).then([this]() {
|
||||
return Boss::log( bus, Debug
|
||||
, "AskreneVolatileLayer: wiped %s "
|
||||
"(blocks re-accumulate from fresh "
|
||||
"failures)."
|
||||
, Boss::Mod::AskreneLayer::clboss_volatile_layer_name
|
||||
.c_str()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public:
|
||||
explicit
|
||||
Impl(S::Bus& bus_
|
||||
) : bus(bus_)
|
||||
, rpc(nullptr)
|
||||
, wipe_secs(default_wipe_secs)
|
||||
, last_wipe(0.0)
|
||||
{ start(); }
|
||||
};
|
||||
|
||||
AskreneVolatileLayer::AskreneVolatileLayer(AskreneVolatileLayer&&) =default;
|
||||
AskreneVolatileLayer::~AskreneVolatileLayer() =default;
|
||||
|
||||
AskreneVolatileLayer::AskreneVolatileLayer(S::Bus& bus)
|
||||
: pimpl(Util::make_unique<Impl>(bus)) { }
|
||||
|
||||
}}
|
||||
43
Boss/Mod/AskreneVolatileLayer.hpp
Normal file
43
Boss/Mod/AskreneVolatileLayer.hpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#ifndef BOSS_MOD_ASKRENEVOLATILELAYER_HPP
|
||||
#define BOSS_MOD_ASKRENEVOLATILELAYER_HPP
|
||||
|
||||
#include<memory>
|
||||
|
||||
namespace S { class Bus; }
|
||||
|
||||
namespace Boss { namespace Mod {
|
||||
|
||||
/** class Boss::Mod::AskreneVolatileLayer
|
||||
*
|
||||
* @brief owns the lifecycle of the shared, non-persistent
|
||||
* `clboss-volatile` askrene layer: creates it at startup and wipes it
|
||||
* (remove + recreate) on a periodic timer.
|
||||
*
|
||||
* @desc the volatile layer holds the "blocks" both rebalancers learn --
|
||||
* node disables and channel_update overrides. askrene-age never removes
|
||||
* those (they carry no aging timestamp), so without this they would
|
||||
* accumulate forever and a node/channel could never heal. Wiping the
|
||||
* whole layer on a cadence lets the blocks re-accumulate from fresh
|
||||
* failures, so a recovered node becomes routable again within one wipe
|
||||
* interval. Cadence is clboss-volatile-layer-wipe-secs (dynamic; default
|
||||
* 3h). The layer is non-persistent, so a CLN restart is itself a free
|
||||
* wipe.
|
||||
*/
|
||||
class AskreneVolatileLayer {
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> pimpl;
|
||||
|
||||
public:
|
||||
AskreneVolatileLayer() =delete;
|
||||
|
||||
AskreneVolatileLayer(AskreneVolatileLayer&&);
|
||||
~AskreneVolatileLayer();
|
||||
|
||||
explicit
|
||||
AskreneVolatileLayer(S::Bus&);
|
||||
};
|
||||
|
||||
}}
|
||||
|
||||
#endif /* !defined(BOSS_MOD_ASKRENEVOLATILELAYER_HPP) */
|
||||
|
|
@ -545,6 +545,7 @@ private:
|
|||
* to cover that plus its real fee.
|
||||
*/
|
||||
la.entry(Boss::Mod::AskreneLayer::clboss_layer_name);
|
||||
la.entry(Boss::Mod::AskreneLayer::clboss_volatile_layer_name);
|
||||
la.end_array();
|
||||
obj.field("maxfee_msat", route_maxfee.to_msat());
|
||||
obj.field("final_cltv", cltv_delta + 14);
|
||||
|
|
@ -1339,22 +1340,24 @@ private:
|
|||
;
|
||||
/* 0x2000 == NODE level error. */
|
||||
if ((fail & 0x2000)) {
|
||||
/* Persistent disable_node is correct
|
||||
* for NODE-level failures and is also
|
||||
* consulted by this Attempter's own
|
||||
* subsequent getroutes calls (the
|
||||
* clboss layer is in the layers
|
||||
* array), so no separate transient
|
||||
* write is needed for this case.
|
||||
/* disable_node goes to the shared
|
||||
* clboss-volatile layer: it is a block
|
||||
* (askrene-age never removes it), so it
|
||||
* lives in the wiped layer where it can
|
||||
* heal rather than accumulating forever.
|
||||
* That layer is also in this Attempter's
|
||||
* own getroutes layers array, so the
|
||||
* disable steers subsequent routes with
|
||||
* no separate transient write needed.
|
||||
*/
|
||||
feedback = Boss::Mod::AskreneLayer::disable_node(
|
||||
rpc,
|
||||
Boss::Mod::AskreneLayer::clboss_layer_name,
|
||||
Boss::Mod::AskreneLayer::clboss_volatile_layer_name,
|
||||
enode
|
||||
)
|
||||
+ Boss::log( bus, Debug
|
||||
, "FundsMover[%s]: feedback: "
|
||||
"disable_node %s on clboss"
|
||||
"disable_node %s on clboss-volatile"
|
||||
, attempt_tag().c_str()
|
||||
, std::string(enode).c_str()
|
||||
);
|
||||
|
|
@ -1560,16 +1563,20 @@ private:
|
|||
* channel_update. Cache it
|
||||
* for apply_policy_overrides
|
||||
* on the next retry, and
|
||||
* mirror to the clboss
|
||||
* layer (for other CLBOSS
|
||||
* subsystems that consult
|
||||
* the layer).
|
||||
* mirror to the shared
|
||||
* clboss-volatile layer: a
|
||||
* policy override is a block
|
||||
* (askrene-age never removes
|
||||
* it), so it lives in the
|
||||
* wiped layer where it can
|
||||
* heal, and is consulted by
|
||||
* both modes' getroutes.
|
||||
*/
|
||||
policy_overrides[key] = cu;
|
||||
feedback = std::move(feedback)
|
||||
+ Boss::Mod::AskreneLayer::update_channel(
|
||||
rpc,
|
||||
Boss::Mod::AskreneLayer::clboss_layer_name,
|
||||
Boss::Mod::AskreneLayer::clboss_volatile_layer_name,
|
||||
echan,
|
||||
std::uint32_t(edir),
|
||||
cu.enabled,
|
||||
|
|
@ -1581,7 +1588,7 @@ private:
|
|||
)
|
||||
+ Boss::log( bus, Debug
|
||||
, "FundsMover[%s]: "
|
||||
"feedback: clboss "
|
||||
"feedback: volatile "
|
||||
"update_channel %s/%d "
|
||||
"enabled=%d "
|
||||
"base=%umsat prop=%uppm "
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include"Boss/Mod/ActiveProber.hpp"
|
||||
#include"Boss/Mod/AmountSettingsHandler.hpp"
|
||||
#include"Boss/Mod/AskreneVolatileLayer.hpp"
|
||||
#include"Boss/Mod/AutoDisconnector.hpp"
|
||||
#include"Boss/Mod/AvailableRpcCommandsAnnouncer.hpp"
|
||||
#include"Boss/Mod/BlockTracker.hpp"
|
||||
|
|
@ -209,6 +210,7 @@ std::shared_ptr<void> all( std::ostream& cout
|
|||
|
||||
/* Channel balancing. */
|
||||
all->install<RebalanceModeManager>(bus);
|
||||
all->install<AskreneVolatileLayer>(bus);
|
||||
all->install<FundsMover::Main>(bus);
|
||||
all->install<MoveFundsCommand>(bus);
|
||||
all->install<EarningsTracker>(bus);
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ libclboss_la_SOURCES = \
|
|||
Boss/Mod/AmountSettingsHandler.hpp \
|
||||
Boss/Mod/AskreneLayer.cpp \
|
||||
Boss/Mod/AskreneLayer.hpp \
|
||||
Boss/Mod/AskreneVolatileLayer.cpp \
|
||||
Boss/Mod/AskreneVolatileLayer.hpp \
|
||||
Boss/Mod/AutoDisconnector.cpp \
|
||||
Boss/Mod/AutoDisconnector.hpp \
|
||||
Boss/Mod/AvailableRpcCommandsAnnouncer.cpp \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue