mirror of https://github.com/oxen-io/lokinet
Merge pull request #1318 from notlesh/peer-stats-follow-up-2020-07-09
Peer stats follow up 2020 07 09pull/1321/head
commit
998d4c4ec3
@ -0,0 +1 @@
|
||||
Subproject commit f7ef17a6bde6162e8b487deb36519bace412920a
|
@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include <sqlite_orm/sqlite_orm.h>
|
||||
|
||||
#include <peerstats/types.hpp>
|
||||
|
||||
/// Contains some code to help deal with sqlite_orm in hopes of keeping other headers clean
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
inline auto
|
||||
initStorage(const std::string& file)
|
||||
{
|
||||
using namespace sqlite_orm;
|
||||
return make_storage(
|
||||
file,
|
||||
make_table(
|
||||
"peerstats",
|
||||
make_column("routerId", &PeerStats::routerId, primary_key(), unique()),
|
||||
make_column("numConnectionAttempts", &PeerStats::numConnectionAttempts),
|
||||
make_column("numConnectionSuccesses", &PeerStats::numConnectionSuccesses),
|
||||
make_column("numConnectionRejections", &PeerStats::numConnectionRejections),
|
||||
make_column("numConnectionTimeouts", &PeerStats::numConnectionTimeouts),
|
||||
make_column("numPathBuilds", &PeerStats::numPathBuilds),
|
||||
make_column("numPacketsAttempted", &PeerStats::numPacketsAttempted),
|
||||
make_column("numPacketsSent", &PeerStats::numPacketsSent),
|
||||
make_column("numPacketsDropped", &PeerStats::numPacketsDropped),
|
||||
make_column("numPacketsResent", &PeerStats::numPacketsResent),
|
||||
make_column("numDistinctRCsReceived", &PeerStats::numDistinctRCsReceived),
|
||||
make_column("numLateRCs", &PeerStats::numLateRCs),
|
||||
make_column("peakBandwidthBytesPerSec", &PeerStats::peakBandwidthBytesPerSec),
|
||||
make_column("longestRCReceiveInterval", &PeerStats::longestRCReceiveInterval),
|
||||
make_column("leastRCRemainingLifetime", &PeerStats::leastRCRemainingLifetime)));
|
||||
}
|
||||
|
||||
using PeerDbStorage = decltype(initStorage(""));
|
||||
|
||||
} // namespace llarp
|
||||
|
||||
/// "custom" types for sqlite_orm
|
||||
/// reference: https://github.com/fnc12/sqlite_orm/blob/master/examples/enum_binding.cpp
|
||||
namespace sqlite_orm
|
||||
{
|
||||
/// llarp_time_t serialization
|
||||
template <>
|
||||
struct type_printer<llarp_time_t> : public integer_printer
|
||||
{
|
||||
};
|
||||
|
||||
template <>
|
||||
struct statement_binder<llarp_time_t>
|
||||
{
|
||||
int
|
||||
bind(sqlite3_stmt* stmt, int index, const llarp_time_t& value)
|
||||
{
|
||||
return statement_binder<int64_t>().bind(stmt, index, value.count());
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct field_printer<llarp_time_t>
|
||||
{
|
||||
std::string
|
||||
operator()(const llarp_time_t& value) const
|
||||
{
|
||||
std::stringstream stream;
|
||||
stream << value.count();
|
||||
return stream.str();
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct row_extractor<llarp_time_t>
|
||||
{
|
||||
llarp_time_t
|
||||
extract(const char* row_value)
|
||||
{
|
||||
int64_t raw = static_cast<int64_t>(atoi(row_value));
|
||||
return llarp_time_t(raw);
|
||||
}
|
||||
|
||||
llarp_time_t
|
||||
extract(sqlite3_stmt* stmt, int columnIndex)
|
||||
{
|
||||
auto str = sqlite3_column_text(stmt, columnIndex);
|
||||
return this->extract((const char*)str);
|
||||
}
|
||||
};
|
||||
|
||||
/// RouterID serialization
|
||||
template <>
|
||||
struct type_printer<llarp::RouterID> : public text_printer
|
||||
{
|
||||
};
|
||||
|
||||
template <>
|
||||
struct statement_binder<llarp::RouterID>
|
||||
{
|
||||
int
|
||||
bind(sqlite3_stmt* stmt, int index, const llarp::RouterID& value)
|
||||
{
|
||||
return statement_binder<std::string>().bind(stmt, index, value.ToString());
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct field_printer<llarp::RouterID>
|
||||
{
|
||||
std::string
|
||||
operator()(const llarp::RouterID& value) const
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct row_extractor<llarp::RouterID>
|
||||
{
|
||||
llarp::RouterID
|
||||
extract(const char* row_value)
|
||||
{
|
||||
llarp::RouterID id;
|
||||
if (not id.FromString(row_value))
|
||||
throw std::runtime_error("Invalid RouterID in sqlite3 database");
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
llarp::RouterID
|
||||
extract(sqlite3_stmt* stmt, int columnIndex)
|
||||
{
|
||||
auto str = sqlite3_column_text(stmt, columnIndex);
|
||||
return this->extract((const char*)str);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace sqlite_orm
|
@ -0,0 +1,303 @@
|
||||
#include <peerstats/peer_db.hpp>
|
||||
|
||||
#include <util/logging/logger.hpp>
|
||||
#include <util/status.hpp>
|
||||
#include <util/str.hpp>
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
PeerDb::PeerDb()
|
||||
{
|
||||
m_lastFlush.store({});
|
||||
}
|
||||
|
||||
void
|
||||
PeerDb::loadDatabase(std::optional<fs::path> file)
|
||||
{
|
||||
std::lock_guard guard(m_statsLock);
|
||||
|
||||
if (m_storage)
|
||||
throw std::runtime_error("Reloading database not supported"); // TODO
|
||||
|
||||
m_peerStats.clear();
|
||||
|
||||
// sqlite_orm treats empty-string as an indicator to load a memory-backed database, which we'll
|
||||
// use if file is an empty-optional
|
||||
std::string fileString;
|
||||
if (file.has_value())
|
||||
{
|
||||
fileString = file->string();
|
||||
LogInfo("Loading PeerDb from file ", fileString);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogInfo("Loading memory-backed PeerDb");
|
||||
}
|
||||
|
||||
m_storage = std::make_unique<PeerDbStorage>(initStorage(fileString));
|
||||
m_storage->sync_schema(true); // true for "preserve" as in "don't nuke" (how cute!)
|
||||
|
||||
auto allStats = m_storage->get_all<PeerStats>();
|
||||
LogInfo("Loading ", allStats.size(), " PeerStats from table peerstats...");
|
||||
for (PeerStats& stats : allStats)
|
||||
{
|
||||
// we cleared m_peerStats, and the database should enforce that routerId is unique...
|
||||
assert(m_peerStats.find(stats.routerId) == m_peerStats.end());
|
||||
|
||||
stats.stale = false;
|
||||
m_peerStats[stats.routerId] = stats;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PeerDb::flushDatabase()
|
||||
{
|
||||
LogDebug("flushing PeerDb...");
|
||||
|
||||
auto start = time_now_ms();
|
||||
if (not shouldFlush(start))
|
||||
{
|
||||
LogWarn("Call to flushDatabase() while already in progress, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
if (not m_storage)
|
||||
throw std::runtime_error("Cannot flush database before it has been loaded");
|
||||
|
||||
std::vector<PeerStats> staleStats;
|
||||
|
||||
{
|
||||
std::lock_guard guard(m_statsLock);
|
||||
|
||||
// copy all stale entries
|
||||
for (auto& entry : m_peerStats)
|
||||
{
|
||||
if (entry.second.stale)
|
||||
{
|
||||
staleStats.push_back(entry.second);
|
||||
entry.second.stale = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogInfo("Updating ", staleStats.size(), " stats");
|
||||
|
||||
{
|
||||
auto guard = m_storage->transaction_guard();
|
||||
|
||||
for (const auto& stats : staleStats)
|
||||
{
|
||||
m_storage->replace(stats);
|
||||
}
|
||||
|
||||
guard.commit();
|
||||
}
|
||||
|
||||
auto end = time_now_ms();
|
||||
|
||||
auto elapsed = end - start;
|
||||
LogInfo("PeerDb flush took about ", elapsed, " seconds");
|
||||
|
||||
m_lastFlush.store(end);
|
||||
}
|
||||
|
||||
void
|
||||
PeerDb::accumulatePeerStats(const RouterID& routerId, const PeerStats& delta)
|
||||
{
|
||||
if (routerId != delta.routerId)
|
||||
throw std::invalid_argument(
|
||||
stringify("routerId ", routerId, " doesn't match ", delta.routerId));
|
||||
|
||||
std::lock_guard guard(m_statsLock);
|
||||
auto itr = m_peerStats.find(routerId);
|
||||
if (itr == m_peerStats.end())
|
||||
itr = m_peerStats.insert({routerId, delta}).first;
|
||||
else
|
||||
itr->second += delta;
|
||||
|
||||
itr->second.stale = true;
|
||||
}
|
||||
|
||||
void
|
||||
PeerDb::modifyPeerStats(const RouterID& routerId, std::function<void(PeerStats&)> callback)
|
||||
{
|
||||
std::lock_guard guard(m_statsLock);
|
||||
|
||||
PeerStats& stats = m_peerStats[routerId];
|
||||
stats.routerId = routerId;
|
||||
stats.stale = true;
|
||||
callback(stats);
|
||||
}
|
||||
|
||||
std::optional<PeerStats>
|
||||
PeerDb::getCurrentPeerStats(const RouterID& routerId) const
|
||||
{
|
||||
std::lock_guard guard(m_statsLock);
|
||||
auto itr = m_peerStats.find(routerId);
|
||||
if (itr == m_peerStats.end())
|
||||
return std::nullopt;
|
||||
else
|
||||
return itr->second;
|
||||
}
|
||||
|
||||
std::vector<PeerStats>
|
||||
PeerDb::listAllPeerStats() const
|
||||
{
|
||||
std::lock_guard guard(m_statsLock);
|
||||
|
||||
std::vector<PeerStats> statsList;
|
||||
statsList.reserve(m_peerStats.size());
|
||||
|
||||
for (const auto& [routerId, stats] : m_peerStats)
|
||||
{
|
||||
statsList.push_back(stats);
|
||||
}
|
||||
|
||||
return statsList;
|
||||
}
|
||||
|
||||
std::vector<PeerStats>
|
||||
PeerDb::listPeerStats(const std::vector<RouterID>& ids) const
|
||||
{
|
||||
std::lock_guard guard(m_statsLock);
|
||||
|
||||
std::vector<PeerStats> statsList;
|
||||
statsList.reserve(ids.size());
|
||||
|
||||
for (const auto& id : ids)
|
||||
{
|
||||
const auto itr = m_peerStats.find(id);
|
||||
if (itr != m_peerStats.end())
|
||||
statsList.push_back(itr->second);
|
||||
}
|
||||
|
||||
return statsList;
|
||||
}
|
||||
|
||||
/// Assume we receive an RC at some point `R` in time which was signed at some point `S` in time
|
||||
/// and expires at some point `E` in time, as depicted below:
|
||||
///
|
||||
/// +-----------------------------+
|
||||
/// | signed rc | <- useful lifetime of RC
|
||||
/// +-----------------------------+
|
||||
/// ^ [ . . . . . . . . ] <----------- window in which we receive this RC gossiped to us
|
||||
/// | ^ ^
|
||||
/// | | |
|
||||
/// S R E
|
||||
///
|
||||
/// One useful metric from this is the difference between (E - R), the useful contact time of this
|
||||
/// RC. As we track this metric over time, the high and low watermarks serve to tell us how
|
||||
/// quickly we receive signed RCs from a given router and how close to expiration they are when
|
||||
/// we receive them. The latter is particularly useful, and should always be a positive number for
|
||||
/// a healthy router. A negative number indicates that we are receiving an expired RC.
|
||||
///
|
||||
/// TODO: we actually discard expired RCs, so we currently would not detect a negative value for
|
||||
/// (E - R)
|
||||
///
|
||||
/// Another related metric is the distance between a newly received RC and the previous RC's
|
||||
/// expiration, which represents how close we came to having no useful RC to work with. This
|
||||
/// should be a high (positive) number for a healthy router, and if negative indicates that we
|
||||
/// had no way to contact this router for a period of time.
|
||||
///
|
||||
/// E1 E2 E3
|
||||
/// | | |
|
||||
/// v | |
|
||||
/// +-----------------------------+ | |
|
||||
/// | signed rc 1 | | |
|
||||
/// +-----------------------------+ | |
|
||||
/// [ . . . . . ] v |
|
||||
/// ^ +-----------------------------+ |
|
||||
/// | | signed rc 2 | |
|
||||
/// | +-----------------------------+ |
|
||||
/// | [ . . . . . . . . . . ] v
|
||||
/// | ^ +-----------------------------+
|
||||
/// | | | signed rc 3 |
|
||||
/// | | +-----------------------------+
|
||||
/// | | [ . . ]
|
||||
/// | | ^
|
||||
/// | | |
|
||||
/// R1 R2 R3
|
||||
///
|
||||
/// Example: the delta between (E1 - R2) is healthy, but the delta between (E2 - R3) is indicates
|
||||
/// that we had a brief period of time where we had no valid (non-expired) RC for this router
|
||||
/// (because it is negative).
|
||||
void
|
||||
PeerDb::handleGossipedRC(const RouterContact& rc, llarp_time_t now)
|
||||
{
|
||||
std::lock_guard guard(m_statsLock);
|
||||
|
||||
RouterID id(rc.pubkey);
|
||||
auto& stats = m_peerStats[id];
|
||||
stats.routerId = id;
|
||||
|
||||
const bool isNewRC = (stats.lastRCUpdated < rc.last_updated);
|
||||
|
||||
if (isNewRC)
|
||||
{
|
||||
stats.numDistinctRCsReceived++;
|
||||
|
||||
if (stats.numDistinctRCsReceived > 1)
|
||||
{
|
||||
auto prevRCExpiration = (stats.lastRCUpdated + RouterContact::Lifetime);
|
||||
|
||||
// we track max expiry as the delta between (last expiration time - time received),
|
||||
// and this value will be negative for an unhealthy router
|
||||
// TODO: handle case where new RC is also expired? just ignore?
|
||||
auto expiry = prevRCExpiration - now;
|
||||
|
||||
if (stats.numDistinctRCsReceived == 2)
|
||||
stats.leastRCRemainingLifetime = expiry;
|
||||
else
|
||||
stats.leastRCRemainingLifetime = std::min(stats.leastRCRemainingLifetime, expiry);
|
||||
}
|
||||
|
||||
stats.lastRCUpdated = rc.last_updated;
|
||||
stats.stale = true;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
PeerDb::configure(const RouterConfig& routerConfig)
|
||||
{
|
||||
if (not routerConfig.m_enablePeerStats)
|
||||
throw std::runtime_error("[router]:enable-peer-stats is not enabled");
|
||||
|
||||
fs::path dbPath = routerConfig.m_dataDir / "peerstats.sqlite";
|
||||
|
||||
loadDatabase(dbPath);
|
||||
}
|
||||
|
||||
bool
|
||||
PeerDb::shouldFlush(llarp_time_t now)
|
||||
{
|
||||
constexpr llarp_time_t TargetFlushInterval = 30s;
|
||||
return (now - m_lastFlush.load() >= TargetFlushInterval);
|
||||
}
|
||||
|
||||
util::StatusObject
|
||||
PeerDb::ExtractStatus() const
|
||||
{
|
||||
std::lock_guard guard(m_statsLock);
|
||||
|
||||
bool loaded = (m_storage.get() != nullptr);
|
||||
util::StatusObject dbFile = nullptr;
|
||||
if (loaded)
|
||||
dbFile = m_storage->filename();
|
||||
|
||||
std::vector<util::StatusObject> statsObjs;
|
||||
statsObjs.reserve(m_peerStats.size());
|
||||
for (const auto& pair : m_peerStats)
|
||||
{
|
||||
statsObjs.push_back(pair.second.toJson());
|
||||
}
|
||||
|
||||
util::StatusObject obj{
|
||||
{"dbLoaded", loaded},
|
||||
{"dbFile", dbFile},
|
||||
{"lastFlushMs", m_lastFlush.load().count()},
|
||||
{"stats", statsObjs},
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
|
||||
}; // namespace llarp
|
@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <sqlite_orm/sqlite_orm.h>
|
||||
|
||||
#include <util/fs.hpp>
|
||||
#include <config/config.hpp>
|
||||
#include <router_id.hpp>
|
||||
#include <util/time.hpp>
|
||||
#include <peerstats/types.hpp>
|
||||
#include <peerstats/orm.hpp>
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
/// Maintains a database of stats collected about the connections with our Service Node peers.
|
||||
/// This uses a sqlite3 database behind the scenes as persistance, but this database is
|
||||
/// periodically flushed to, meaning that it will become stale as PeerDb accumulates stats without
|
||||
/// a flush.
|
||||
struct PeerDb
|
||||
{
|
||||
/// Constructor
|
||||
PeerDb();
|
||||
|
||||
/// Loads the database from disk using the provided filepath. If the file is equal to
|
||||
/// `std::nullopt`, the database will be loaded into memory (useful for testing).
|
||||
///
|
||||
/// This must be called prior to calling flushDatabase(), and will truncate any existing data.
|
||||
///
|
||||
/// This is a blocking call, both in the sense that it blocks on disk/database I/O and that it
|
||||
/// will sit on a mutex while the database is loaded.
|
||||
///
|
||||
/// @param file is an optional file which doesn't have to exist but must be writable, if a value
|
||||
/// is provided. If no value is provided, the database will be memory-backed.
|
||||
/// @throws if sqlite_orm/sqlite3 is unable to open or create a database at the given file
|
||||
void
|
||||
loadDatabase(std::optional<fs::path> file);
|
||||
|
||||
/// Flushes the database. Must be called after loadDatabase(). This call will block during I/O
|
||||
/// and should be called in an appropriate threading context. However, it will make a temporary
|
||||
/// copy of the peer stats so as to avoid sitting on a mutex lock during disk I/O.
|
||||
///
|
||||
/// @throws if the database could not be written to (esp. if loadDatabase() has not been called)
|
||||
void
|
||||
flushDatabase();
|
||||
|
||||
/// Add the given stats to the cummulative stats for the given peer. For cummulative stats, the
|
||||
/// stats are added together; for watermark stats, the max is kept.
|
||||
///
|
||||
/// This is intended to be used in the following pattern:
|
||||
///
|
||||
/// 1) Initialize an empty PeerStats
|
||||
/// 2) Collect relevant stats
|
||||
/// 3) Call accumulatePeerStats() with the stats
|
||||
/// 4) Reset the stats to 0
|
||||
/// 5) <Repeat 2-4 periodically>
|
||||
///
|
||||
/// @param routerId is the id of the router whose stats should be modified.
|
||||
/// @param delta is the stats to add to the existing stats
|
||||
void
|
||||
accumulatePeerStats(const RouterID& routerId, const PeerStats& delta);
|
||||
|
||||
/// Allows write-access to the stats for a given peer while appropriate mutex lock is held. This
|
||||
/// is an alternative means of incrementing peer stats that is suitable for one-off
|
||||
/// modifications.
|
||||
///
|
||||
/// Note that this holds m_statsLock during the callback invocation, so the callback should
|
||||
/// return as quickly as possible.
|
||||
///
|
||||
/// @param routerId is the id of the router whose stats should be modified.
|
||||
/// @param callback is a function which will be called immediately with mutex held
|
||||
void
|
||||
modifyPeerStats(const RouterID& routerId, std::function<void(PeerStats&)> callback);
|
||||
|
||||
/// Provides a snapshot of the most recent PeerStats we have for the given peer. If we don't
|
||||
/// have any stats for the peer, std::nullopt
|
||||
///
|
||||
/// @param routerId is the RouterID of the requested peer
|
||||
/// @return a copy of the most recent peer stats or an empty one if no such peer is known
|
||||
std::optional<PeerStats>
|
||||
getCurrentPeerStats(const RouterID& routerId) const;
|
||||
|
||||
/// Lists all peer stats. This essentially dumps the database into a list of PeerStats objects.
|
||||
///
|
||||
/// Note that this avoids disk I/O by copying from our cached map of peers.
|
||||
///
|
||||
/// @return a list of all PeerStats we have maintained
|
||||
std::vector<PeerStats>
|
||||
listAllPeerStats() const;
|
||||
|
||||
/// Lists specific peer stats.
|
||||
///
|
||||
/// @param peers is list of RouterIDs which are desired
|
||||
/// @return a list of the requested peers. Peers not found will be omitted.
|
||||
std::vector<PeerStats>
|
||||
listPeerStats(const std::vector<RouterID>& ids) const;
|
||||
|
||||
/// Handles a new gossiped RC, updating stats as needed. The database tracks the last
|
||||
/// advertised update time, so it knows whether this is a new RC or not.
|
||||
///
|
||||
/// The given RC is assumed to be valid.
|
||||
///
|
||||
/// @param rc is the RouterContact to handle
|
||||
/// @param now is an optional time representing the current time
|
||||
void
|
||||
handleGossipedRC(const RouterContact& rc, llarp_time_t now = time_now_ms());
|
||||
|
||||
/// Configures the PeerDb based on RouterConfig
|
||||
///
|
||||
/// @param routerConfig
|
||||
void
|
||||
configure(const RouterConfig& routerConfig);
|
||||
|
||||
/// Returns whether or not we should flush, as determined by the last time we flushed and the
|
||||
/// configured flush interval.
|
||||
///
|
||||
/// @param now is the current[-ish] time
|
||||
bool
|
||||
shouldFlush(llarp_time_t now);
|
||||
|
||||
/// Get JSON status for API
|
||||
///
|
||||
/// @return JSON object representing our current status
|
||||
util::StatusObject
|
||||
ExtractStatus() const;
|
||||
|
||||
private:
|
||||
std::unordered_map<RouterID, PeerStats, RouterID::Hash> m_peerStats;
|
||||
mutable std::mutex m_statsLock;
|
||||
|
||||
std::unique_ptr<PeerDbStorage> m_storage;
|
||||
|
||||
std::atomic<llarp_time_t> m_lastFlush;
|
||||
};
|
||||
|
||||
} // namespace llarp
|
@ -0,0 +1,159 @@
|
||||
#include <peerstats/types.hpp>
|
||||
|
||||
#include <util/str.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
|
||||
constexpr auto RouterIdKey = "routerId";
|
||||
constexpr auto NumConnectionAttemptsKey = "numConnectionAttempts";
|
||||
constexpr auto NumConnectionSuccessesKey = "numConnectionSuccesses";
|
||||
constexpr auto NumConnectionRejectionsKey = "numConnectionRejections";
|
||||
constexpr auto NumConnectionTimeoutsKey = "numConnectionTimeouts";
|
||||
constexpr auto NumPathBuildsKey = "numPathBuilds";
|
||||
constexpr auto NumPacketsAttemptedKey = "numPacketsAttempted";
|
||||
constexpr auto NumPacketsSentKey = "numPacketsSent";
|
||||
constexpr auto NumPacketsDroppedKey = "numPacketsDropped";
|
||||
constexpr auto NumPacketsResentKey = "numPacketsResent";
|
||||
constexpr auto NumDistinctRCsReceivedKey = "numDistinctRCsReceived";
|
||||
constexpr auto NumLateRCsKey = "numLateRCs";
|
||||
constexpr auto PeakBandwidthBytesPerSecKey = "peakBandwidthBytesPerSec";
|
||||
constexpr auto LongestRCReceiveIntervalKey = "longestRCReceiveInterval";
|
||||
constexpr auto LeastRCRemainingLifetimeKey = "leastRCRemainingLifetime";
|
||||
constexpr auto LastRCUpdatedKey = "lastRCUpdated";
|
||||
|
||||
PeerStats::PeerStats() = default;
|
||||
|
||||
PeerStats::PeerStats(const RouterID& routerId_) : routerId(routerId_)
|
||||
{
|
||||
}
|
||||
|
||||
PeerStats&
|
||||
PeerStats::operator+=(const PeerStats& other)
|
||||
{
|
||||
numConnectionAttempts += other.numConnectionAttempts;
|
||||
numConnectionSuccesses += other.numConnectionSuccesses;
|
||||
numConnectionRejections += other.numConnectionRejections;
|
||||
numConnectionTimeouts += other.numConnectionTimeouts;
|
||||
|
||||
numPathBuilds += other.numPathBuilds;
|
||||
numPacketsAttempted += other.numPacketsAttempted;
|
||||
numPacketsSent += other.numPacketsSent;
|
||||
numPacketsDropped += other.numPacketsDropped;
|
||||
numPacketsResent += other.numPacketsResent;
|
||||
|
||||
numDistinctRCsReceived += other.numDistinctRCsReceived;
|
||||
numLateRCs += other.numLateRCs;
|
||||
|
||||
peakBandwidthBytesPerSec = std::max(peakBandwidthBytesPerSec, other.peakBandwidthBytesPerSec);
|
||||
longestRCReceiveInterval = std::max(longestRCReceiveInterval, other.longestRCReceiveInterval);
|
||||
leastRCRemainingLifetime = std::max(leastRCRemainingLifetime, other.leastRCRemainingLifetime);
|
||||
lastRCUpdated = std::max(lastRCUpdated, other.lastRCUpdated);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool
|
||||
PeerStats::operator==(const PeerStats& other) const
|
||||
{
|
||||
return routerId == other.routerId and numConnectionAttempts == other.numConnectionAttempts
|
||||
and numConnectionSuccesses == other.numConnectionSuccesses
|
||||
and numConnectionRejections == other.numConnectionRejections
|
||||
and numConnectionTimeouts == other.numConnectionTimeouts
|
||||
|
||||
and numPathBuilds == other.numPathBuilds
|
||||
and numPacketsAttempted == other.numPacketsAttempted
|
||||
and numPacketsSent == other.numPacketsSent and numPacketsDropped == other.numPacketsDropped
|
||||
and numPacketsResent == other.numPacketsResent
|
||||
|
||||
and numDistinctRCsReceived == other.numDistinctRCsReceived
|
||||
and numLateRCs == other.numLateRCs
|
||||
|
||||
and peakBandwidthBytesPerSec == other.peakBandwidthBytesPerSec
|
||||
and longestRCReceiveInterval == other.longestRCReceiveInterval
|
||||
and leastRCRemainingLifetime == other.leastRCRemainingLifetime
|
||||
and lastRCUpdated == other.lastRCUpdated;
|
||||
}
|
||||
|
||||
util::StatusObject
|
||||
PeerStats::toJson() const
|
||||
{
|
||||
return {
|
||||
{RouterIdKey, routerId.ToString()},
|
||||
{NumConnectionAttemptsKey, numConnectionAttempts},
|
||||
{NumConnectionSuccessesKey, numConnectionSuccesses},
|
||||
{NumConnectionRejectionsKey, numConnectionRejections},
|
||||
{NumConnectionTimeoutsKey, numConnectionTimeouts},
|
||||
{NumPathBuildsKey, numPathBuilds},
|
||||
{NumPacketsAttemptedKey, numPacketsAttempted},
|
||||
{NumPacketsSentKey, numPacketsSent},
|
||||
{NumPacketsDroppedKey, numPacketsDropped},
|
||||
{NumPacketsResentKey, numPacketsResent},
|
||||
{NumDistinctRCsReceivedKey, numDistinctRCsReceived},
|
||||
{NumLateRCsKey, numLateRCs},
|
||||
{PeakBandwidthBytesPerSecKey, peakBandwidthBytesPerSec},
|
||||
{LongestRCReceiveIntervalKey, longestRCReceiveInterval.count()},
|
||||
{LeastRCRemainingLifetimeKey, leastRCRemainingLifetime.count()},
|
||||
{LastRCUpdatedKey, lastRCUpdated.count()},
|
||||
};
|
||||
}
|
||||
|
||||
void
|
||||
PeerStats::BEncode(llarp_buffer_t* buf) const
|
||||
{
|
||||
if (not buf)
|
||||
throw std::runtime_error("PeerStats: Can't use null buf");
|
||||
|
||||
auto encodeUint64Entry = [&](std::string_view key, uint64_t value) {
|
||||
if (not bencode_write_uint64_entry(buf, key.data(), key.size(), value))
|
||||
throw std::runtime_error(stringify("PeerStats: Could not encode ", key));
|
||||
};
|
||||
|
||||
if (not bencode_start_dict(buf))
|
||||
throw std::runtime_error("PeerStats: Could not create bencode dict");
|
||||
|
||||
// TODO: we don't have bencode support for dict entries other than uint64...?
|
||||
|
||||
// encodeUint64Entry(RouterIdKey, routerId);
|
||||
encodeUint64Entry(NumConnectionAttemptsKey, numConnectionAttempts);
|
||||
encodeUint64Entry(NumConnectionSuccessesKey, numConnectionSuccesses);
|
||||
encodeUint64Entry(NumConnectionRejectionsKey, numConnectionRejections);
|
||||
encodeUint64Entry(NumConnectionTimeoutsKey, numConnectionTimeouts);
|
||||
encodeUint64Entry(NumPathBuildsKey, numPathBuilds);
|
||||
encodeUint64Entry(NumPacketsAttemptedKey, numPacketsAttempted);
|
||||
encodeUint64Entry(NumPacketsSentKey, numPacketsSent);
|
||||
encodeUint64Entry(NumPacketsDroppedKey, numPacketsDropped);
|
||||
encodeUint64Entry(NumPacketsResentKey, numPacketsResent);
|
||||
encodeUint64Entry(NumDistinctRCsReceivedKey, numDistinctRCsReceived);
|
||||
encodeUint64Entry(NumLateRCsKey, numLateRCs);
|
||||
encodeUint64Entry(PeakBandwidthBytesPerSecKey, (uint64_t)peakBandwidthBytesPerSec);
|
||||
encodeUint64Entry(LongestRCReceiveIntervalKey, longestRCReceiveInterval.count());
|
||||
encodeUint64Entry(LeastRCRemainingLifetimeKey, leastRCRemainingLifetime.count());
|
||||
encodeUint64Entry(LastRCUpdatedKey, lastRCUpdated.count());
|
||||
|
||||
if (not bencode_end(buf))
|
||||
throw std::runtime_error("PeerStats: Could not end bencode dict");
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
PeerStats::BEncodeList(const std::vector<PeerStats>& statsList, llarp_buffer_t* buf)
|
||||
{
|
||||
if (not buf)
|
||||
throw std::runtime_error("PeerStats: Can't use null buf");
|
||||
|
||||
if (not bencode_start_list(buf))
|
||||
throw std::runtime_error("PeerStats: Could not create bencode dict");
|
||||
|
||||
for (const auto& stats : statsList)
|
||||
{
|
||||
stats.BEncode(buf);
|
||||
}
|
||||
|
||||
if (not bencode_end(buf))
|
||||
throw std::runtime_error("PeerStats: Could not end bencode dict");
|
||||
}
|
||||
|
||||
}; // namespace llarp
|
@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <router_id.hpp>
|
||||
#include <util/status.hpp>
|
||||
#include <util/time.hpp>
|
||||
|
||||
/// Types stored in our peerstats database are declared here
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
// Struct containing stats we know about a peer
|
||||
struct PeerStats
|
||||
{
|
||||
RouterID routerId;
|
||||
|
||||
int32_t numConnectionAttempts = 0;
|
||||
int32_t numConnectionSuccesses = 0;
|
||||
int32_t numConnectionRejections = 0;
|
||||
int32_t numConnectionTimeouts = 0;
|
||||
|
||||
int32_t numPathBuilds = 0;
|
||||
int64_t numPacketsAttempted = 0;
|
||||
int64_t numPacketsSent = 0;
|
||||
int64_t numPacketsDropped = 0;
|
||||
int64_t numPacketsResent = 0;
|
||||
|
||||
int32_t numDistinctRCsReceived = 0;
|
||||
int32_t numLateRCs = 0;
|
||||
|
||||
double peakBandwidthBytesPerSec = 0;
|
||||
llarp_time_t longestRCReceiveInterval = 0ms;
|
||||
llarp_time_t leastRCRemainingLifetime = 0ms;
|
||||
llarp_time_t lastRCUpdated = 0ms;
|
||||
|
||||
// not serialized
|
||||
bool stale = true;
|
||||
|
||||
PeerStats();
|
||||
PeerStats(const RouterID& routerId);
|
||||
|
||||
PeerStats&
|
||||
operator+=(const PeerStats& other);
|
||||
bool
|
||||
operator==(const PeerStats& other) const;
|
||||
|
||||
util::StatusObject
|
||||
toJson() const;
|
||||
|
||||
void
|
||||
BEncode(llarp_buffer_t* buf) const;
|
||||
|
||||
static void
|
||||
BEncodeList(const std::vector<PeerStats>& statsList, llarp_buffer_t* buf);
|
||||
};
|
||||
|
||||
} // namespace llarp
|
@ -0,0 +1,33 @@
|
||||
#include <tooling/hive_context.hpp>
|
||||
|
||||
#include <tooling/hive_router.hpp>
|
||||
|
||||
namespace tooling
|
||||
{
|
||||
HiveContext::HiveContext(RouterHive* hive) : m_hive(hive)
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<llarp::AbstractRouter>
|
||||
HiveContext::makeRouter(
|
||||
llarp_ev_loop_ptr netloop,
|
||||
std::shared_ptr<llarp::Logic> logic)
|
||||
{
|
||||
return std::make_unique<HiveRouter>(netloop, logic, m_hive);
|
||||
}
|
||||
|
||||
HiveRouter*
|
||||
HiveContext::getRouterAsHiveRouter()
|
||||
{
|
||||
if (not router)
|
||||
return nullptr;
|
||||
|
||||
HiveRouter* hiveRouter = dynamic_cast<HiveRouter*>(router.get());
|
||||
|
||||
if (hiveRouter == nullptr)
|
||||
throw std::runtime_error("HiveContext has a router not of type HiveRouter");
|
||||
|
||||
return hiveRouter;
|
||||
}
|
||||
|
||||
} // namespace tooling
|
@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <llarp.hpp>
|
||||
#include <tooling/hive_router.hpp>
|
||||
|
||||
namespace tooling
|
||||
{
|
||||
/// HiveContext is a subclass of llarp::Context which allows RouterHive to
|
||||
/// perform custom behavior which might be undesirable in production code.
|
||||
struct HiveContext : public llarp::Context
|
||||
{
|
||||
HiveContext(RouterHive* hive);
|
||||
|
||||
std::unique_ptr<llarp::AbstractRouter>
|
||||
makeRouter(
|
||||
llarp_ev_loop_ptr netloop,
|
||||
std::shared_ptr<llarp::Logic> logic) override;
|
||||
|
||||
/// Get this context's router as a HiveRouter.
|
||||
///
|
||||
/// Returns nullptr if there is no router or throws an exception if the
|
||||
/// router is somehow not an instance of HiveRouter.
|
||||
HiveRouter*
|
||||
getRouterAsHiveRouter();
|
||||
|
||||
protected:
|
||||
RouterHive* m_hive = nullptr;
|
||||
};
|
||||
|
||||
} // namespace tooling
|
@ -0,0 +1,37 @@
|
||||
#include <tooling/hive_router.hpp>
|
||||
|
||||
#include <tooling/router_hive.hpp>
|
||||
|
||||
namespace tooling
|
||||
{
|
||||
HiveRouter::HiveRouter(
|
||||
llarp_ev_loop_ptr netloop, std::shared_ptr<llarp::Logic> logic, RouterHive* hive)
|
||||
: Router(netloop, logic), m_hive(hive)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
HiveRouter::disableGossipingRC_TestingOnly()
|
||||
{
|
||||
return m_disableGossiping;
|
||||
}
|
||||
|
||||
void
|
||||
HiveRouter::disableGossiping()
|
||||
{
|
||||
m_disableGossiping = true;
|
||||
}
|
||||
|
||||
void
|
||||
HiveRouter::enableGossiping()
|
||||
{
|
||||
m_disableGossiping = false;
|
||||
}
|
||||
|
||||
void
|
||||
HiveRouter::HandleRouterEvent(RouterEventPtr event) const
|
||||
{
|
||||
m_hive->NotifyEvent(std::move(event));
|
||||
}
|
||||
|
||||
} // namespace tooling
|
@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <router/router.hpp>
|
||||
|
||||
namespace tooling
|
||||
{
|
||||
/// HiveRouter is a subclass of Router which overrides specific behavior in
|
||||
/// order to perform testing-related functions. It exists largely to prevent
|
||||
/// this behavior (which may often be "dangerous") from leaking into release
|
||||
/// code.
|
||||
struct HiveRouter : public llarp::Router
|
||||
{
|
||||
HiveRouter(
|
||||
llarp_ev_loop_ptr netloop,
|
||||
std::shared_ptr<llarp::Logic> logic,
|
||||
RouterHive* hive);
|
||||
|
||||
virtual ~HiveRouter() = default;
|
||||
|
||||
/// Override logic to prevent base Router class from gossiping its RC.
|
||||
virtual bool
|
||||
disableGossipingRC_TestingOnly() override;
|
||||
|
||||
void
|
||||
disableGossiping();
|
||||
|
||||
void
|
||||
enableGossiping();
|
||||
|
||||
protected:
|
||||
bool m_disableGossiping = false;
|
||||
RouterHive* m_hive = nullptr;
|
||||
|
||||
virtual void
|
||||
HandleRouterEvent(RouterEventPtr event) const override;
|
||||
};
|
||||
|
||||
} // namespace tooling
|
@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "router_event.hpp"
|
||||
|
||||
namespace tooling
|
||||
{
|
||||
struct LinkSessionEstablishedEvent : public RouterEvent
|
||||
{
|
||||
llarp::RouterID remoteId;
|
||||
bool inbound = false;
|
||||
|
||||
LinkSessionEstablishedEvent(
|
||||
const llarp::RouterID& ourRouterId, const llarp::RouterID& remoteId_, bool inbound_)
|
||||
: RouterEvent("Link: LinkSessionEstablishedEvent", ourRouterId, false)
|
||||
, remoteId(remoteId_)
|
||||
, inbound(inbound_)
|
||||
{
|
||||
}
|
||||
|
||||
std::string
|
||||
ToString() const
|
||||
{
|
||||
return RouterEvent::ToString() + (inbound ? "inbound" : "outbound")
|
||||
+ " : LinkSessionEstablished with " + remoteId.ToString();
|
||||
}
|
||||
};
|
||||
|
||||
struct ConnectionAttemptEvent : public RouterEvent
|
||||
{
|
||||
llarp::RouterID remoteId;
|
||||
|
||||
ConnectionAttemptEvent(const llarp::RouterID& ourRouterId, const llarp::RouterID& remoteId_)
|
||||
: RouterEvent("Link: ConnectionAttemptEvent", ourRouterId, false), remoteId(remoteId_)
|
||||
{
|
||||
}
|
||||
|
||||
std::string
|
||||
ToString() const
|
||||
{
|
||||
return RouterEvent::ToString() + " : LinkSessionEstablished with " + remoteId.ToString();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tooling
|
@ -0,0 +1,40 @@
|
||||
#include "common.hpp"
|
||||
#include "config/config.hpp"
|
||||
#include "peerstats/peer_db.hpp"
|
||||
#include "peerstats/types.hpp"
|
||||
|
||||
#include <netinet/in.h>
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
void
|
||||
PeerDb_Init(py::module& mod)
|
||||
{
|
||||
using PeerDb_ptr = std::shared_ptr<PeerDb>;
|
||||
py::class_<PeerDb, PeerDb_ptr>(mod, "PeerDb")
|
||||
.def("getCurrentPeerStats", &PeerDb::getCurrentPeerStats);
|
||||
}
|
||||
|
||||
void
|
||||
PeerStats_Init(py::module& mod)
|
||||
{
|
||||
py::class_<PeerStats>(mod, "PeerStats")
|
||||
.def_readwrite("routerId", &PeerStats::routerId)
|
||||
.def_readwrite("numConnectionAttempts", &PeerStats::numConnectionAttempts)
|
||||
.def_readwrite("numConnectionSuccesses", &PeerStats::numConnectionSuccesses)
|
||||
.def_readwrite("numConnectionRejections", &PeerStats::numConnectionRejections)
|
||||
.def_readwrite("numConnectionTimeouts", &PeerStats::numConnectionTimeouts)
|
||||
.def_readwrite("numPathBuilds", &PeerStats::numPathBuilds)
|
||||
.def_readwrite("numPacketsAttempted", &PeerStats::numPacketsAttempted)
|
||||
.def_readwrite("numPacketsSent", &PeerStats::numPacketsSent)
|
||||
.def_readwrite("numPacketsDropped", &PeerStats::numPacketsDropped)
|
||||
.def_readwrite("numPacketsResent", &PeerStats::numPacketsResent)
|
||||
.def_readwrite("numDistinctRCsReceived", &PeerStats::numDistinctRCsReceived)
|
||||
.def_readwrite("numLateRCs", &PeerStats::numLateRCs)
|
||||
.def_readwrite("peakBandwidthBytesPerSec", &PeerStats::peakBandwidthBytesPerSec)
|
||||
.def_readwrite("longestRCReceiveInterval", &PeerStats::longestRCReceiveInterval)
|
||||
.def_readwrite("leastRCRemainingLifetime", &PeerStats::leastRCRemainingLifetime)
|
||||
.def_readwrite("lastRCUpdated", &PeerStats::lastRCUpdated)
|
||||
.def_readwrite("stale", &PeerStats::stale);
|
||||
}
|
||||
} // namespace llarp
|
@ -0,0 +1,28 @@
|
||||
#include "common.hpp"
|
||||
|
||||
#include "router/abstractrouter.hpp"
|
||||
#include "tooling/hive_router.hpp"
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
void
|
||||
AbstractRouter_Init(py::module& mod)
|
||||
{
|
||||
py::class_<AbstractRouter>(mod, "AbstractRouter")
|
||||
.def("rc", &AbstractRouter::rc)
|
||||
.def("Stop", &AbstractRouter::Stop)
|
||||
.def("peerDb", &AbstractRouter::peerDb);
|
||||
}
|
||||
|
||||
} // namespace llarp
|
||||
|
||||
namespace tooling
|
||||
{
|
||||
void
|
||||
HiveRouter_Init(py::module& mod)
|
||||
{
|
||||
py::class_<HiveRouter, llarp::AbstractRouter>(mod, "HiveRouter")
|
||||
.def("disableGossiping", &HiveRouter::disableGossiping)
|
||||
.def("enableGossiping", &HiveRouter::enableGossiping);
|
||||
}
|
||||
} // namespace tooling
|
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "router_event.hpp"
|
||||
|
||||
namespace tooling
|
||||
{
|
||||
struct LinkSessionEstablishedEvent : public RouterEvent
|
||||
{
|
||||
llarp::RouterID remoteId;
|
||||
bool inbound = false;
|
||||
|
||||
LinkSessionEstablishedEvent(
|
||||
const llarp::RouterID& ourRouterId, const llarp::RouterID& remoteId_, bool inbound_)
|
||||
: RouterEvent("Link: LinkSessionEstablishedEvent", ourRouterId, false)
|
||||
, remoteId(remoteId_)
|
||||
, inbound(inbound_)
|
||||
{
|
||||
}
|
||||
|
||||
std::string
|
||||
ToString() const
|
||||
{
|
||||
return RouterEvent::ToString() + (inbound ? "inbound" : "outbound")
|
||||
+ " : LinkSessionEstablished with " + remoteId.ToString();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tooling
|
@ -0,0 +1,96 @@
|
||||
import pyllarp
|
||||
from time import time
|
||||
|
||||
def test_peer_stats(HiveForPeerStats):
|
||||
|
||||
numRelays = 12
|
||||
|
||||
hive = HiveForPeerStats(n_relays=numRelays, n_clients=0, netid="hive")
|
||||
someRouterId = None
|
||||
|
||||
def collectStatsForAWhile(duration):
|
||||
print("collecting router hive stats for {} seconds...", duration)
|
||||
|
||||
start_time = time()
|
||||
cur_time = start_time
|
||||
|
||||
# we track the number of attempted sessions and inbound/outbound established sessions
|
||||
numInbound = 0
|
||||
numOutbound = 0
|
||||
numAttempts = 0
|
||||
|
||||
nonlocal someRouterId
|
||||
|
||||
while cur_time < start_time + duration:
|
||||
hive.CollectAllEvents()
|
||||
|
||||
for event in hive.events:
|
||||
event_name = event.__class__.__name__
|
||||
|
||||
if event_name == "LinkSessionEstablishedEvent":
|
||||
if event.inbound:
|
||||
numInbound += 1
|
||||
else:
|
||||
numOutbound += 1
|
||||
|
||||
if event_name == "ConnectionAttemptEvent":
|
||||
numAttempts += 1
|
||||
|
||||
# we pick an arbitrary router out of our routers
|
||||
if someRouterId is None:
|
||||
someRouterId = event.remoteId;
|
||||
|
||||
hive.events = []
|
||||
cur_time = time()
|
||||
|
||||
# these should be strictly equal, although there is variation because of
|
||||
# the time we sample
|
||||
print("test duration exceeded")
|
||||
print("in: {} out: {} attempts: {}", numInbound, numOutbound, numAttempts);
|
||||
totalReceived = tally_rc_received_for_peer(hive.hive, someRouterId)
|
||||
|
||||
# every router should have received this relay's RC exactly once
|
||||
print("total times RC received: {} numRelays: {}", totalReceived, numRelays)
|
||||
|
||||
return {
|
||||
"numInbound": numInbound,
|
||||
"numOutbound": numOutbound,
|
||||
"numAttempts": numAttempts,
|
||||
"totalTargetRCsReceived": totalReceived,
|
||||
};
|
||||
|
||||
results1 = collectStatsForAWhile(30);
|
||||
assert(results1["totalTargetRCsReceived"] == numRelays)
|
||||
|
||||
# stop our router from gossiping
|
||||
router = hive.hive.GetRelay(someRouterId, True)
|
||||
router.disableGossiping();
|
||||
|
||||
ignore = collectStatsForAWhile(30);
|
||||
|
||||
# ensure that no one hears a fresh RC from this router again
|
||||
print("Starting second (longer) stats collection...")
|
||||
results2 = collectStatsForAWhile(3600);
|
||||
assert(results2["totalTargetRCsReceived"] == numRelays) # should not have increased
|
||||
|
||||
def tally_rc_received_for_peer(hive, routerId):
|
||||
|
||||
numFound = 0
|
||||
|
||||
def visit(context):
|
||||
nonlocal numFound
|
||||
|
||||
peerDb = context.getRouterAsHiveRouter().peerDb()
|
||||
stats = peerDb.getCurrentPeerStats(routerId);
|
||||
|
||||
assert(stats.routerId == routerId)
|
||||
|
||||
numFound += stats.numDistinctRCsReceived
|
||||
|
||||
hive.ForEachRelay(visit)
|
||||
|
||||
return numFound;
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -0,0 +1,217 @@
|
||||
#include <peerstats/peer_db.hpp>
|
||||
#include <test_util.hpp>
|
||||
|
||||
#include <numeric>
|
||||
#include <catch2/catch.hpp>
|
||||
#include "peerstats/types.hpp"
|
||||
#include "router_contact.hpp"
|
||||
#include "util/time.hpp"
|
||||
|
||||
TEST_CASE("Test PeerDb PeerStats memory storage", "[PeerDb]")
|
||||
{
|
||||
const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0x01);
|
||||
const llarp::PeerStats empty(id);
|
||||
|
||||
llarp::PeerDb db;
|
||||
CHECK(db.getCurrentPeerStats(id).has_value() == false);
|
||||
|
||||
llarp::PeerStats delta(id);
|
||||
delta.numConnectionAttempts = 4;
|
||||
delta.peakBandwidthBytesPerSec = 5;
|
||||
db.accumulatePeerStats(id, delta);
|
||||
CHECK(* db.getCurrentPeerStats(id) == delta);
|
||||
|
||||
delta = llarp::PeerStats(id);
|
||||
delta.numConnectionAttempts = 5;
|
||||
delta.peakBandwidthBytesPerSec = 6;
|
||||
db.accumulatePeerStats(id, delta);
|
||||
|
||||
llarp::PeerStats expected(id);
|
||||
expected.numConnectionAttempts = 9;
|
||||
expected.peakBandwidthBytesPerSec = 6;
|
||||
CHECK(* db.getCurrentPeerStats(id) == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("Test PeerDb flush before load", "[PeerDb]")
|
||||
{
|
||||
llarp::PeerDb db;
|
||||
CHECK_THROWS_WITH(db.flushDatabase(), "Cannot flush database before it has been loaded");
|
||||
}
|
||||
|
||||
TEST_CASE("Test PeerDb load twice", "[PeerDb]")
|
||||
{
|
||||
llarp::PeerDb db;
|
||||
CHECK_NOTHROW(db.loadDatabase(std::nullopt));
|
||||
CHECK_THROWS_WITH(db.loadDatabase(std::nullopt), "Reloading database not supported");
|
||||
}
|
||||
|
||||
TEST_CASE("Test PeerDb nukes stats on load", "[PeerDb]")
|
||||
{
|
||||
const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0x01);
|
||||
|
||||
llarp::PeerDb db;
|
||||
|
||||
llarp::PeerStats stats(id);
|
||||
stats.numConnectionAttempts = 1;
|
||||
|
||||
db.accumulatePeerStats(id, stats);
|
||||
CHECK(* db.getCurrentPeerStats(id) == stats);
|
||||
|
||||
db.loadDatabase(std::nullopt);
|
||||
|
||||
CHECK(db.getCurrentPeerStats(id).has_value() == false);
|
||||
}
|
||||
|
||||
TEST_CASE("Test PeerDb file-backed database reloads properly", "[PeerDb]")
|
||||
{
|
||||
const std::string filename = "/tmp/peerdb_test_tmp2.db.sqlite";
|
||||
const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0x02);
|
||||
|
||||
{
|
||||
llarp::PeerDb db;
|
||||
db.loadDatabase(filename);
|
||||
|
||||
llarp::PeerStats stats(id);
|
||||
stats.numConnectionAttempts = 43;
|
||||
|
||||
db.accumulatePeerStats(id, stats);
|
||||
|
||||
db.flushDatabase();
|
||||
}
|
||||
|
||||
{
|
||||
llarp::PeerDb db;
|
||||
db.loadDatabase(filename);
|
||||
|
||||
auto stats = db.getCurrentPeerStats(id);
|
||||
CHECK(stats.has_value() == true);
|
||||
CHECK(stats->numConnectionAttempts == 43);
|
||||
}
|
||||
|
||||
fs::remove(filename);
|
||||
}
|
||||
|
||||
TEST_CASE("Test PeerDb modifyPeerStats", "[PeerDb]")
|
||||
{
|
||||
const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0xF2);
|
||||
|
||||
int numTimesCalled = 0;
|
||||
|
||||
llarp::PeerDb db;
|
||||
db.loadDatabase(std::nullopt);
|
||||
|
||||
db.modifyPeerStats(id, [&](llarp::PeerStats& stats) {
|
||||
numTimesCalled++;
|
||||
|
||||
stats.numPathBuilds += 42;
|
||||
});
|
||||
|
||||
db.flushDatabase();
|
||||
|
||||
CHECK(numTimesCalled == 1);
|
||||
|
||||
auto stats = db.getCurrentPeerStats(id);
|
||||
CHECK(stats.has_value());
|
||||
CHECK(stats->numPathBuilds == 42);
|
||||
}
|
||||
|
||||
TEST_CASE("Test PeerDb handleGossipedRC", "[PeerDb]")
|
||||
{
|
||||
const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0xCA);
|
||||
|
||||
auto rcLifetime = llarp::RouterContact::Lifetime;
|
||||
llarp_time_t now = 0s;
|
||||
|
||||
llarp::RouterContact rc;
|
||||
rc.pubkey = llarp::PubKey(id);
|
||||
rc.last_updated = 10s;
|
||||
|
||||
llarp::PeerDb db;
|
||||
db.handleGossipedRC(rc, now);
|
||||
|
||||
auto stats = db.getCurrentPeerStats(id);
|
||||
CHECK(stats.has_value());
|
||||
CHECK(stats->leastRCRemainingLifetime == 0ms); // not calculated on first received RC
|
||||
CHECK(stats->numDistinctRCsReceived == 1);
|
||||
CHECK(stats->lastRCUpdated == 10000ms);
|
||||
|
||||
now = 9s;
|
||||
db.handleGossipedRC(rc, now);
|
||||
stats = db.getCurrentPeerStats(id);
|
||||
CHECK(stats.has_value());
|
||||
// these values should remain unchanged, this is not a new RC
|
||||
CHECK(stats->leastRCRemainingLifetime == 0ms);
|
||||
CHECK(stats->numDistinctRCsReceived == 1);
|
||||
CHECK(stats->lastRCUpdated == 10000ms);
|
||||
|
||||
rc.last_updated = 11s;
|
||||
|
||||
db.handleGossipedRC(rc, now);
|
||||
stats = db.getCurrentPeerStats(id);
|
||||
// should be (previous expiration time - new received time)
|
||||
CHECK(stats->leastRCRemainingLifetime == ((10s + rcLifetime) - now));
|
||||
CHECK(stats->numDistinctRCsReceived == 2);
|
||||
CHECK(stats->lastRCUpdated == 11000ms);
|
||||
}
|
||||
|
||||
TEST_CASE("Test PeerDb handleGossipedRC expiry calcs", "[PeerDb]")
|
||||
{
|
||||
const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0xF9);
|
||||
|
||||
// see comments in peer_db.cpp above PeerDb::handleGossipedRC() for some context around these
|
||||
// tests and esp. these numbers
|
||||
const llarp_time_t ref = 48h;
|
||||
const llarp_time_t rcLifetime = llarp::RouterContact::Lifetime;
|
||||
|
||||
// rc1, first rc received
|
||||
const llarp_time_t s1 = ref;
|
||||
const llarp_time_t r1 = s1 + 30s;
|
||||
const llarp_time_t e1 = s1 + rcLifetime;
|
||||
llarp::RouterContact rc1;
|
||||
rc1.pubkey = llarp::PubKey(id);
|
||||
rc1.last_updated = s1;
|
||||
|
||||
// rc2, second rc received
|
||||
// received "healthily", with lots of room to spare before rc1 expires
|
||||
const llarp_time_t s2 = s1 + 8h;
|
||||
const llarp_time_t r2 = s2 + 30s; // healthy recv time
|
||||
const llarp_time_t e2 = s2 + rcLifetime;
|
||||
llarp::RouterContact rc2;
|
||||
rc2.pubkey = llarp::PubKey(id);
|
||||
rc2.last_updated = s2;
|
||||
|
||||
// rc3, third rc received
|
||||
// received "unhealthily" (after rc2 expires)
|
||||
const llarp_time_t s3 = s2 + 8h;
|
||||
const llarp_time_t r3 = e2 + 1h; // received after e2
|
||||
llarp::RouterContact rc3;
|
||||
rc3.pubkey = llarp::PubKey(id);
|
||||
rc3.last_updated = s3;
|
||||
|
||||
llarp::PeerDb db;
|
||||
|
||||
db.handleGossipedRC(rc1, r1);
|
||||
auto stats1 = db.getCurrentPeerStats(id);
|
||||
CHECK(stats1.has_value());
|
||||
CHECK(stats1->leastRCRemainingLifetime == 0ms);
|
||||
CHECK(stats1->numDistinctRCsReceived == 1);
|
||||
CHECK(stats1->lastRCUpdated == s1);
|
||||
|
||||
db.handleGossipedRC(rc2, r2);
|
||||
auto stats2 = db.getCurrentPeerStats(id);
|
||||
CHECK(stats2.has_value());
|
||||
CHECK(stats2->leastRCRemainingLifetime == (e1 - r2));
|
||||
CHECK(stats2->leastRCRemainingLifetime > 0ms); // ensure positive indicates healthy
|
||||
CHECK(stats2->numDistinctRCsReceived == 2);
|
||||
CHECK(stats2->lastRCUpdated == s2);
|
||||
|
||||
db.handleGossipedRC(rc3, r3);
|
||||
auto stats3 = db.getCurrentPeerStats(id);
|
||||
CHECK(stats3.has_value());
|
||||
CHECK(stats3->leastRCRemainingLifetime == (e2 - r3));
|
||||
CHECK(
|
||||
stats3->leastRCRemainingLifetime
|
||||
< 0ms); // ensure negative indicates unhealthy and we use min()
|
||||
CHECK(stats3->numDistinctRCsReceived == 3);
|
||||
CHECK(stats3->lastRCUpdated == s3);
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
#include <numeric>
|
||||
#include <peerstats/types.hpp>
|
||||
#include <test_util.hpp>
|
||||
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
TEST_CASE("Test PeerStats operator+=", "[PeerStats]")
|
||||
{
|
||||
llarp::RouterID id = {};
|
||||
|
||||
// TODO: test all members
|
||||
llarp::PeerStats stats(id);
|
||||
stats.numConnectionAttempts = 1;
|
||||
stats.peakBandwidthBytesPerSec = 12;
|
||||
|
||||
llarp::PeerStats delta(id);
|
||||
delta.numConnectionAttempts = 2;
|
||||
delta.peakBandwidthBytesPerSec = 4;
|
||||
|
||||
stats += delta;
|
||||
|
||||
CHECK(stats.numConnectionAttempts == 3);
|
||||
CHECK(stats.peakBandwidthBytesPerSec == 12); // should take max(), not add
|
||||
}
|
||||
|
||||
TEST_CASE("Test PeerStats BEncode", "[PeerStats]")
|
||||
{
|
||||
llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0x01);
|
||||
|
||||
llarp::PeerStats stats(id);
|
||||
|
||||
stats.numConnectionAttempts = 1;
|
||||
stats.numConnectionSuccesses = 2;
|
||||
stats.numConnectionRejections = 3;
|
||||
stats.numConnectionTimeouts = 4;
|
||||
stats.numPathBuilds = 5;
|
||||
stats.numPacketsAttempted = 6;
|
||||
stats.numPacketsSent = 7;
|
||||
stats.numPacketsDropped = 8;
|
||||
stats.numPacketsResent = 9;
|
||||
stats.numDistinctRCsReceived = 10;
|
||||
stats.numLateRCs = 11;
|
||||
stats.peakBandwidthBytesPerSec = 12.1; // should truncate to 12
|
||||
stats.longestRCReceiveInterval = 13ms;
|
||||
stats.leastRCRemainingLifetime = 14ms;
|
||||
stats.lastRCUpdated = 15ms;
|
||||
|
||||
constexpr int bufSize = 4096;
|
||||
byte_t* raw = new byte_t[bufSize];
|
||||
llarp_buffer_t buf(raw, bufSize);
|
||||
|
||||
CHECK_NOTHROW(stats.BEncode(&buf));
|
||||
|
||||
std::string asString = (const char*)raw;
|
||||
constexpr std::string_view expected =
|
||||
"d"
|
||||
"21:numConnectionAttempts" "i1e"
|
||||
"22:numConnectionSuccesses" "i2e"
|
||||
"23:numConnectionRejections" "i3e"
|
||||
"21:numConnectionTimeouts" "i4e"
|
||||
"13:numPathBuilds" "i5e"
|
||||
"19:numPacketsAttempted" "i6e"
|
||||
"14:numPacketsSent" "i7e"
|
||||
"17:numPacketsDropped" "i8e"
|
||||
"16:numPacketsResent" "i9e"
|
||||
"22:numDistinctRCsReceived" "i10e"
|
||||
"10:numLateRCs" "i11e"
|
||||
"24:peakBandwidthBytesPerSec" "i12e"
|
||||
"24:longestRCReceiveInterval" "i13e"
|
||||
"24:leastRCRemainingLifetime" "i14e"
|
||||
"13:lastRCUpdated" "i15e"
|
||||
"e";
|
||||
|
||||
CHECK(asString == expected);
|
||||
|
||||
delete [] raw;
|
||||
}
|
Loading…
Reference in New Issue