diff --git a/Cargo.lock b/Cargo.lock index 62bab445e4..3c05b1e72c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7768,7 +7768,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "4.6.1" +version = "4.6.2-rc" dependencies = [ "ammonia", "anyhow", diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 729404d8b6..71e0a4e039 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "4.6.1" +version = "4.6.2-rc" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs index d26db1d7df..bc0a74100e 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/testruns.rs @@ -58,6 +58,31 @@ async fn request_testrun( return Err(HttpError::no_testruns_available()); } + // 1. guard against other agents attempting to get testruns so that we would be able to check + // for ticketbook count without cross-interaction + let _guard = state.lock_testrun_assignment().await; + + // 2. check if we have enough ticketbooks of ALL types before assigning the run + match state + .ticketbook_manager_state() + .has_enough_ticketbooks() + .await + { + Err(err) => { + return Err(HttpError::internal_with_logging(format!( + "failed to check ticketbook storage: {err}" + ))); + } + Ok(false) => { + tracing::warn!("not enough ticketbooks available, rejecting testrun assignment"); + return Err(HttpError::internal_with_logging( + "not enough ticketbooks available to assign a testrun", + )); + } + Ok(true) => (), + } + + // 3. attempt to assign the testrun itself match db::queries::testruns::assign_oldest_testrun(&mut conn).await { Ok(res) => { let Some(assignment) = res else { @@ -71,6 +96,7 @@ async fn request_testrun( assignment.gateway_identity_key, ); + // 4. retrieve required ticketbooks (we should always have sufficient number as we hold an exclusive lock) let materials = state .ticketbook_manager_state() .attempt_assign_ticket_materials(assignment.testrun_id) diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index b630c17d40..c1a635a2d6 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -11,7 +11,7 @@ use semver::Version; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc, time::Duration}; use time::UtcDateTime; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, MutexGuard, RwLock}; use tracing::{error, instrument, trace, warn}; use utoipa::ToSchema; @@ -42,6 +42,7 @@ pub(crate) struct AppState { node_delegations: Arc>, bin_info: BinaryInfo, ticketbook_manager_state: TicketbookManagerState, + testrun_assignment_guard: Arc>, } impl AppState { @@ -66,6 +67,7 @@ impl AppState { node_delegations, bin_info: BinaryInfo::new(), ticketbook_manager_state, + testrun_assignment_guard: Arc::new(Default::default()), } } @@ -81,6 +83,10 @@ impl AppState { self.agent_key_list.contains(agent_pubkey) } + pub(crate) async fn lock_testrun_assignment(&self) -> MutexGuard<'_, ()> { + self.testrun_assignment_guard.lock().await + } + pub(crate) fn agent_max_count(&self) -> i64 { self.agent_max_count } diff --git a/nym-node-status-api/nym-node-status-api/src/ticketbook_manager/state.rs b/nym-node-status-api/nym-node-status-api/src/ticketbook_manager/state.rs index e1e507b272..d6939ad1d9 100644 --- a/nym-node-status-api/nym-node-status-api/src/ticketbook_manager/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/ticketbook_manager/state.rs @@ -53,6 +53,26 @@ impl TicketbookManagerState { } } + pub async fn has_enough_ticketbooks(&self) -> anyhow::Result { + let mut required = HashMap::new(); + for typ in &self.buffered_ticket_types { + let count = required.entry(*typ).or_insert(0); + *count += 1; + } + + for (typ, required_count) in required { + let available = self.storage.available_tickets_of_type(typ).await?; + if available < required_count { + warn!( + "not enough tickets of type {typ} available in storage, required {required_count}, available {available}" + ); + return Ok(false); + } + } + + Ok(true) + } + pub async fn attempt_assign_ticket_materials( &self, testrun_id: i32,