From 03c4895f2bc3a2aa9c0715ce655f2e209ec9bb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 16 Jan 2025 15:13:08 +0000 Subject: [PATCH] feature: introduce /load endpoint for self-reported quantised NymNode load (#5326) * feature: introduce /load endpoint for self-reported quantised NymNode load * return Load::Unknown for value of 0 because it means we misread some data * add additional filtering on 'en...' endpoints --- Cargo.lock | 79 +++++-- Cargo.toml | 3 +- common/statistics/src/report.rs | 2 +- nym-node/Cargo.toml | 1 + nym-node/nym-node-requests/Cargo.toml | 2 +- nym-node/nym-node-requests/src/api/client.rs | 10 +- nym-node/nym-node-requests/src/api/v1/mod.rs | 1 + .../src/api/v1/node_load/mod.rs | 4 + .../src/api/v1/node_load/models.rs | 79 +++++++ nym-node/nym-node-requests/src/lib.rs | 2 + nym-node/src/config/mod.rs | 10 + .../src/config/old_configs/old_config_v6.rs | 1 + nym-node/src/node/http/helpers/system_info.rs | 2 +- .../src/node/http/router/api/v1/health.rs | 4 +- nym-node/src/node/http/router/api/v1/load.rs | 33 +++ nym-node/src/node/http/router/api/v1/mod.rs | 2 + .../src/node/http/router/api/v1/openapi.rs | 3 + nym-node/src/node/http/state/load.rs | 209 ++++++++++++++++++ nym-node/src/node/http/state/mod.rs | 12 +- nym-node/src/node/mod.rs | 6 +- nym-wallet/Cargo.lock | 2 +- 21 files changed, 440 insertions(+), 27 deletions(-) create mode 100644 nym-node/nym-node-requests/src/api/v1/node_load/mod.rs create mode 100644 nym-node/nym-node-requests/src/api/v1/node_load/models.rs create mode 100644 nym-node/src/node/http/router/api/v1/load.rs create mode 100644 nym-node/src/node/http/state/load.rs diff --git a/Cargo.lock b/Cargo.lock index bcbf5df4be..ccf5903ba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "argon2" version = "0.5.3" @@ -1292,9 +1298,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cosmos-sdk-proto" @@ -6070,6 +6076,7 @@ name = "nym-node" version = "1.3.1" dependencies = [ "anyhow", + "arc-swap", "async-trait", "axum 0.7.7", "axum-extra", @@ -6153,7 +6160,6 @@ name = "nym-node-requests" version = "0.1.0" dependencies = [ "async-trait", - "base64 0.22.1", "celes", "humantime 2.1.0", "humantime-serde", @@ -6166,6 +6172,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "strum 0.26.3", "thiserror 1.0.69", "time", "tokio", @@ -9667,17 +9674,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.13" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ - "cfg-if", "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", "rayon", - "windows 0.52.0", + "windows 0.57.0", ] [[package]] @@ -11388,11 +11394,11 @@ dependencies = [ [[package]] name = "windows" -version = "0.52.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-core 0.52.0", + "windows-core 0.57.0", "windows-targets 0.52.6", ] @@ -11415,19 +11421,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", "windows-strings", "windows-targets 0.52.6", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -11439,6 +11468,17 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -11450,6 +11490,15 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -11465,7 +11514,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] diff --git a/Cargo.toml b/Cargo.toml index ebc238cdc4..dc20a58404 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,6 +194,7 @@ aes-gcm = "0.10.1" aes-gcm-siv = "0.11.1" aead = "0.5.2" anyhow = "1.0.95" +arc-swap = "1.7.1" argon2 = "0.5.0" async-trait = "0.1.85" axum-client-ip = "0.6.1" @@ -324,7 +325,7 @@ strum = "0.26" strum_macros = "0.26" subtle-encoding = "0.5" syn = "1" -sysinfo = "0.30.13" +sysinfo = "0.33.0" tap = "1.0.1" tar = "0.4.43" tempfile = "3.15" diff --git a/common/statistics/src/report.rs b/common/statistics/src/report.rs index 336de04f03..f01cf2be06 100644 --- a/common/statistics/src/report.rs +++ b/common/statistics/src/report.rs @@ -67,7 +67,7 @@ impl Default for ClientStatsReport { pub struct OsInformation { pub(crate) os_type: String, pub(crate) os_version: Option, - pub(crate) os_arch: Option, + pub(crate) os_arch: String, } impl OsInformation { diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 6cf93479f0..865c8b7679 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -16,6 +16,7 @@ license = "GPL-3.0" [dependencies] async-trait = { workspace = true } anyhow.workspace = true +arc-swap = { workspace = true } bip39 = { workspace = true, features = ["zeroize"] } bs58.workspace = true celes = { workspace = true } # country codes diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index 8b8849c480..9e88943930 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -11,13 +11,13 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -base64 = { workspace = true } celes = { workspace = true } # country codes humantime = { workspace = true } humantime-serde = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +strum = { workspace = true, features = ["derive"] } time = { workspace = true, features = ["serde", "formatting", "parsing"] } thiserror = { workspace = true } diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 068b128ef2..2d21e5477c 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -11,16 +11,16 @@ use async_trait::async_trait; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_http_api_client::{ApiClient, HttpClientError}; +use super::v1::gateway::models::Wireguard; +use super::v1::metrics::models::SessionStats; use crate::api::v1::authenticator::models::Authenticator; use crate::api::v1::health::models::NodeHealth; use crate::api::v1::ip_packet_router::models::IpPacketRouter; use crate::api::v1::network_requester::exit_policy::models::UsedExitPolicy; use crate::api::v1::network_requester::models::NetworkRequester; +use crate::api::v1::node_load::models::NodeLoad; pub use nym_http_api_client::Client; -use super::v1::gateway::models::Wireguard; -use super::v1::metrics::models::SessionStats; - pub type NymNodeApiClientError = HttpClientError; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -30,6 +30,10 @@ pub trait NymNodeApiClientExt: ApiClient { self.get_json_from(routes::api::v1::health_absolute()).await } + async fn get_node_load(&self) -> Result { + self.get_json_from(routes::api::v1::load_absolute()).await + } + async fn get_host_information(&self) -> Result { self.get_json_from(routes::api::v1::host_info_absolute()) .await diff --git a/nym-node/nym-node-requests/src/api/v1/mod.rs b/nym-node/nym-node-requests/src/api/v1/mod.rs index 9705e4e4eb..544633944f 100644 --- a/nym-node/nym-node-requests/src/api/v1/mod.rs +++ b/nym-node/nym-node-requests/src/api/v1/mod.rs @@ -9,3 +9,4 @@ pub mod metrics; pub mod mixnode; pub mod network_requester; pub mod node; +pub mod node_load; diff --git a/nym-node/nym-node-requests/src/api/v1/node_load/mod.rs b/nym-node/nym-node-requests/src/api/v1/node_load/mod.rs new file mode 100644 index 0000000000..13811deb67 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/node_load/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/node_load/models.rs b/nym-node/nym-node-requests/src/api/v1/node_load/models.rs new file mode 100644 index 0000000000..dfea7b57d7 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/node_load/models.rs @@ -0,0 +1,79 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumString}; + +#[derive( + Display, + Default, + Serialize, + Deserialize, + Clone, + Copy, + Debug, + EnumString, + PartialEq, + Eq, + JsonSchema, + PartialOrd, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub enum Load { + #[default] + Unknown, + + // order of the variants matter as we've derived `PartialOrd` on them + Negligible, // 0 - 0.1 + Low, // 0.1 - 0.3 + Medium, // 0.3 - 0.6 + High, // 0.6 - 0.8 + VeryHigh, // 0.8 - 0.95 + AtCapacity, // >= 0.95 +} + +impl Load { + // returns load of one tier higher + pub fn increment(&self) -> Self { + match self { + Self::Unknown => Self::Unknown, + Self::Negligible => Self::Low, + Self::Low => Self::Medium, + Self::Medium => Self::High, + Self::High => Self::VeryHigh, + Self::VeryHigh => Self::AtCapacity, + Self::AtCapacity => Self::AtCapacity, + } + } +} + +impl From for Load { + fn from(value: f64) -> Self { + if value == 0. || value.is_nan() || value.is_infinite() { + Self::Unknown + } else if value <= 0.1 { + Self::Negligible + } else if value <= 0.3 { + Self::Low + } else if value <= 0.6 { + Self::Medium + } else if value <= 0.8 { + Self::High + } else if value <= 0.95 { + Self::VeryHigh + } else { + Self::AtCapacity + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct NodeLoad { + pub total: Load, + pub machine: Load, + pub network: Load, +} diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index 1d7cd01846..aed1648515 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -35,6 +35,7 @@ pub mod routes { pub const NODE_DESCRIPTION: &str = "/description"; pub const AUXILIARY: &str = "/auxiliary-details"; pub const HEALTH: &str = "/health"; + pub const LOAD: &str = "/load"; pub const SWAGGER: &str = "/swagger"; pub const GATEWAY: &str = "/gateway"; @@ -46,6 +47,7 @@ pub mod routes { // define helper functions to get absolute routes absolute_route!(health_absolute, v1_absolute(), HEALTH); + absolute_route!(load_absolute, v1_absolute(), LOAD); absolute_route!(roles_absolute, v1_absolute(), ROLES); absolute_route!(build_info_absolute, v1_absolute(), BUILD_INFO); absolute_route!(host_info_absolute, v1_absolute(), HOST_INFO); diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 243f0b403f..ce7caf3700 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -470,6 +470,15 @@ pub struct Http { /// This option is superseded by `expose_system_hardware` /// default: true pub expose_crypto_hardware: bool, + + /// Specify the cache ttl of the node load. + /// default: 30s + #[serde(with = "humantime_serde")] + pub node_load_cache_ttl: Duration, +} + +impl Http { + pub const DEFAULT_NODE_LOAD_CACHE_TTL: Duration = Duration::from_secs(30); } impl Default for Http { @@ -481,6 +490,7 @@ impl Default for Http { expose_system_info: true, expose_system_hardware: true, expose_crypto_hardware: true, + node_load_cache_ttl: Self::DEFAULT_NODE_LOAD_CACHE_TTL, } } } diff --git a/nym-node/src/config/old_configs/old_config_v6.rs b/nym-node/src/config/old_configs/old_config_v6.rs index 7e9eba426f..0ca616e842 100644 --- a/nym-node/src/config/old_configs/old_config_v6.rs +++ b/nym-node/src/config/old_configs/old_config_v6.rs @@ -1027,6 +1027,7 @@ pub async fn try_upgrade_config_v6>( expose_system_info: old_cfg.http.expose_system_info, expose_system_hardware: old_cfg.http.expose_system_hardware, expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + ..Default::default() }, verloc: Verloc { bind_address: old_cfg.mixnode.verloc.bind_address, diff --git a/nym-node/src/node/http/helpers/system_info.rs b/nym-node/src/node/http/helpers/system_info.rs index 980c305ba9..c1d9df0a7b 100644 --- a/nym-node/src/node/http/helpers/system_info.rs +++ b/nym-node/src/node/http/helpers/system_info.rs @@ -72,7 +72,7 @@ pub(crate) fn get_system_info( system_name: System::name(), kernel_version: System::kernel_version(), os_version: System::os_version(), - cpu_arch: System::cpu_arch(), + cpu_arch: Some(System::cpu_arch()), hardware, } } diff --git a/nym-node/src/node/http/router/api/v1/health.rs b/nym-node/src/node/http/router/api/v1/health.rs index 510107039f..7aed916fe9 100644 --- a/nym-node/src/node/http/router/api/v1/health.rs +++ b/nym-node/src/node/http/router/api/v1/health.rs @@ -14,8 +14,8 @@ use nym_node_requests::api::v1::health::models::NodeHealth; tag = "Health", responses( (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml") + (NodeHealth = "application/json"), + (NodeHealth = "application/yaml") ), description = "the api is available and healthy") ), params(OutputParams) diff --git a/nym-node/src/node/http/router/api/v1/load.rs b/nym-node/src/node/http/router/api/v1/load.rs new file mode 100644 index 0000000000..de7d1874dd --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/load.rs @@ -0,0 +1,33 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_requests::api::v1::node_load::models::NodeLoad; + +/// Returns current relative load this node. +#[utoipa::path( + get, + path = "/load", + context_path = "/api/v1", + tag = "Node", + responses( + (status = 200, content( + (NodeLoad = "application/json"), + (NodeLoad = "application/yaml") + ), description = "current node load") + ), + params(OutputParams) +)] +pub(crate) async fn root_load( + Query(output): Query, + State(state): State, +) -> NodeLoadResponse { + let output = output.output.unwrap_or_default(); + let load = state.cached_load.current_load(); + + output.to_response(load) +} + +pub type NodeLoadResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/mod.rs b/nym-node/src/node/http/router/api/v1/mod.rs index 41ccef50cd..8e0c3d5bf7 100644 --- a/nym-node/src/node/http/router/api/v1/mod.rs +++ b/nym-node/src/node/http/router/api/v1/mod.rs @@ -10,6 +10,7 @@ pub mod authenticator; pub mod gateway; pub mod health; pub mod ip_packet_router; +pub mod load; pub mod metrics; pub mod mixnode; pub mod network_requester; @@ -30,6 +31,7 @@ pub struct Config { pub(super) fn routes(config: Config) -> Router { Router::new() .route(v1::HEALTH, get(health::root_health)) + .route(v1::LOAD, get(load::root_load)) .nest(v1::METRICS, metrics::routes(config.metrics)) .nest(v1::GATEWAY, gateway::routes(config.gateway)) .nest(v1::MIXNODE, mixnode::routes(config.mixnode)) diff --git a/nym-node/src/node/http/router/api/v1/openapi.rs b/nym-node/src/node/http/router/api/v1/openapi.rs index 6b209c450f..59de2ad6de 100644 --- a/nym-node/src/node/http/router/api/v1/openapi.rs +++ b/nym-node/src/node/http/router/api/v1/openapi.rs @@ -24,6 +24,7 @@ use utoipa_swagger_ui::SwaggerUi; api::v1::metrics::verloc::verloc_stats, api::v1::metrics::prometheus::prometheus_metrics, api::v1::health::root_health, + api::v1::load::root_load, api::v1::gateway::root::root_gateway, api::v1::gateway::client_interfaces::client_interfaces, api::v1::gateway::client_interfaces::mixnet_websockets, @@ -38,6 +39,8 @@ use utoipa_swagger_ui::SwaggerUi; nym_http_api_common::OutputParams, api_requests::v1::health::models::NodeHealth, api_requests::v1::health::models::NodeStatus, + api_requests::v1::node_load::models::NodeLoad, + api_requests::v1::node_load::models::Load, api_requests::v1::node::models::BinaryBuildInformationOwned, api_requests::v1::node::models::HostInformation, api_requests::v1::node::models::HostKeys, diff --git a/nym-node/src/node/http/state/load.rs b/nym-node/src/node/http/state/load.rs new file mode 100644 index 0000000000..96199426db --- /dev/null +++ b/nym-node/src/node/http/state/load.rs @@ -0,0 +1,209 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use arc_swap::ArcSwap; +use nym_node_requests::api::v1::node_load::models::{Load, NodeLoad}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use sysinfo::{CpuRefreshKind, MemoryRefreshKind, Networks, System}; +use time::OffsetDateTime; +use tracing::debug; + +#[derive(Clone)] +pub struct CachedNodeLoad { + ttl: Duration, + being_updated: Arc, + inner: Arc>, +} + +impl CachedNodeLoad { + pub(crate) fn new(ttl: Duration) -> Self { + CachedNodeLoad { + ttl, + being_updated: Arc::new(Default::default()), + inner: Arc::new(ArcSwap::new(Arc::new(CachedNodeLoadInner::initial()))), + } + } + + pub(crate) fn current_load(&self) -> NodeLoad { + let now = OffsetDateTime::now_utc(); + + let inner = self.inner.load(); + if inner.timestamp + self.ttl < now { + // new + let already_being_updated = self.being_updated.swap(true, Ordering::SeqCst); + if already_being_updated { + // use the 'stale' entry because it is already being updated by another thread + inner.load + } else { + self.update_cache() + } + } else { + inner.load + } + } + + fn update_cache(&self) -> NodeLoad { + let old = self.inner.load(); + let current_load = CachedNodeLoadInner::update(&old); + let load = current_load.load; + self.inner.store(Arc::new(current_load)); + self.being_updated.store(false, Ordering::SeqCst); + load + } +} + +pub struct CachedNodeLoadInner { + timestamp: OffsetDateTime, + last_transmitted: u64, + last_received: u64, + load: NodeLoad, +} + +struct RawUsage { + load_per_cpu: f64, + memory_usage: f64, + swap_usage: f64, + eth_transmitted: u64, + eth_received: u64, + total_swap: u64, +} + +impl RawUsage { + fn current() -> RawUsage { + let mut system = sysinfo::System::new(); + let networks = Networks::new_with_refreshed_list(); + + system.refresh_memory_specifics(MemoryRefreshKind::everything()); + system.refresh_cpu_specifics(CpuRefreshKind::nothing()); + + let average_load = System::load_average(); + let cpu_count = system.cpus().len(); + + let load_per_cpu = average_load.five / cpu_count as f64; + + let total_memory = system.total_memory(); + let used_memory = system.used_memory(); + + let memory_usage = used_memory as f64 / total_memory as f64; + + let total_swap = system.total_swap(); + let used_swap = system.free_swap(); + + let swap_usage = used_swap as f64 / total_swap as f64; + + let mut eth_transmitted = 0; + let mut eth_received = 0; + + // we're only interested in interfaces with 'eth' or 'en' prefix + // (that's a very weak assumption, but that's just first iteration of this endpoint) + for (interface, data) in networks.list() { + if interface.starts_with("eth") || interface.starts_with("en") { + eth_transmitted += data.total_transmitted(); + eth_received += data.total_received(); + } + } + + debug!(average_load = ?average_load, memory_usage=memory_usage, swap_usage = swap_usage, "current load"); + + RawUsage { + load_per_cpu, + memory_usage, + swap_usage, + eth_transmitted, + eth_received, + total_swap, + } + } +} + +impl CachedNodeLoadInner { + pub fn initial() -> CachedNodeLoadInner { + let timestamp = OffsetDateTime::now_utc(); + let raw_usage = RawUsage::current(); + + let mut base_load = Load::from(raw_usage.load_per_cpu); + let memory_load = Load::from(raw_usage.memory_usage); + + // if memory load is of higher tier, increment the base load by one level + // (i.e. for example from 'Low' to 'Medium') + if memory_load > base_load { + base_load = base_load.increment(); + } + + if raw_usage.total_swap > 1024 * 1024 * 1024 { + // same with swap + let swap_load = Load::from(raw_usage.swap_usage); + if swap_load > base_load { + base_load = base_load.increment(); + } + } + + CachedNodeLoadInner { + timestamp, + last_transmitted: raw_usage.eth_transmitted, + last_received: raw_usage.eth_received, + load: NodeLoad { + total: base_load, + machine: base_load, + network: Load::Unknown, + }, + } + } + + pub fn update(previous: &Self) -> CachedNodeLoadInner { + let timestamp = OffsetDateTime::now_utc(); + let raw_usage = RawUsage::current(); + + let time_delta = (timestamp - previous.timestamp).as_seconds_f64(); + let tx_delta = raw_usage.eth_transmitted - previous.last_transmitted; + let rx_delta = raw_usage.eth_received - previous.last_received; + + let tx_bs = tx_delta as f64 / time_delta; + let rx_bs = rx_delta as f64 / time_delta; + + // currently we consider value of 1Gbps to be maximum load + // in the future we should allow specifying custom sizes of network cards + // (Gbps = Bytes/s * 0.000000008) + let tx_gbps = tx_bs * 0.000000008; + let rx_gbps = rx_bs * 0.000000008; + let network_load = Load::from(tx_gbps.max(rx_gbps)); + + debug!(tx_gbps = tx_gbps, rx_gbps = rx_gbps, "network load"); + + let mut base_load = Load::from(raw_usage.load_per_cpu); + let memory_load = Load::from(raw_usage.memory_usage); + + // if memory load is of higher tier, increment the base load by one level + // (i.e. for example from 'Low' to 'Medium') + if memory_load > base_load { + base_load = base_load.increment(); + } + + if raw_usage.total_swap > 1024 * 1024 * 1024 { + // same with swap + let swap_load = Load::from(raw_usage.swap_usage); + if swap_load > base_load { + base_load = base_load.increment(); + } + } + + let total_load = if base_load > network_load { + base_load + } else { + base_load.increment() + }; + + CachedNodeLoadInner { + timestamp, + last_transmitted: raw_usage.eth_transmitted, + last_received: raw_usage.eth_received, + load: NodeLoad { + total: total_load, + machine: base_load, + network: network_load, + }, + } + } +} diff --git a/nym-node/src/node/http/state/mod.rs b/nym-node/src/node/http/state/mod.rs index 0e2fa98786..33dcf4ea9f 100644 --- a/nym-node/src/node/http/state/mod.rs +++ b/nym-node/src/node/http/state/mod.rs @@ -1,29 +1,39 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::http::state::load::CachedNodeLoad; use crate::node::http::state::metrics::MetricsAppState; use nym_node_metrics::NymNodeMetrics; use nym_verloc::measurements::SharedVerlocStats; +use std::time::Duration; use tokio::time::Instant; +pub mod load; pub mod metrics; #[derive(Clone)] pub struct AppState { pub(crate) startup_time: Instant, + pub(crate) cached_load: CachedNodeLoad, + pub(crate) metrics: MetricsAppState, } impl AppState { #[allow(clippy::new_without_default)] - pub fn new(metrics: NymNodeMetrics, verloc: SharedVerlocStats) -> Self { + pub fn new( + metrics: NymNodeMetrics, + verloc: SharedVerlocStats, + load_cache_ttl: Duration, + ) -> Self { AppState { // is it 100% accurate? // no. // does it have to be? // also no. startup_time: Instant::now(), + cached_load: CachedNodeLoad::new(load_cache_ttl), metrics: MetricsAppState { metrics, verloc }, } } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 8b8eb75212..81ac88487a 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -785,7 +785,11 @@ impl NymNode { config.api.v1_config.node.roles.ip_packet_router_enabled = true; } - let app_state = AppState::new(self.metrics.clone(), self.verloc_stats.clone()); + let app_state = AppState::new( + self.metrics.clone(), + self.verloc_stats.clone(), + self.config.http.node_load_cache_ttl, + ); Ok(NymNodeRouter::new(config, app_state) .build_server(&self.config.http.bind_address) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 1bdfa8c154..e7c1e7e72b 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3329,7 +3329,6 @@ name = "nym-node-requests" version = "0.1.0" dependencies = [ "async-trait", - "base64 0.22.1", "celes", "humantime 2.1.0", "humantime-serde", @@ -3341,6 +3340,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "strum 0.26.3", "thiserror 1.0.64", "time", "utoipa",