diff --git a/Boss/Mod/PeerMetrician.cpp b/Boss/Mod/PeerMetrician.cpp new file mode 100644 index 0000000..3f47137 --- /dev/null +++ b/Boss/Mod/PeerMetrician.cpp @@ -0,0 +1,212 @@ +#include"Boss/Mod/PeerMetrician.hpp" +#include"Boss/ModG/ReqResp.hpp" +#include"Boss/Msg/RequestPeerMetrics.hpp" +#include"Boss/Msg/RequestPeerStatistics.hpp" +#include"Boss/Msg/ResponsePeerMetrics.hpp" +#include"Boss/Msg/ResponsePeerStatistics.hpp" +#include"Boss/Msg/ProvideStatus.hpp" +#include"Boss/Msg/SolicitStatus.hpp" +#include"Ev/Io.hpp" +#include"Ev/map.hpp" +#include"Ev/now.hpp" +#include"Util/make_unique.hpp" +#include +#include + +namespace Boss { namespace Mod { + +class PeerMetrician::Impl { +private: + S::Bus& bus; + ModG::ReqResp< Msg::RequestPeerStatistics + , Msg::ResponsePeerStatistics + > statistician; + +public: + Impl() =delete; + Impl(Impl&&) =delete; + Impl(Impl const&) =delete; + + Impl(S::Bus& bus_ + ) : bus(bus_) + , statistician( bus_ + , [](Msg::RequestPeerStatistics& m, void* p) { + m.requester = p; + } + , [](Msg::ResponsePeerStatistics& m) { + return m.requester; + } + ) + { + start(); + } + +private: + void start() { + bus.subscribe([this](Msg::RequestPeerMetrics const& m) { + return run(m); + }); + bus.subscribe([this](Msg::SolicitStatus const& m) { + return Ev::lift().then([this]() { + return get_metrics(); + }).then([this](Msg::ResponsePeerMetrics m) { + return bus.raise(Msg::ProvideStatus{ + "peer_metrics", provide_metrics(m) + }); + }); + }); + } + Ev::Io> + get_stats(double start, double end) { + return statistician.execute(Msg::RequestPeerStatistics{ + nullptr, start, end + }).then([this](Msg::ResponsePeerStatistics resp) { + return Ev::lift(std::move(resp.statistics)); + }); + } + static + std::map + map_stats_to_mets(std::map< Ln::NodeId + , Msg::PeerStatistics + > const& stats) { + auto mets = std::map(); + std::transform( stats.begin(), stats.end() + , std::inserter(mets, mets.begin()) + , [](std::pair< Ln::NodeId + , Msg::PeerStatistics + > const& entry) { + auto mets = stats_to_mets(entry.second); + return std::make_pair(entry.first, std::move(mets)); + }); + return mets; + } + static + Msg::PeerMetrics + stats_to_mets(Msg::PeerStatistics const& stats) { + auto mets = Msg::PeerMetrics(); + auto time = stats.end_time - stats.start_time; + mets.age = stats.age; + mets.seconds_per_attempt = stats.lockrealtime + / double(stats.attempts) + ; + if (stats.attempts > 0) { + mets.success_per_attempt = Util::make_unique(); + *mets.success_per_attempt = double(stats.successes) + / double(stats.attempts) + ; + } + mets.success_per_day = (double(stats.successes) * 86400) + / time + ; + if (stats.connect_checks > 0) { + mets.connect_rate = Util::make_unique(); + *mets.connect_rate = double(stats.connects) + / double(stats.connect_checks) + ; + } + mets.in_fee_msat_per_day = ( double(stats.in_fee.to_msat()) + * 86400 + ) + / time + ; + mets.out_fee_msat_per_day = ( double(stats.out_fee.to_msat()) + * 86400 + ) + / time + ; + return mets; + } + Ev::Io + get_metrics() { + return Ev::lift().then([this]() { + auto end = Ev::now(); + auto f = [this, end](double time) { + auto start = end - time; + return get_stats(start, end); + }; + auto static const times = std::vector{ + 86400 * 3, + 86400 * 14, + 86400 * 30, + }; + return Ev::map(std::move(f), times); + }).then([ this + ](std::vector> stats) { + auto mets = std::vector>(stats.size()); + std::transform( stats.begin(), stats.end() + , mets.begin() + , &map_stats_to_mets + ); + return Ev::lift(Msg::ResponsePeerMetrics{ + nullptr, + std::move(mets[0]), + std::move(mets[1]), + std::move(mets[2]) + }); + }); + } + + Ev::Io run(Msg::RequestPeerMetrics const& req) { + return get_metrics().then([this, req + ](Msg::ResponsePeerMetrics resp) { + resp.requester = req.requester; + return bus.raise(std::move(resp)); + }); + } + + Json::Out provide_metrics(Msg::ResponsePeerMetrics const& resp) { + auto const& data = resp.day3; + + auto out = Json::Out(); + auto obj = out.start_object(); + + for (auto& e : data) { + auto& mets = e.second; + auto mets_j = Json::Out(); + auto mets_o = mets_j.start_object(); + + mets_o.field("age", mets.age); + mets_o.field( "seconds_per_attempt" + , mets.seconds_per_attempt + ); + mets_o.field( "success_per_attempt" + , mets.success_per_attempt + ); + mets_o.field( "success_per_day" + , mets.success_per_day + ); + mets_o.field( "connect_rate" + , mets.connect_rate + ); + mets_o.field( "in_fee_msat_per_day" + , mets.in_fee_msat_per_day + ); + mets_o.field( "out_fee_msat_per_day" + , mets.out_fee_msat_per_day + ); + mets_o.end_object(); + + auto& node = e.first; + obj.field( std::string(node) + , std::move(mets_j) + ); + } + + obj.end_object(); + return out; + } +}; + +PeerMetrician::PeerMetrician(PeerMetrician&&) =default; +PeerMetrician::~PeerMetrician() =default; + +PeerMetrician::PeerMetrician(S::Bus& bus) + : pimpl(Util::make_unique(bus)) { } + +}} diff --git a/Boss/Mod/PeerMetrician.hpp b/Boss/Mod/PeerMetrician.hpp new file mode 100644 index 0000000..3237a3e --- /dev/null +++ b/Boss/Mod/PeerMetrician.hpp @@ -0,0 +1,35 @@ +#ifndef BOSS_MOD_PEERMETRICIAN_HPP +#define BOSS_MOD_PEERMETRICIAN_HPP + +#include + +namespace S { class Bus; } + +namespace Boss { namespace Mod { + +/** class Boss::Mod::PeerMetrician + * + * @brief This is not a poet. + * Instead, this queries the peer statistician for raw + * data, then generates metrics we might find useful + * for judging if we should close channels to the peer + * or not. + */ +class PeerMetrician { +private: + class Impl; + std::unique_ptr pimpl; + +public: + PeerMetrician() =delete; + + PeerMetrician(PeerMetrician&&); + ~PeerMetrician(); + + explicit + PeerMetrician(S::Bus& bus); +}; + +}} + +#endif /* !defined(BOSS_MOD_PEERMETRICIAN_HPP) */ diff --git a/Boss/Mod/all.cpp b/Boss/Mod/all.cpp index 7e0e351..68e8a4f 100644 --- a/Boss/Mod/all.cpp +++ b/Boss/Mod/all.cpp @@ -35,6 +35,7 @@ #include"Boss/Mod/OnchainFeeMonitor.hpp" #include"Boss/Mod/OnchainFundsAnnouncer.hpp" #include"Boss/Mod/PeerCompetitorFeeMonitor/Main.hpp" +#include"Boss/Mod/PeerMetrician.hpp" #include"Boss/Mod/PeerStatistician.hpp" #include"Boss/Mod/Reconnector.hpp" #include"Boss/Mod/SendpayResultMonitor.hpp" @@ -145,6 +146,7 @@ std::shared_ptr all( std::ostream& cout all->install(bus); all->install(bus); all->install(bus); + all->install(bus); return all; } diff --git a/Boss/Msg/PeerMetrics.hpp b/Boss/Msg/PeerMetrics.hpp new file mode 100644 index 0000000..8cc634e --- /dev/null +++ b/Boss/Msg/PeerMetrics.hpp @@ -0,0 +1,56 @@ +#ifndef BOSS_MSG_PEERMETRICS_HPP +#define BOSS_MSG_PEERMETRICS_HPP + +#include + +namespace Boss { namespace Msg { + +/** struct Boss::Msg::PeerMetrics + * + * @brief this is not an emitted message, but is instead used + * in `Boss::Msg::ResponsePeerMetrics` to store the metrics + * of each peer. + */ +struct PeerMetrics { + /* Age of our channel with this peer, in seconds. */ + double age; + /* Average amount of time our payment gets locked by this peer + * before it gets either failed or succeeded. + * 0 if we never attempted with this peer. + */ + double seconds_per_attempt; + /* Rate at which an outgoing payment via this peer reaches the + * destination. + * 1.0 = 100%. + * nullptr if we have never attempted this peer within the + * time frame of this peer metric. + */ + std::unique_ptr success_per_attempt; + /* Average number of times we reached the destination when + * sending out via this peer, per day. + * 0 if no successes with this peer. + */ + double success_per_day; + /* Rate at which we saw the peer connected in `listpeers`. + * 1.0 = 100%. + * nullptr if we have not made any connectivity checks in + * the time frame of this peer metric. + */ + std::unique_ptr connect_rate; + /* Fees we earned from incoming forwards from this peer, + * divided by time frame. + * Unit is millisatoshi per day. + * 0 if we never had incoming forwards from this peer in + * this time frame. + */ + double in_fee_msat_per_day; + /* Fees we earned from outgoing forwards to this peer, + * divided by time frame. + */ + double out_fee_msat_per_day; + +}; + +}} + +#endif /* !defined(BOSS_MSG_PEERMETRICS_HPP) */ diff --git a/Boss/Msg/RequestPeerMetrics.hpp b/Boss/Msg/RequestPeerMetrics.hpp new file mode 100644 index 0000000..c8f7c10 --- /dev/null +++ b/Boss/Msg/RequestPeerMetrics.hpp @@ -0,0 +1,21 @@ +#ifndef BOSS_MSG_REQUESTPEERMETRICS_HPP +#define BOSS_MSG_REQUESTPEERMETRICS_HPP + +namespace Boss { namespace Msg { + +/** struct Boss::Msg::RequestPeerMetrics + * + * @brief emit this in order to request peer metrics. + */ +struct RequestPeerMetrics { + /* Used to identify the object requesting + * for peer metrics. + * Will be copied in the corresponding + * `Boss::Msg::ResponsePeerMetrics`. + */ + void* requester; +}; + +}} + +#endif /* !defined(BOSS_MSG_REQUESTPEERMETRICS_HPP) */ diff --git a/Boss/Msg/ResponsePeerMetrics.hpp b/Boss/Msg/ResponsePeerMetrics.hpp new file mode 100644 index 0000000..3e9863f --- /dev/null +++ b/Boss/Msg/ResponsePeerMetrics.hpp @@ -0,0 +1,29 @@ +#ifndef BOSS_MSG_RESPONSEPEERMETRICS_HPP +#define BOSS_MSG_RESPONSEPEERMETRICS_HPP + +#include"Boss/Msg/PeerMetrics.hpp" +#include"Ln/NodeId.hpp" +#include + +namespace Boss { namespace Msg { + +/** struct Boss::Msg::ResponsePeerMetrics + * + * @brief eventually emitted in response to a + * `Boss::Msg::RequestPeerMetrics` with the + * `requester` field copied. + * + * @desc contains metrics from recorded statistical + * data for the last 3 days, 2 weeks, and month. + */ +struct ResponsePeerMetrics { + void* requester; + + std::map day3; + std::map weeks2; + std::map month1; +}; + +}} + +#endif /* !defined(BOSS_MSG_RESPONSEPEERMETRICS_HPP) */ diff --git a/Makefile.am b/Makefile.am index 36f5df4..ceb2efd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -152,6 +152,8 @@ libclboss_la_SOURCES = \ Boss/Mod/PeerCompetitorFeeMonitor/Main.hpp \ Boss/Mod/PeerCompetitorFeeMonitor/Surveyor.cpp \ Boss/Mod/PeerCompetitorFeeMonitor/Surveyor.hpp \ + Boss/Mod/PeerMetrician.cpp \ + Boss/Mod/PeerMetrician.hpp \ Boss/Mod/PeerStatistician.cpp \ Boss/Mod/PeerStatistician.hpp \ Boss/Mod/Reconnector.cpp \ @@ -204,6 +206,7 @@ libclboss_la_SOURCES = \ Boss/Msg/PatronizeChannelCandidate.hpp \ Boss/Msg/PayInvoice.hpp \ Boss/Msg/PeerMedianChannelFee.hpp \ + Boss/Msg/PeerMetrics.hpp \ Boss/Msg/PeerStatistics.hpp \ Boss/Msg/PreinvestigateChannelCandidates.hpp \ Boss/Msg/ProposeChannelCandidates.hpp \ @@ -218,10 +221,12 @@ libclboss_la_SOURCES = \ Boss/Msg/RequestConnect.hpp \ Boss/Msg/RequestListpays.hpp \ Boss/Msg/RequestNewaddr.hpp \ + Boss/Msg/RequestPeerMetrics.hpp \ Boss/Msg/RequestPeerStatistics.hpp \ Boss/Msg/ResponseConnect.hpp \ Boss/Msg/ResponseListpays.hpp \ Boss/Msg/ResponseNewaddr.hpp \ + Boss/Msg/ResponsePeerMetrics.hpp \ Boss/Msg/ResponsePeerStatistics.hpp \ Boss/Msg/SendpayResult.hpp \ Boss/Msg/SetChannelFee.hpp \