mirror of
https://github.com/ZmnSCPxj/clboss.git
synced 2026-08-14 12:43:19 +02:00
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.
216 lines
6.4 KiB
C++
216 lines
6.4 KiB
C++
#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)) { }
|
|
|
|
}}
|