queries for detailed node statuses with broken pagination

This commit is contained in:
Jędrzej Stuczyński
2024-05-17 17:02:29 +01:00
parent e5f41731ae
commit 89e34b4fd3
10 changed files with 611 additions and 15 deletions
+1
View File
@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
pub mod coconut;
pub mod models;
pub mod pagination;
pub trait Deprecatable {
fn deprecate(self) -> Deprecated<Self>
+26
View File
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::pagination::PaginatedResponse;
use cosmwasm_std::{Addr, Coin, Decimal};
use nym_mixnet_contract_common::families::FamilyHead;
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
@@ -569,3 +570,28 @@ pub struct SignerInformationResponse {
pub verification_key: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, Default)]
pub struct TestNode {
pub node_id: Option<u32>,
pub identity_key: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TestRoute {
pub gateway: TestNode,
pub layer1: TestNode,
pub layer2: TestNode,
pub layer3: TestNode,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TestResult {
pub monitor_run_id: i64,
pub timestamp: i64,
pub reliability: Option<u8>,
pub test_routes: Vec<TestRoute>,
}
pub type MixnodeTestResultResponse = PaginatedResponse<TestResult>;
pub type GatewayTestResultResponse = PaginatedResponse<TestResult>;
@@ -0,0 +1,18 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct Pagination {
pub total: usize,
pub page: u32,
pub size: usize,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct PaginatedResponse<T> {
pub pagination: Pagination,
pub data: Vec<T>,
}
+2
View File
@@ -53,6 +53,8 @@ pub(crate) fn node_status_routes(
routes::get_active_set_detailed,
routes::get_gateways_detailed,
routes::get_gateways_detailed_unfiltered,
routes::unstable::mixnode_test_results,
routes::unstable::gateway_test_results,
]
} else {
// in the minimal variant we would not have access to endpoints relying on existence
+298 -13
View File
@@ -1,6 +1,19 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_api_requests::models::{
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated,
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse,
UptimeResponse,
};
use nym_mixnet_contract_common::MixId;
use rocket::serde::json::Json;
use rocket::State;
use rocket_okapi::openapi;
use super::helpers::_get_gateways_detailed;
use super::NodeStatusCache;
use crate::node_status_api::helpers::{
@@ -15,18 +28,6 @@ use crate::node_status_api::helpers::{
use crate::node_status_api::models::ErrorResponse;
use crate::storage::NymApiStorage;
use crate::NymContractCache;
use nym_api_requests::models::{
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated,
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse,
UptimeResponse,
};
use nym_mixnet_contract_common::MixId;
use rocket::serde::json::Json;
use rocket::State;
use rocket_okapi::openapi;
#[openapi(tag = "status")]
#[get("/gateway/<identity>/report")]
@@ -227,3 +228,287 @@ pub async fn get_gateways_detailed_unfiltered(
) -> Json<Vec<GatewayBondAnnotated>> {
Json(_get_gateways_detailed_unfiltered(cache).await)
}
pub mod unstable {
use crate::node_status_api::models::ErrorResponse;
use crate::support::http::helpers::PaginationRequest;
use crate::support::storage::NymApiStorage;
use nym_api_requests::models::{
GatewayTestResultResponse, MixnodeTestResultResponse, TestNode, TestResult, TestRoute,
};
use nym_api_requests::pagination::Pagination;
use nym_mixnet_contract_common::MixId;
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
use rocket_okapi::openapi;
use std::cmp::min;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub type DbId = i64;
// a simply in-memory cache of node details
#[derive(Debug, Default)]
pub struct NodeInfoCache {
inner: Arc<RwLock<NodeInfoCacheInner>>,
}
impl NodeInfoCache {
async fn get_mix_node_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode {
{
let read_guard = self.inner.read().await;
if let Some(cached) = read_guard.mixnodes.get(&db_id) {
trace!("cache hit for mixnode {db_id}");
return cached.clone();
}
}
trace!("cache miss for mixnode {db_id}");
let mut write_guard = self.inner.write().await;
// double-check the cache in case somebody already updated it while we were waiting for the lock
if let Some(cached) = write_guard.mixnodes.get(&db_id) {
return cached.clone();
}
let details = match storage.get_mixnode_details_by_db_id(db_id).await {
Ok(Some(details)) => details.into(),
Ok(None) => {
error!("somebody has been messing with the database! details for mixnode with database id {db_id} have been removed!");
TestNode::default()
}
Err(err) => {
// don't insert into the cache in case another request is successful
error!("failed to retrieve details for mixnode {db_id}: {err}");
return TestNode::default();
}
};
write_guard.mixnodes.insert(db_id, details.clone());
details
}
async fn get_gateway_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode {
{
let read_guard = self.inner.read().await;
if let Some(cached) = read_guard.gateways.get(&db_id) {
trace!("cache hit for gateway {db_id}");
return cached.clone();
}
}
trace!("cache miss for gateway {db_id}");
let mut write_guard = self.inner.write().await;
// double-check the cache in case somebody already updated it while we were waiting for the lock
if let Some(cached) = write_guard.gateways.get(&db_id) {
return cached.clone();
}
let details = match storage.get_gateway_details_by_db_id(db_id).await {
Ok(Some(details)) => details.into(),
Ok(None) => {
error!("somebody has been messing with the database! details for gateway with database id {db_id} have been removed!");
TestNode::default()
}
Err(err) => {
// don't insert into the cache in case another request is successful
error!("failed to retrieve details for gateway {db_id}: {err}");
return TestNode::default();
}
};
write_guard.gateways.insert(db_id, details.clone());
details
}
}
#[derive(Debug, Default)]
struct NodeInfoCacheInner {
mixnodes: HashMap<DbId, TestNode>,
gateways: HashMap<DbId, TestNode>,
}
const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100;
const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50;
async fn _mixnode_test_results(
mix_id: MixId,
page: u32,
per_page: u32,
info_cache: &State<NodeInfoCache>,
storage: &State<NymApiStorage>,
) -> anyhow::Result<MixnodeTestResultResponse> {
// convert to db offset
// we're paging from page 0 like civilised people,
// so we have to skip (page * per_page) results
let offset = page * per_page;
let limit = per_page;
let raw_results = storage
.get_mixnode_detailed_statuses(mix_id, limit, offset)
.await?;
let total = match raw_results.first() {
None => 0,
Some(r) => storage.get_mixnode_detailed_statuses_count(r.db_id).await?,
};
let mut test_results = HashMap::new();
for result in raw_results {
let entry = test_results
.entry(result.monitor_run_id)
.or_insert(TestResult {
monitor_run_id: result.monitor_run_id,
timestamp: result.timestamp,
reliability: result.reliability,
test_routes: vec![],
});
let gateway = info_cache
.get_gateway_details(result.gateway_id, storage)
.await;
let layer1 = info_cache
.get_mix_node_details(result.layer1_mix_id, storage)
.await;
let layer2 = info_cache
.get_mix_node_details(result.layer2_mix_id, storage)
.await;
let layer3 = info_cache
.get_mix_node_details(result.layer3_mix_id, storage)
.await;
entry.test_routes.push(TestRoute {
gateway,
layer1,
layer2,
layer3,
})
}
Ok(MixnodeTestResultResponse {
pagination: Pagination {
total,
page,
size: test_results.len(),
},
data: test_results.into_values().collect(),
})
}
#[openapi(tag = "UNSTABLE - DO **NOT** USE")]
#[get("/mixnodes/<mix_id>/test-results?<pagination..>")]
pub async fn mixnode_test_results(
mix_id: MixId,
pagination: PaginationRequest,
info_cache: &State<NodeInfoCache>,
storage: &State<NymApiStorage>,
) -> Result<Json<MixnodeTestResultResponse>, ErrorResponse> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
.per_page
.unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE),
MAX_TEST_RESULTS_PAGE_SIZE,
);
match _mixnode_test_results(mix_id, page, per_page, info_cache, storage).await {
Ok(res) => Ok(Json(res)),
Err(err) => Err(ErrorResponse::new(
format!("failed to retrieve mixnode test results for node {mix_id}: {err}"),
Status::InternalServerError,
)),
}
}
async fn _gateway_test_results(
gateway_identity: &str,
page: u32,
per_page: u32,
info_cache: &State<NodeInfoCache>,
storage: &State<NymApiStorage>,
) -> anyhow::Result<GatewayTestResultResponse> {
// convert to db offset
// we're paging from page 0 like civilised people,
// so we have to skip (page * per_page) results
let offset = page * per_page;
let limit = per_page;
let raw_results = storage
.get_gateway_detailed_statuses(gateway_identity, limit, offset)
.await?;
let total = match raw_results.first() {
None => 0,
Some(r) => storage.get_gateway_detailed_statuses_count(r.db_id).await?,
};
let mut test_results = HashMap::new();
for result in raw_results {
let entry = test_results
.entry(result.monitor_run_id)
.or_insert(TestResult {
monitor_run_id: result.monitor_run_id,
timestamp: result.timestamp,
reliability: result.reliability,
test_routes: vec![],
});
let gateway = info_cache
.get_gateway_details(result.gateway_id, storage)
.await;
let layer1 = info_cache
.get_mix_node_details(result.layer1_mix_id, storage)
.await;
let layer2 = info_cache
.get_mix_node_details(result.layer2_mix_id, storage)
.await;
let layer3 = info_cache
.get_mix_node_details(result.layer3_mix_id, storage)
.await;
entry.test_routes.push(TestRoute {
gateway,
layer1,
layer2,
layer3,
})
}
Ok(GatewayTestResultResponse {
pagination: Pagination {
total,
page,
size: test_results.len(),
},
data: test_results.into_values().collect(),
})
}
#[openapi(tag = "UNSTABLE - DO **NOT** USE")]
#[get("/gateways/<gateway_identity>/test-results?<pagination..>")]
pub async fn gateway_test_results(
gateway_identity: &str,
pagination: PaginationRequest,
info_cache: &State<NodeInfoCache>,
storage: &State<NymApiStorage>,
) -> Result<Json<GatewayTestResultResponse>, ErrorResponse> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
.per_page
.unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE),
MAX_TEST_RESULTS_PAGE_SIZE,
);
match _gateway_test_results(gateway_identity, page, per_page, info_cache, storage).await {
Ok(res) => Ok(Json(res)),
Err(err) => Err(ErrorResponse::new(
format!(
"failed to retrieve mixnode test results for gateway {gateway_identity}: {err}"
),
Status::InternalServerError,
)),
}
}
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, FromForm, Debug, JsonSchema)]
pub struct PaginationRequest {
pub page: Option<u32>,
pub per_page: Option<u32>,
}
+4 -1
View File
@@ -7,6 +7,7 @@ use crate::coconut::{self, comm::QueryCommunicationChannel};
use crate::network::models::NetworkDetails;
use crate::network::network_routes;
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::routes::unstable;
use crate::node_status_api::{self, NodeStatusCache};
use crate::nym_contract_cache::cache::NymContractCache;
use crate::status::{api_status_routes, ApiStatusState, SignerState};
@@ -23,6 +24,7 @@ use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};
use rocket_okapi::mount_endpoints_and_merged_docs;
use rocket_okapi::swagger_ui::make_swagger_ui;
pub(crate) mod helpers;
pub(crate) mod openapi;
pub(crate) async fn setup_rocket(
@@ -57,7 +59,8 @@ pub(crate) async fn setup_rocket(
.attach(setup_cors()?)
.attach(NymContractCache::stage())
.attach(NodeStatusCache::stage())
.attach(CirculatingSupplyCache::stage(mix_denom.clone()));
.attach(CirculatingSupplyCache::stage(mix_denom.clone()))
.manage(unstable::NodeInfoCache::default());
// This is not a very nice approach. A lazy value would be more suitable, but that's still
// a nightly feature: https://github.com/rust-lang/rust/issues/74465
+127 -1
View File
@@ -4,7 +4,8 @@ use crate::network_monitor::monitor::summary_producer::{GatewayResult, MixnodeRe
use crate::node_status_api::models::{HistoricalUptime, Uptime};
use crate::node_status_api::utils::{ActiveGatewayStatuses, ActiveMixnodeStatuses};
use crate::support::storage::models::{
ActiveGateway, ActiveMixnode, NodeStatus, RewardingReport, TestingRoute,
ActiveGateway, ActiveMixnode, GatewayDetails, MixnodeDetails, NodeStatus, RewardingReport,
TestedGatewayStatus, TestedMixnodeStatus, TestingRoute,
};
use nym_mixnet_contract_common::{EpochId, IdentityKey, MixId};
@@ -971,4 +972,129 @@ impl StorageManager {
Ok(active_day_statuses)
}
pub(crate) async fn get_mixnode_details_by_db_id(
&self,
id: i64,
) -> Result<Option<MixnodeDetails>, sqlx::Error> {
sqlx::query_as!(
MixnodeDetails,
"SELECT * FROM mixnode_details WHERE id = ?",
id
)
.fetch_optional(&self.connection_pool)
.await
}
pub(crate) async fn get_gateway_details_by_db_id(
&self,
id: i64,
) -> Result<Option<GatewayDetails>, sqlx::Error> {
sqlx::query_as!(
GatewayDetails,
"SELECT * FROM gateway_details WHERE id = ?",
id
)
.fetch_optional(&self.connection_pool)
.await
}
pub(crate) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result<i32, sqlx::Error> {
sqlx::query!(
r#"
SELECT COUNT(*) as count
FROM mixnode_status
JOIN monitor_run ON mixnode_status.timestamp = monitor_run.timestamp
WHERE mixnode_details_id = ?
"#,
db_id
)
.fetch_one(&self.connection_pool)
.await
.map(|record| record.count)
}
pub(crate) async fn get_mixnode_statuses(
&self,
mix_id: MixId,
limit: u32,
offset: u32,
) -> Result<Vec<TestedMixnodeStatus>, sqlx::Error> {
sqlx::query_as!(
TestedMixnodeStatus,
r#"
SELECT
mixnode_details.id as "db_id",
mix_id as "mix_id!",
identity_key,
reliability as "reliability: u8",
monitor_run.timestamp as "timestamp!",
gateway_id as "gateway_id!",
layer1_mix_id as "layer1_mix_id!",
layer2_mix_id as "layer2_mix_id!",
layer3_mix_id as "layer3_mix_id!",
monitor_run_id as "monitor_run_id!"
FROM mixnode_status
JOIN mixnode_details ON mixnode_status.mixnode_details_id = mixnode_details.id
JOIN monitor_run ON mixnode_status.timestamp = monitor_run.timestamp
JOIN testing_route ON monitor_run.id = testing_route.monitor_run_id
WHERE mix_id = ?
LIMIT ? OFFSET ?
"#,
mix_id,
limit,
offset
)
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn get_gateway_statuses_count(&self, db_id: i64) -> Result<i32, sqlx::Error> {
sqlx::query!(
r#"
SELECT COUNT(*) as count
FROM gateway_status
JOIN monitor_run ON gateway_status.timestamp = monitor_run.timestamp
WHERE gateway_details_id = ?
"#,
db_id
)
.fetch_one(&self.connection_pool)
.await
.map(|record| record.count)
}
pub(crate) async fn get_gateway_statuses(
&self,
gateway_identity: &str,
limit: u32,
offset: u32,
) -> Result<Vec<TestedGatewayStatus>, sqlx::Error> {
sqlx::query_as!(
TestedGatewayStatus,
r#"
SELECT
gateway_details.id as "db_id",
identity as "identity_key",
reliability as "reliability: u8",
monitor_run.timestamp as "timestamp!",
gateway_id as "gateway_id!",
layer1_mix_id as "layer1_mix_id!",
layer2_mix_id as "layer2_mix_id!",
layer3_mix_id as "layer3_mix_id!",
monitor_run_id as "monitor_run_id!"
FROM gateway_status
JOIN gateway_details ON gateway_status.gateway_details_id = gateway_details.id
JOIN monitor_run ON gateway_status.timestamp = monitor_run.timestamp
JOIN testing_route ON monitor_run.id = testing_route.monitor_run_id
WHERE identity = ?
LIMIT ? OFFSET ?
"#,
gateway_identity,
limit,
offset
)
.fetch_all(&self.connection_pool)
.await
}
}
+65
View File
@@ -10,6 +10,9 @@ use crate::node_status_api::models::{
use crate::node_status_api::{ONE_DAY, ONE_HOUR};
use crate::storage::manager::StorageManager;
use crate::storage::models::{NodeStatus, TestingRoute};
use crate::support::storage::models::{
GatewayDetails, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus,
};
use nym_mixnet_contract_common::MixId;
use rocket::fairing::AdHoc;
use sqlx::ConnectOptions;
@@ -716,4 +719,66 @@ impl NymApiStorage {
.await
.map_err(|err| err.into())
}
pub(crate) async fn get_mixnode_details_by_db_id(
&self,
id: i64,
) -> Result<Option<MixnodeDetails>, NymApiStorageError> {
Ok(self.manager.get_mixnode_details_by_db_id(id).await?)
}
pub(crate) async fn get_gateway_details_by_db_id(
&self,
id: i64,
) -> Result<Option<GatewayDetails>, NymApiStorageError> {
Ok(self.manager.get_gateway_details_by_db_id(id).await?)
}
pub(crate) async fn get_mixnode_detailed_statuses_count(
&self,
db_id: i64,
) -> Result<usize, NymApiStorageError> {
Ok(self
.manager
.get_mixnode_statuses_count(db_id)
.await?
.try_into()
.unwrap_or(usize::MAX))
}
pub(crate) async fn get_mixnode_detailed_statuses(
&self,
mix_id: MixId,
limit: u32,
offset: u32,
) -> Result<Vec<TestedMixnodeStatus>, NymApiStorageError> {
Ok(self
.manager
.get_mixnode_statuses(mix_id, limit, offset)
.await?)
}
pub(crate) async fn get_gateway_detailed_statuses_count(
&self,
db_id: i64,
) -> Result<usize, NymApiStorageError> {
Ok(self
.manager
.get_gateway_statuses_count(db_id)
.await?
.try_into()
.unwrap_or(usize::MAX))
}
pub(crate) async fn get_gateway_detailed_statuses(
&self,
gateway_identity: &str,
limit: u32,
offset: u32,
) -> Result<Vec<TestedGatewayStatus>, NymApiStorageError> {
Ok(self
.manager
.get_gateway_statuses(gateway_identity, limit, offset)
.await?)
}
}
+59
View File
@@ -1,6 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_api_requests::models::TestNode;
use nym_mixnet_contract_common::MixId;
// Internally used struct to catch results from the database to calculate uptimes for given mixnode/gateway
@@ -49,3 +50,61 @@ pub(crate) struct RewardingReport {
pub(crate) eligible_mixnodes: u32,
}
pub struct MixnodeDetails {
pub id: i64,
pub mix_id: i64,
pub owner: String,
pub identity_key: String,
}
impl From<MixnodeDetails> for TestNode {
fn from(value: MixnodeDetails) -> Self {
TestNode {
node_id: Some(value.mix_id.try_into().unwrap_or(u32::MAX)),
identity_key: Some(value.identity_key),
}
}
}
pub struct GatewayDetails {
pub id: i64,
pub owner: String,
pub identity: String,
}
impl From<GatewayDetails> for TestNode {
fn from(value: GatewayDetails) -> Self {
TestNode {
node_id: None,
identity_key: Some(value.identity),
}
}
}
pub struct TestedMixnodeStatus {
pub db_id: i64,
pub mix_id: i64,
pub identity_key: String,
pub reliability: Option<u8>,
pub timestamp: i64,
pub gateway_id: i64,
pub layer1_mix_id: i64,
pub layer2_mix_id: i64,
pub layer3_mix_id: i64,
pub monitor_run_id: i64,
}
pub struct TestedGatewayStatus {
pub db_id: i64,
pub identity_key: String,
pub reliability: Option<u8>,
pub timestamp: i64,
pub gateway_id: i64,
pub layer1_mix_id: i64,
pub layer2_mix_id: i64,
pub layer3_mix_id: i64,
pub monitor_run_id: i64,
}