Files
nym/validator-api/src/node_status_api/storage.rs
T
Jędrzej Stuczyński e5afd54ce0 Feature/node status api (#680)
* Basic storage stub

* New models for new node status api

* Route handling

* Mounting new routes

* Missing selective commit

* Moved network monitor related files to separate package

* Starting to see some sqlx action

* Schema updates

* Log statement upon finished migration

* Removed old diesel related imports

* Converted mixnode cache initialisation into a fairing

* Moved cache related functionalities to separate package

Also defined staging there

* Created run method for validator cache + removed unwrap

* Removed old node-status-api types and left bunch of todo placeholders in their place

* Fixed managing validatorcache

* Status reports are starting to get constructed

* Submitting some dummy results to the database

* Removing duplicate code for generating reports

* Removed statuses older than 48h

* Initial attempt at trying to obtain reports for all active nodes

* Removed duplicates from the full report

* Grabbing uptime history

* Updating historical uptimes of active nodes

* Updated sqlx-data.json

* Removed all placeholder foomp owner values

* Changed Layer serde behaviour for easier usage

* Extended validator api config

* Initial (seems working !) integration with network monitor

* Added database path configuration to config

* Using ValidatorCache in NetworkMonitor

* Flag indicating whether validator cache has been initialised

* Introduced a locla-only route for reward script to perform daily chores

* Flag to save config to a file

* Moved spawning of receiving future to run method rather than new

* Removed arguments that dont make sense to be configured via CLI

* Removed dead code from config file

* More dead code removal

* Added validator API to CI

* Corrected manifest-path arguments

* Constructing network monitor by passing config

* Combined validator API CI with the main CI file

* Using query_as for NodeStatus

* Checking if historical uptimes were already calculated on particular day

* Making id field NOT NULL

* More query_as! action

* Updated sqlx-data.json

* Removed unused chrono feature

* Renamed the migration file

* Changed default validator endpoint to point to local validator

* Removing unnecessary clone

* More appropriate naming

* Removed dead code

* Lock file updates

* Updated network monitor address in contract code

* Don't stage node status api if network monitor is disabled

* cargo fmt

* Updated all license notices to SPDX
2021-07-19 14:02:47 +01:00

1046 lines
36 KiB
Rust

// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::network_monitor::monitor::summary_producer::NodeResult;
use crate::node_status_api::models::{
GatewayStatusReport, GatewayUptimeHistory, HistoricalUptime, MixnodeStatusReport,
MixnodeUptimeHistory, NodeStatusApiError, Uptime,
};
use crate::node_status_api::utils::{ActiveNode, ActiveNodeDayStatuses, NodeStatus};
use crate::node_status_api::ONE_DAY;
use rocket::fairing::{self, AdHoc};
use rocket::{Build, Rocket};
use sqlx::types::time::OffsetDateTime;
use sqlx::ConnectOptions;
use std::convert::TryFrom;
use std::path::PathBuf;
// A type alias to be more explicit about type of timestamp used.
type UnixTimestamp = i64;
// note that clone here is fine as upon cloning the same underlying pool will be used
//
// note2: the reason 'inner' was introduced was so that there would be an explicit split to
// where pure SQL is used (i.e. `Inner` should be the only place containing any sort
// of SQL while `NodeStatusStorage` should provide a slightly higher level API)
#[derive(Clone)]
pub(crate) struct NodeStatusStorage {
inner: NodeStatusStorageInner,
}
#[derive(Clone)]
struct NodeStatusStorageInner {
connection_pool: sqlx::SqlitePool,
}
impl NodeStatusStorage {
async fn init(rocket: Rocket<Build>, database_path: PathBuf) -> fairing::Result {
// TODO: we can inject here more stuff based on our validator-api global config
// struct. Maybe different pool size or timeout intervals?
let mut opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(&database_path)
.create_if_missing(true);
// TODO: do we want auto_vacuum ?
opts.disable_statement_logging();
let connection_pool = match sqlx::SqlitePool::connect_with(opts).await {
Ok(db) => db,
Err(e) => {
error!("Failed to connect to SQLx database: {}", e);
return Err(rocket);
}
};
if let Err(e) = sqlx::migrate!("./migrations").run(&connection_pool).await {
error!("Failed to initialize SQLx database: {}", e);
return Err(rocket);
}
info!("Database migration finished!");
let storage = NodeStatusStorage {
inner: NodeStatusStorageInner { connection_pool },
};
Ok(rocket.manage(storage))
}
pub(crate) fn stage(database_path: PathBuf) -> AdHoc {
AdHoc::try_on_ignite("SQLx Database", |rocket| {
NodeStatusStorage::init(rocket, database_path)
})
}
/// Gets all statuses for particular mixnode (ipv4 and ipv6) that were inserted in last 24h.
async fn get_mixnode_daily_statuses(
&self,
identity: &str,
) -> Result<(Vec<NodeStatus>, Vec<NodeStatus>), NodeStatusApiError> {
let now = OffsetDateTime::now_utc();
let day_ago = now - ONE_DAY;
let ipv4_statuses = self
.inner
.get_mixnode_ipv4_statuses_since(identity, day_ago.unix_timestamp())
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
let ipv6_statuses = self
.inner
.get_mixnode_ipv6_statuses_since(identity, day_ago.unix_timestamp())
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
Ok((ipv4_statuses, ipv6_statuses))
}
/// Gets all statuses for particular gateway (ipv4 and ipv6) that were inserted in last 24h.
async fn get_gateway_daily_statuses(
&self,
identity: &str,
) -> Result<(Vec<NodeStatus>, Vec<NodeStatus>), NodeStatusApiError> {
let now = OffsetDateTime::now_utc();
let day_ago = now - ONE_DAY;
let ipv4_statuses = self
.inner
.get_gateway_ipv4_statuses_since(identity, day_ago.unix_timestamp())
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
let ipv6_statuses = self
.inner
.get_gateway_ipv6_statuses_since(identity, day_ago.unix_timestamp())
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
Ok((ipv4_statuses, ipv6_statuses))
}
/// Tries to construct a status report for mixnode with the specified identity.
pub(crate) async fn construct_mixnode_report(
&self,
identity: &str,
) -> Result<MixnodeStatusReport, NodeStatusApiError> {
let (ipv4_statuses, ipv6_statuses) = self.get_mixnode_daily_statuses(identity).await?;
// if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report
if ipv4_statuses.is_empty() {
return Err(NodeStatusApiError::MixnodeReportNotFound(
identity.to_owned(),
));
}
// now, technically this is not a critical error, but this should have NEVER happened in the first place
// so something super weird is going on
if ipv4_statuses.len() != ipv6_statuses.len() {
error!("Somehow we have different number of ipv4 and ipv6 statuses for mixnode {}! (ipv4: {}, ipv6: {})",
identity,
ipv4_statuses.len(),
ipv6_statuses.len(),
)
}
let mixnode_owner = self
.inner
.get_mixnode_owner(identity)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?
.expect("The node doesn't have an owner even though we have status information on it!");
Ok(MixnodeStatusReport::construct_from_last_day_reports(
identity.to_owned(),
mixnode_owner,
ipv4_statuses,
ipv6_statuses,
))
}
pub(crate) async fn construct_gateway_report(
&self,
identity: &str,
) -> Result<GatewayStatusReport, NodeStatusApiError> {
let (ipv4_statuses, ipv6_statuses) = self.get_gateway_daily_statuses(identity).await?;
// if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report
if ipv4_statuses.is_empty() {
return Err(NodeStatusApiError::GatewayReportNotFound(
identity.to_owned(),
));
}
// now, technically this is not a critical error, but this should have NEVER happened in the first place
// so something super weird is going on
if ipv4_statuses.len() != ipv6_statuses.len() {
error!("Somehow we have different number of ipv4 and ipv6 statuses for gateway {}! (ipv4: {}, ipv6: {})",
identity,
ipv4_statuses.len(),
ipv6_statuses.len(),
)
}
let gateway_owner = self
.inner
.get_gateway_owner(identity)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?
.expect(
"The gateway doesn't have an owner even though we have status information on it!",
);
Ok(GatewayStatusReport::construct_from_last_day_reports(
identity.to_owned(),
gateway_owner,
ipv4_statuses,
ipv6_statuses,
))
}
pub(crate) async fn get_mixnode_uptime_history(
&self,
identity: &str,
) -> Result<MixnodeUptimeHistory, NodeStatusApiError> {
let history = self
.inner
.get_mixnode_historical_uptimes(identity)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
if history.is_empty() {
return Err(NodeStatusApiError::MixnodeUptimeHistoryNotFound(
identity.to_owned(),
));
}
let mixnode_owner = self
.inner
.get_mixnode_owner(identity)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?
.expect("The node doesn't have an owner even though we have uptime history for it!");
Ok(MixnodeUptimeHistory::new(
identity.to_owned(),
mixnode_owner,
history,
))
}
pub(crate) async fn get_gateway_uptime_history(
&self,
identity: &str,
) -> Result<GatewayUptimeHistory, NodeStatusApiError> {
let history = self
.inner
.get_gateway_historical_uptimes(identity)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
if history.is_empty() {
return Err(NodeStatusApiError::GatewayUptimeHistoryNotFound(
identity.to_owned(),
));
}
let gateway_owner = self
.inner
.get_gateway_owner(identity)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?
.expect("The gateway doesn't have an owner even though we have uptime history for it!");
Ok(GatewayUptimeHistory::new(
identity.to_owned(),
gateway_owner,
history,
))
}
// NOTE: this method will go away once we move payments into the validator-api
// it just helps us to get rid of having to query for reports of each node individually
pub(crate) async fn get_all_mixnode_reports(
&self,
) -> Result<Vec<MixnodeStatusReport>, NodeStatusApiError> {
let reports = self
.inner
.get_all_active_mixnodes_statuses()
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?
.into_iter()
.map(|statuses| {
MixnodeStatusReport::construct_from_last_day_reports(
statuses.pub_key,
statuses.owner,
statuses.ipv4_statuses,
statuses.ipv6_statuses,
)
})
.collect();
Ok(reports)
}
// NOTE: this method will go away once we move payments into the validator-api
// it just helps us to get rid of having to query for reports of each node individually
pub(crate) async fn get_all_gateway_reports(
&self,
) -> Result<Vec<GatewayStatusReport>, NodeStatusApiError> {
let reports = self
.inner
.get_all_active_gateways_statuses()
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?
.into_iter()
.map(|statuses| {
GatewayStatusReport::construct_from_last_day_reports(
statuses.pub_key,
statuses.owner,
statuses.ipv4_statuses,
statuses.ipv6_statuses,
)
})
.collect();
Ok(reports)
}
// Used by network monitor
pub(crate) async fn submit_new_statuses(
&self,
mixnode_results: Vec<NodeResult>,
gateway_results: Vec<NodeResult>,
) -> Result<(), NodeStatusApiError> {
self.inner
.submit_new_statuses(mixnode_results, gateway_results)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)
}
// Called on timer/reward script
async fn update_historical_uptimes(
&self,
today_iso_8601: &str,
) -> Result<(), NodeStatusApiError> {
// get statuses for all active mixnodes...
let active_mixnodes_statuses = self
.inner
.get_all_active_mixnodes_statuses()
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
for statuses in active_mixnodes_statuses.into_iter() {
let ipv4_day_up = statuses
.ipv4_statuses
.iter()
.filter(|status| status.up)
.count();
let ipv6_day_up = statuses
.ipv6_statuses
.iter()
.filter(|status| status.up)
.count();
// calculate their uptimes for the last 24h
let ipv4_uptime = Uptime::from_ratio(ipv4_day_up, statuses.ipv4_statuses.len())
.unwrap()
.u8();
let ipv6_uptime = Uptime::from_ratio(ipv6_day_up, statuses.ipv6_statuses.len())
.unwrap()
.u8();
// and insert into the database
self.inner
.insert_mixnode_historical_uptime(
statuses.node_id,
today_iso_8601,
ipv4_uptime,
ipv6_uptime,
)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
}
// get statuses for all active gateways...
let active_gateways_statuses = self
.inner
.get_all_active_gateways_statuses()
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
for statuses in active_gateways_statuses.into_iter() {
let ipv4_day_up = statuses
.ipv4_statuses
.iter()
.filter(|status| status.up)
.count();
let ipv6_day_up = statuses
.ipv6_statuses
.iter()
.filter(|status| status.up)
.count();
// calculate their uptimes for the last 24h
let ipv4_uptime = Uptime::from_ratio(ipv4_day_up, statuses.ipv4_statuses.len())
.unwrap()
.u8();
let ipv6_uptime = Uptime::from_ratio(ipv6_day_up, statuses.ipv6_statuses.len())
.unwrap()
.u8();
// and insert into the database
self.inner
.insert_gateway_historical_uptime(
statuses.node_id,
today_iso_8601,
ipv4_uptime,
ipv6_uptime,
)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)?;
}
Ok(())
}
async fn check_if_historical_uptimes_exist_for_date(
&self,
date_iso_8601: &str,
) -> Result<bool, NodeStatusApiError> {
self.inner
.check_for_historical_uptime_existence(date_iso_8601)
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)
}
// Called on timer/reward script
async fn purge_old_statuses(&self) -> Result<(), NodeStatusApiError> {
self.inner
.purge_old_statuses()
.await
.map_err(|_| NodeStatusApiError::InternalDatabaseError)
}
pub(crate) async fn daily_chores(&self) -> Result<bool, NodeStatusApiError> {
let today_iso_8601 = OffsetDateTime::now_utc().date().to_string();
// if we have already performed the update for today's date, don't do anything
if self
.check_if_historical_uptimes_exist_for_date(&today_iso_8601)
.await?
{
Ok(false)
} else {
info!(
"Updating historical daily uptimes of all nodes and purging old status reports..."
);
self.update_historical_uptimes(&today_iso_8601).await?;
self.purge_old_statuses().await?;
Ok(true)
}
}
}
// all SQL goes here
impl NodeStatusStorageInner {
/// Tries to obtain owner value of given mixnode given its identity
async fn get_mixnode_owner(&self, identity: &str) -> Result<Option<String>, sqlx::Error> {
let owner = sqlx::query!(
"SELECT owner FROM mixnode_details WHERE pub_key = ?",
identity
)
.fetch_optional(&self.connection_pool)
.await?
.map(|row| row.owner);
Ok(owner)
}
/// Tries to obtain owner value of given gateway given its identity
async fn get_gateway_owner(&self, identity: &str) -> Result<Option<String>, sqlx::Error> {
let owner = sqlx::query!(
"SELECT owner FROM gateway_details WHERE pub_key = ?",
identity
)
.fetch_optional(&self.connection_pool)
.await?
.map(|row| row.owner);
Ok(owner)
}
/// Gets all ipv4 statuses for mixnode with particular identity that were inserted
/// into the database after the specified unix timestamp.
async fn get_mixnode_ipv4_statuses_since(
&self,
identity: &str,
timestamp: UnixTimestamp,
) -> Result<Vec<NodeStatus>, sqlx::Error> {
sqlx::query_as!(
NodeStatus,
r#"
SELECT timestamp, up
FROM mixnode_ipv4_status
JOIN mixnode_details
ON mixnode_ipv4_status.mixnode_details_id = mixnode_details.id
WHERE mixnode_details.pub_key=? AND mixnode_ipv4_status.timestamp > ?;
"#,
identity,
timestamp,
)
.fetch_all(&self.connection_pool)
.await
}
/// Gets all ipv6 statuses for mixnode with particular identity that were inserted
/// into the database after the specified unix timestamp.
async fn get_mixnode_ipv6_statuses_since(
&self,
identity: &str,
timestamp: UnixTimestamp,
) -> Result<Vec<NodeStatus>, sqlx::Error> {
sqlx::query_as!(
NodeStatus,
r#"
SELECT timestamp, up
FROM mixnode_ipv6_status
JOIN mixnode_details
ON mixnode_ipv6_status.mixnode_details_id = mixnode_details.id
WHERE mixnode_details.pub_key=? AND mixnode_ipv6_status.timestamp > ?;
"#,
identity,
timestamp
)
.fetch_all(&self.connection_pool)
.await
}
/// Gets all ipv4 statuses for gateway with particular identity that were inserted
/// into the database after the specified unix timestamp.
async fn get_gateway_ipv4_statuses_since(
&self,
identity: &str,
timestamp: UnixTimestamp,
) -> Result<Vec<NodeStatus>, sqlx::Error> {
sqlx::query_as!(
NodeStatus,
r#"
SELECT timestamp, up
FROM gateway_ipv4_status
JOIN gateway_details
ON gateway_ipv4_status.gateway_details_id = gateway_details.id
WHERE gateway_details.pub_key=? AND gateway_ipv4_status.timestamp > ?;
"#,
identity,
timestamp,
)
.fetch_all(&self.connection_pool)
.await
}
/// Gets all ipv6 statuses for gateway with particular identity that were inserted
/// into the database after the specified unix timestamp.
async fn get_gateway_ipv6_statuses_since(
&self,
identity: &str,
timestamp: UnixTimestamp,
) -> Result<Vec<NodeStatus>, sqlx::Error> {
sqlx::query_as!(
NodeStatus,
r#"
SELECT timestamp, up
FROM gateway_ipv6_status
JOIN gateway_details
ON gateway_ipv6_status.gateway_details_id = gateway_details.id
WHERE gateway_details.pub_key=? AND gateway_ipv6_status.timestamp > ?;
"#,
identity,
timestamp
)
.fetch_all(&self.connection_pool)
.await
}
/// Gets the historical daily uptime associated with the particular mixnode
async fn get_mixnode_historical_uptimes(
&self,
identity: &str,
) -> Result<Vec<HistoricalUptime>, sqlx::Error> {
let uptimes = sqlx::query!(
r#"
SELECT date, ipv4_uptime, ipv6_uptime
FROM mixnode_historical_uptime
JOIN mixnode_details
ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id
WHERE mixnode_details.pub_key = ?
ORDER BY date ASC
"#,
identity
)
.fetch_all(&self.connection_pool)
.await?
.into_iter()
// filter out nodes with valid uptime (in theory all should be 100% valid since we insert them ourselves, but
// better safe than sorry and not use an unwrap)
.filter_map(|row| {
Uptime::try_from(row.ipv4_uptime)
.ok()
.map(|ipv4_uptime| {
Uptime::try_from(row.ipv6_uptime)
.ok()
.map(|ipv6_uptime| HistoricalUptime {
date: row.date,
ipv4_uptime,
ipv6_uptime,
})
})
.flatten()
})
.collect();
Ok(uptimes)
}
/// Gets the historical daily uptime associated with the particular gateway
async fn get_gateway_historical_uptimes(
&self,
identity: &str,
) -> Result<Vec<HistoricalUptime>, sqlx::Error> {
let uptimes = sqlx::query!(
r#"
SELECT date, ipv4_uptime, ipv6_uptime
FROM gateway_historical_uptime
JOIN gateway_details
ON gateway_historical_uptime.gateway_details_id = gateway_details.id
WHERE gateway_details.pub_key = ?
ORDER BY date ASC
"#,
identity
)
.fetch_all(&self.connection_pool)
.await?
.into_iter()
// filter out nodes with valid uptime (in theory all should be 100% valid since we insert them ourselves, but
// better safe than sorry and not use an unwrap)
.filter_map(|row| {
Uptime::try_from(row.ipv4_uptime)
.ok()
.map(|ipv4_uptime| {
Uptime::try_from(row.ipv6_uptime)
.ok()
.map(|ipv6_uptime| HistoricalUptime {
date: row.date,
ipv4_uptime,
ipv6_uptime,
})
})
.flatten()
})
.collect();
Ok(uptimes)
}
// NOTE: this method will go away once we move payments into the validator-api
// it just helps us to get rid of having to query for reports of each node individually
/// Returns public key, owner and id of all mixnodes that have had any ipv4 statuses submitted
/// since provided timestamp.
async fn get_all_active_mixnodes(
&self,
timestamp: UnixTimestamp,
) -> Result<Vec<ActiveNode>, sqlx::Error> {
// find mixnode details of all nodes that have had at least 1 ipv4 status since the provided
// timestamp
// TODO: I dont know if theres a potential issue of if we have a lot of inactive nodes that
// haven't mixed in ages, they might increase the query times?
sqlx::query_as!(
ActiveNode,
r#"
SELECT DISTINCT pub_key, owner, id
FROM mixnode_details
JOIN mixnode_ipv4_status
ON mixnode_details.id = mixnode_ipv4_status.mixnode_details_id
WHERE EXISTS (
SELECT 1 FROM mixnode_ipv4_status WHERE timestamp > ?
)
"#,
timestamp
)
.fetch_all(&self.connection_pool)
.await
}
// NOTE: this method will go away once we move payments into the validator-api
// it just helps us to get rid of having to query for reports of each node individually
/// Returns public key, owner and id of all gateways that have had any ipv4 statuses submitted
/// since provided timestamp.
async fn get_all_active_gateways(
&self,
timestamp: UnixTimestamp,
) -> Result<Vec<ActiveNode>, sqlx::Error> {
sqlx::query_as!(
ActiveNode,
r#"
SELECT DISTINCT pub_key, owner, id
FROM gateway_details
JOIN gateway_ipv4_status
ON gateway_details.id = gateway_ipv4_status.gateway_details_id
WHERE EXISTS (
SELECT 1 FROM gateway_ipv4_status WHERE timestamp > ?
)
"#,
timestamp
)
.fetch_all(&self.connection_pool)
.await
}
/// Gets all ipv4 statuses for mixnode with particular id that were inserted
/// into the database after the specified unix timestamp.
async fn get_mixnode_ipv4_statuses_since_by_id(
&self,
id: i64,
timestamp: UnixTimestamp,
) -> Result<Vec<NodeStatus>, sqlx::Error> {
sqlx::query_as!(
NodeStatus,
r#"
SELECT timestamp, up
FROM mixnode_ipv4_status
WHERE mixnode_details_id=? AND timestamp > ?;
"#,
id,
timestamp
)
.fetch_all(&self.connection_pool)
.await
}
/// Gets all ipv6 statuses for mixnode with particular id that were inserted
/// into the database after the specified unix timestamp.
async fn get_mixnode_ipv6_statuses_since_by_id(
&self,
id: i64,
timestamp: UnixTimestamp,
) -> Result<Vec<NodeStatus>, sqlx::Error> {
sqlx::query_as!(
NodeStatus,
r#"
SELECT timestamp, up
FROM mixnode_ipv6_status
WHERE mixnode_details_id=? AND timestamp > ?;
"#,
id,
timestamp
)
.fetch_all(&self.connection_pool)
.await
}
/// Gets all ipv4 statuses for gateway with particular id that were inserted
/// into the database after the specified unix timestamp.
async fn get_gateway_ipv4_statuses_since_by_id(
&self,
id: i64,
timestamp: UnixTimestamp,
) -> Result<Vec<NodeStatus>, sqlx::Error> {
sqlx::query_as!(
NodeStatus,
r#"
SELECT timestamp, up
FROM gateway_ipv4_status
WHERE gateway_details_id=? AND timestamp > ?;
"#,
id,
timestamp
)
.fetch_all(&self.connection_pool)
.await
}
/// Gets all ipv6 statuses for gateway with particular id that were inserted
/// into the database after the specified unix timestamp.
async fn get_gateway_ipv6_statuses_since_by_id(
&self,
id: i64,
timestamp: UnixTimestamp,
) -> Result<Vec<NodeStatus>, sqlx::Error> {
sqlx::query_as!(
NodeStatus,
r#"
SELECT timestamp, up
FROM gateway_ipv6_status
WHERE gateway_details_id=? AND timestamp > ?;
"#,
id,
timestamp
)
.fetch_all(&self.connection_pool)
.await
}
// NOTE: this method will go away once we move payments into the validator-api
// it just helps us to get rid of having to query for reports of each node individually
// TODO: should that live on the 'Inner' struct or should it rather exist on the actual storage struct
// since technically it doesn't touch any SQL directly
async fn get_all_active_mixnodes_statuses(
&self,
) -> Result<Vec<ActiveNodeDayStatuses>, sqlx::Error> {
let now = OffsetDateTime::now_utc();
let day_ago = (now - ONE_DAY).unix_timestamp();
let active_nodes = self.get_all_active_mixnodes(day_ago).await?;
let mut active_day_statuses = Vec::with_capacity(active_nodes.len());
for active_node in active_nodes.into_iter() {
let ipv4_statuses = self
.get_mixnode_ipv4_statuses_since_by_id(active_node.id, day_ago)
.await?;
let ipv6_statuses = self
.get_mixnode_ipv6_statuses_since_by_id(active_node.id, day_ago)
.await?;
let statuses = ActiveNodeDayStatuses {
pub_key: active_node.pub_key,
owner: active_node.owner,
node_id: active_node.id,
ipv4_statuses,
ipv6_statuses,
};
active_day_statuses.push(statuses);
}
Ok(active_day_statuses)
}
// NOTE: this method will go away once we move payments into the validator-api
// it just helps us to get rid of having to query for reports of each node individually
// TODO: should that live on the 'Inner' struct or should it rather exist on the actual storage struct
// since technically it doesn't touch any SQL directly
async fn get_all_active_gateways_statuses(
&self,
) -> Result<Vec<ActiveNodeDayStatuses>, sqlx::Error> {
let now = OffsetDateTime::now_utc();
let day_ago = (now - ONE_DAY).unix_timestamp();
let active_nodes = self.get_all_active_gateways(day_ago).await?;
let mut active_day_statuses = Vec::with_capacity(active_nodes.len());
for active_node in active_nodes.into_iter() {
let ipv4_statuses = self
.get_gateway_ipv4_statuses_since_by_id(active_node.id, day_ago)
.await?;
let ipv6_statuses = self
.get_gateway_ipv6_statuses_since_by_id(active_node.id, day_ago)
.await?;
let statuses = ActiveNodeDayStatuses {
pub_key: active_node.pub_key,
owner: active_node.owner,
node_id: active_node.id,
ipv4_statuses,
ipv6_statuses,
};
active_day_statuses.push(statuses);
}
Ok(active_day_statuses)
}
/// Tries to submit [`NodeResult`] from the network monitor to the database.
async fn submit_new_statuses(
&self,
mixnode_results: Vec<NodeResult>,
gateway_results: Vec<NodeResult>,
) -> Result<(), sqlx::Error> {
// TODO: lower that to debug before creating PR
info!("Submitting new node results to the database. There are {} mixnode results and {} gateway results", mixnode_results.len(), gateway_results.len());
let now = OffsetDateTime::now_utc().unix_timestamp();
// insert it all in a transaction to make sure all nodes are updated at the same time
// (plus it's a nice guard against new nodes)
let mut tx = self.connection_pool.begin().await?;
for mixnode_result in mixnode_results {
// if mixnode info doesn't exist, insert it and get its id
// TODO: the potential "problem" (if you can call it that way) is that if entry DID exist
// then the id field will be incremented for the next node we create thus we will
// have gaps in our ids. ask @DH if that's fine (I don't see why not because nodes
// are still correctly ordered and you can get their total number with a simple query
// and we'd have to run the system until the heat death of the universe to run out of id numbers)
let mixnode_id = sqlx::query!(
r#"
INSERT OR IGNORE INTO mixnode_details(pub_key, owner) VALUES (?, ?);
SELECT id FROM mixnode_details WHERE pub_key = ?;
"#,
mixnode_result.pub_key,
mixnode_result.owner,
mixnode_result.pub_key,
)
.fetch_one(&mut tx)
.await?
.id;
// insert ipv4 status
sqlx::query!(
r#"
INSERT INTO mixnode_ipv4_status (mixnode_details_id, up, timestamp) VALUES (?, ?, ?);
"#,
mixnode_id,
mixnode_result.working_ipv4,
now
)
.execute(&mut tx)
.await?;
// insert ipv6 status
sqlx::query!(
r#"
INSERT INTO mixnode_ipv6_status (mixnode_details_id, up, timestamp) VALUES (?, ?, ?);
"#,
mixnode_id,
mixnode_result.working_ipv6,
now
)
.execute(&mut tx)
.await?;
}
// repeat the procedure for gateways
for gateway_result in gateway_results {
// if gateway info doesn't exist, insert it and get its id
// same ID "problem" as described for mixnode insertion
let gateway_id = sqlx::query!(
r#"
INSERT OR IGNORE INTO gateway_details(pub_key, owner) VALUES (?, ?);
SELECT id FROM gateway_details WHERE pub_key = ?;
"#,
gateway_result.pub_key,
gateway_result.owner,
gateway_result.pub_key,
)
.fetch_one(&mut tx)
.await?
.id;
// insert ipv4 status
sqlx::query!(
r#"
INSERT INTO gateway_ipv4_status (gateway_details_id, up, timestamp) VALUES (?, ?, ?);
"#,
gateway_id,
gateway_result.working_ipv4,
now
)
.execute(&mut tx)
.await?;
// insert ipv6 status
sqlx::query!(
r#"
INSERT INTO gateway_ipv6_status (gateway_details_id, up, timestamp) VALUES (?, ?, ?);
"#,
gateway_id,
gateway_result.working_ipv6,
now
)
.execute(&mut tx)
.await?;
}
// finally commit the transaction
tx.commit().await
}
/// Checks whether there are already any historical uptimes with this particular date.
async fn check_for_historical_uptime_existence(
&self,
today_iso_8601: &str,
) -> Result<bool, sqlx::Error> {
sqlx::query!(
"SELECT EXISTS (SELECT 1 FROM mixnode_historical_uptime WHERE date = ?) AS 'exists'",
today_iso_8601
)
.fetch_one(&self.connection_pool)
.await
.map(|result| result.exists == 1)
}
/// Creates new entry for mixnode historical uptime
async fn insert_mixnode_historical_uptime(
&self,
node_id: i64,
date: &str,
ipv4_uptime: u8,
ipv6_uptime: u8,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO mixnode_historical_uptime(mixnode_details_id, date, ipv4_uptime, ipv6_uptime) VALUES (?, ?, ?, ?)",
node_id,
date,
ipv4_uptime,
ipv6_uptime,
).execute(&self.connection_pool).await?;
Ok(())
}
/// Creates new entry for gatewy historical uptime
async fn insert_gateway_historical_uptime(
&self,
node_id: i64,
date: &str,
ipv4_uptime: u8,
ipv6_uptime: u8,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO gateway_historical_uptime(gateway_details_id, date, ipv4_uptime, ipv6_uptime) VALUES (?, ?, ?, ?)",
node_id,
date,
ipv4_uptime,
ipv6_uptime,
).execute(&self.connection_pool).await?;
Ok(())
}
/// Removes all statuses from the database that are older than 48h.
async fn purge_old_statuses(&self) -> Result<(), sqlx::Error> {
let now = OffsetDateTime::now_utc();
let two_days_ago = (now - 2 * ONE_DAY).unix_timestamp();
sqlx::query!(
"DELETE FROM mixnode_ipv4_status WHERE timestamp < ?",
two_days_ago
)
.execute(&self.connection_pool)
.await?;
sqlx::query!(
"DELETE FROM mixnode_ipv6_status WHERE timestamp < ?",
two_days_ago
)
.execute(&self.connection_pool)
.await?;
sqlx::query!(
"DELETE FROM gateway_ipv4_status WHERE timestamp < ?",
two_days_ago
)
.execute(&self.connection_pool)
.await?;
sqlx::query!(
"DELETE FROM gateway_ipv6_status WHERE timestamp < ?",
two_days_ago
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}