From 62346f16be7cd1fbf96571d8afff6f862895ca94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Thu, 20 Aug 2026 13:57:22 +0300 Subject: [PATCH 1/2] Add prune command to BandwidthController --- Cargo.lock | 1 + common/bandwidth-controller/src/controller.rs | 34 ++++ .../bandwidth-controller/src/in_flight/mod.rs | 4 + .../bandwidth-controller/src/requests/mod.rs | 3 + .../src/requests/sender.rs | 12 ++ common/bandwidth-controller/src/traits.rs | 3 + .../tests/managed_ticket_types.rs | 4 + common/bandwidth-fetcher/Cargo.toml | 3 + common/bandwidth-fetcher/src/credentials.rs | 173 ++++++++++++++++++ sdk/rust/nym-sdk-session/src/fetcher.rs | 8 + sdk/rust/nym-sdk-session/tests/support/mod.rs | 18 +- .../nym-sdk-session/tests/support/prune.rs | 48 +++++ 12 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 sdk/rust/nym-sdk-session/tests/support/prune.rs diff --git a/Cargo.lock b/Cargo.lock index 8197007e5c6..bf8f9e6d162 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6079,6 +6079,7 @@ dependencies = [ "nym-sqlx-pool-guard", "nym-validator-client", "rand 0.8.6", + "serde", "sqlx", "thiserror 2.0.19", "tokio", diff --git a/common/bandwidth-controller/src/controller.rs b/common/bandwidth-controller/src/controller.rs index bbb8b68d569..8f913d35125 100644 --- a/common/bandwidth-controller/src/controller.rs +++ b/common/bandwidth-controller/src/controller.rs @@ -228,6 +228,9 @@ impl BandwidthController { .await .map_err(BandwidthControllerError::credential_storage_error), ), + BandwidthControllerRequest::Prune(return_sender) => { + return_sender.send(self.handle_prune().await) + } BandwidthControllerRequest::GetAvailableTicketbooks(return_sender) => { return_sender.send(self.handle_get_available_ticketbooks().await) } @@ -296,6 +299,37 @@ impl BandwidthController { .map_err(BandwidthControllerError::credential_storage_error) } + // Removes expired ticketbooks from storage and expired ticketbooks that are still in pending. + async fn handle_prune(&mut self) -> Result<(), BandwidthControllerError> { + let expired_stored_ret = self + .storage + .cleanup_expired() + .await + .map_err(BandwidthControllerError::credential_storage_error); + + let expired_pending_ret = if let Some(fetcher) = &self.credential_fetcher { + Some( + fetcher + .prune() + .await + .map_err(BandwidthControllerError::fetcher_error), + ) + } else { + tracing::debug!("No credential fetcher set. No pruning possible there"); + None + }; + + // We propagate the errors after trying to prune as much expired data as possible + // Storage pruning failure is more important, as pending data eventually converts + // into stored data anyway, so we propagate potential storage pruning failure first. + expired_stored_ret?; + if let Some(expired_pending_ret) = expired_pending_ret { + expired_pending_ret?; + } + + Ok(()) + } + async fn handle_get_available_ticketbooks( &self, ) -> Result { diff --git a/common/bandwidth-controller/src/in_flight/mod.rs b/common/bandwidth-controller/src/in_flight/mod.rs index 615457759a9..cebe5524a29 100644 --- a/common/bandwidth-controller/src/in_flight/mod.rs +++ b/common/bandwidth-controller/src/in_flight/mod.rs @@ -278,6 +278,10 @@ mod tests { } } + async fn prune(&self) -> Result<(), CredentialFetcherError> { + Ok(()) + } + async fn cleanup(&self) {} async fn reset(self) -> Result<(), CredentialFetcherError> { diff --git a/common/bandwidth-controller/src/requests/mod.rs b/common/bandwidth-controller/src/requests/mod.rs index e3ebb4b9726..ed505b261d9 100644 --- a/common/bandwidth-controller/src/requests/mod.rs +++ b/common/bandwidth-controller/src/requests/mod.rs @@ -34,6 +34,9 @@ pub enum BandwidthControllerRequest { Reset(ReturnSender<()>), /// Removes the stored emergency (upgrade-mode) credentials only, leaving ticketbooks intact. ClearEmergencyCredentials(ReturnSender<()>), + /// Prunes expired ticketbooks from storage and stops in-flight retrieval of blinded ticketbook + /// shares whose validity has passed. + Prune(ReturnSender<()>), /// Returns the currently stored ticketbooks (also logs a stock summary). GetAvailableTicketbooks(ReturnSender), diff --git a/common/bandwidth-controller/src/requests/sender.rs b/common/bandwidth-controller/src/requests/sender.rs index 7a8b0c6bcc2..477febd0173 100644 --- a/common/bandwidth-controller/src/requests/sender.rs +++ b/common/bandwidth-controller/src/requests/sender.rs @@ -158,6 +158,18 @@ impl BandwidthControllerRequestSender { .map_err(|_| BandwidthControllerError::ChannelClosed)? } + /// Prunes expired ticketbooks from storage and stops in-flight retrieval of blinded ticketbook + /// shares whose validity has passed. + #[instrument(skip(self))] + pub async fn prune(&self) -> Result<(), BandwidthControllerError> { + let (tx, rx) = ReturnSender::new(); + self.command_tx + .send(BandwidthControllerRequest::Prune(tx)) + .map_err(|_| BandwidthControllerError::ChannelClosed)?; + rx.await + .map_err(|_| BandwidthControllerError::ChannelClosed)? + } + /// Returns the currently stored ticketbooks. #[instrument(skip(self))] pub async fn get_available_ticketbooks( diff --git a/common/bandwidth-controller/src/traits.rs b/common/bandwidth-controller/src/traits.rs index 3ea8664f24b..72dc5ac0080 100644 --- a/common/bandwidth-controller/src/traits.rs +++ b/common/bandwidth-controller/src/traits.rs @@ -98,6 +98,9 @@ pub trait CredentialFetcher: CredentialPublicDataFetcher + Send + Sync { ticketbook_type: TicketType, ) -> Result, CredentialFetcherError>; + /// Stops in-flight retrieval of blinded ticketbook shares whose validity has passed. + async fn prune(&self) -> Result<(), CredentialFetcherError>; + /// Persists any in-progress state (e.g. closing storage) before the fetcher is dropped, such /// that it can be resumed later. async fn cleanup(&self); diff --git a/common/bandwidth-controller/tests/managed_ticket_types.rs b/common/bandwidth-controller/tests/managed_ticket_types.rs index f089aaec510..3d314e951f2 100644 --- a/common/bandwidth-controller/tests/managed_ticket_types.rs +++ b/common/bandwidth-controller/tests/managed_ticket_types.rs @@ -68,6 +68,10 @@ impl CredentialFetcher for RecordingFetcher { Ok(Vec::new()) } + async fn prune(&self) -> Result<(), CredentialFetcherError> { + Ok(()) + } + async fn cleanup(&self) {} async fn reset(self) -> Result<(), CredentialFetcherError> { diff --git a/common/bandwidth-fetcher/Cargo.toml b/common/bandwidth-fetcher/Cargo.toml index e98786745f1..50d36799594 100644 --- a/common/bandwidth-fetcher/Cargo.toml +++ b/common/bandwidth-fetcher/Cargo.toml @@ -34,6 +34,9 @@ nym-validator-client = { workspace = true } sqlx = { workspace = true } nym-sqlx-pool-guard = { workspace = true } +[dev-dependencies] +serde = { workspace = true } + [build-dependencies] anyhow = { workspace = true } sqlx = { workspace = true, features = [ diff --git a/common/bandwidth-fetcher/src/credentials.rs b/common/bandwidth-fetcher/src/credentials.rs index 09a03991d0e..41f4bb380f7 100644 --- a/common/bandwidth-fetcher/src/credentials.rs +++ b/common/bandwidth-fetcher/src/credentials.rs @@ -185,6 +185,41 @@ where Ok(issuance_data.to_issued_ticketbook(wallet, epoch_id)) } + + async fn cancel_expired_ticketbooks(&self) -> Result<(), NyxdFetcherError> { + let mut pruned_pending_ticketbooks = 0; + + for expired_pending_ticketbook_id in self + .pending_storage + .get_pending_ticketbooks() + .await? + .iter() + .filter_map(|ticket_book| { + if ticket_book.pending_ticketbook.expired() { + Some(ticket_book.pending_id) + } else { + None + } + }) + { + if let Err(err) = self + .pending_storage + .remove_pending_ticketbook(expired_pending_ticketbook_id) + .await + { + tracing::warn!( + "Failed to remove expired ticketbook id {expired_pending_ticketbook_id} from pending storage: {err}" + ); + } else { + pruned_pending_ticketbooks += 1; + } + } + tracing::debug!( + "Cancelled {pruned_pending_ticketbooks} expired ticketbooks that were pending" + ); + + Ok(()) + } } impl NyxdCredentialFetcher @@ -359,6 +394,11 @@ where } } + async fn prune(&self) -> Result<(), CredentialFetcherError> { + self.cancel_expired_ticketbooks().await?; + Ok(()) + } + async fn cleanup(&self) { self.pending_storage.close().await; } @@ -446,6 +486,11 @@ pub(crate) mod recovery { Ok(recovered_ticketbooks) } + async fn prune(&self) -> Result<(), CredentialFetcherError> { + self.0.cancel_expired_ticketbooks().await?; + Ok(()) + } + async fn cleanup(&self) { self.0.pending_storage.close().await; } @@ -460,3 +505,131 @@ pub(crate) mod recovery { } } } + +#[cfg(test)] +#[allow(clippy::unreachable)] +mod tests { + use std::env::temp_dir; + + use serde::Deserialize; + + use nym_validator_client::nyxd::{ + Fee, + contract_traits::dkg_query_client::DkgQueryMsg, + cosmwasm_client::types::ExecuteResult, + error::NyxdError, + nym_ecash_contract_common::msg::{ExecuteMsg, QueryMsg}, + }; + use tokio::fs::remove_file; + + use super::*; + + struct MockPruneClient {} + + #[async_trait] + impl DkgQueryClient for MockPruneClient { + async fn query_dkg_contract( + &self, + _query: DkgQueryMsg, + ) -> std::result::Result + where + for<'a> T: Deserialize<'a>, + { + unreachable!("client not used in prune unit tests"); + } + } + + #[async_trait] + impl EcashSigningClient for MockPruneClient { + async fn execute_ecash_contract( + &self, + _fee: Option, + _msg: ExecuteMsg, + _memo: String, + _funds: Vec, + ) -> Result { + unreachable!("client not used in prune unit tests"); + } + } + + #[async_trait] + impl EcashQueryClient for MockPruneClient { + async fn query_ecash_contract(&self, _query: QueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + unreachable!("client not used in prune unit tests"); + } + } + + #[tokio::test] + async fn prune_expired() { + let mut db_path = temp_dir(); + db_path.push("prune_expired_unittest.db"); + let fetcher = NyxdCredentialFetcher::new( + Arc::new(MockPruneClient {}), + &db_path, + Zeroizing::new(Vec::new()), + ) + .await + .unwrap(); + + // pruning empty database doesn't fail + fetcher.prune().await.unwrap(); + + // insert late expiration ticketbook + let expired_ticketbook = IssuanceTicketBook::new_with_expiration( + 0, + [], + ed25519::PrivateKey::new(&mut OsRng), + TicketType::V1WireguardEntry, + Date::MIN, + ); + fetcher + .pending_storage + .insert_pending_ticketbook(&expired_ticketbook) + .await + .unwrap(); + + // check pruning emptied it + fetcher.prune().await.unwrap(); + assert_eq!( + fetcher + .pending_storage + .get_pending_ticketbooks() + .await + .unwrap() + .len(), + 0 + ); + + // insert late expiration ticketbook + let unexpired_ticketbook = IssuanceTicketBook::new_with_expiration( + 0, + [], + ed25519::PrivateKey::new(&mut OsRng), + TicketType::V1WireguardEntry, + Date::MAX, + ); + fetcher + .pending_storage + .insert_pending_ticketbook(&unexpired_ticketbook) + .await + .unwrap(); + + // check pruning doesn't affect it + fetcher.prune().await.unwrap(); + assert_ne!( + fetcher + .pending_storage + .get_pending_ticketbooks() + .await + .unwrap() + .len(), + 0 + ); + + fetcher.pending_storage.close().await; + remove_file(db_path).await.unwrap(); + } +} diff --git a/sdk/rust/nym-sdk-session/src/fetcher.rs b/sdk/rust/nym-sdk-session/src/fetcher.rs index a4ee9f9b04c..8b5abbca59e 100644 --- a/sdk/rust/nym-sdk-session/src/fetcher.rs +++ b/sdk/rust/nym-sdk-session/src/fetcher.rs @@ -141,6 +141,10 @@ impl CredentialFetcher for TimeoutFetcher { self.inner.fetch_ticketbooks(ticketbook_type).await } + async fn prune(&self) -> Result<(), CredentialFetcherError> { + self.inner.prune().await + } + async fn cleanup(&self) { self.inner.cleanup().await } @@ -229,6 +233,10 @@ mod tests { self.act().await } + async fn prune(&self) -> Result<(), CredentialFetcherError> { + Ok(()) + } + async fn cleanup(&self) {} async fn reset(self) -> Result<(), CredentialFetcherError> { diff --git a/sdk/rust/nym-sdk-session/tests/support/mod.rs b/sdk/rust/nym-sdk-session/tests/support/mod.rs index d905140ad52..d8e905c6a3c 100644 --- a/sdk/rust/nym-sdk-session/tests/support/mod.rs +++ b/sdk/rust/nym-sdk-session/tests/support/mod.rs @@ -13,6 +13,7 @@ pub mod fake_dkg; pub mod http_harness; +pub mod prune; use nym_compact_ecash::scheme::keygen::{KeyPairAuth, SecretKeyAuth}; use nym_compact_ecash::tests::helpers::{ @@ -77,11 +78,13 @@ impl TestEcash { } } - /// Fabricate a real threshold-signed ticketbook for `typ`. Distinct `seed`s - /// yield distinct books (fresh user keypair per book). - pub fn ticketbook(&self, typ: TicketType, seed: u64) -> IssuedTicketBook { + pub fn ticketbook_with_expiration( + &self, + typ: TicketType, + seed: u64, + expiration_date: Date, + ) -> IssuedTicketBook { let user = generate_keypair_user_from_seed(seed.to_be_bytes()); - let expiration_date = ecash_default_expiration_date(); let expiration_ts = expiration_date.ecash_unix_timestamp(); let (req, req_info) = withdrawal_request(user.secret_key(), expiration_ts, typ.encode()) @@ -118,6 +121,13 @@ impl TestEcash { ) } + /// Fabricate a real threshold-signed ticketbook for `typ`. Distinct `seed`s + /// yield distinct books (fresh user keypair per book). + pub fn ticketbook(&self, typ: TicketType, seed: u64) -> IssuedTicketBook { + let expiration_date = ecash_default_expiration_date(); + self.ticketbook_with_expiration(typ, seed, expiration_date) + } + /// The master verification key as the fetcher would serve it. pub fn epoch_verification_key(&self, epoch_id: u64) -> EpochVerificationKey { EpochVerificationKey { diff --git a/sdk/rust/nym-sdk-session/tests/support/prune.rs b/sdk/rust/nym-sdk-session/tests/support/prune.rs new file mode 100644 index 00000000000..6b2b9f3e7dd --- /dev/null +++ b/sdk/rust/nym-sdk-session/tests/support/prune.rs @@ -0,0 +1,48 @@ +use nym_bandwidth_controller::{BandwidthController, TicketType}; +use nym_credential_storage::{initialise_ephemeral_storage, storage::Storage}; +use nym_task::ShutdownToken; +use time::Date; + +use crate::support::TestEcash; + +/// Calling prune on the bandwidth controller frees up expected data in ticketbook storage, +/// as well as in pending storage. +#[tokio::test] +async fn prune_empty_storage() { + let ecash = TestEcash::new(); + let storage = initialise_ephemeral_storage(); + let controller = BandwidthController::new(storage.clone()); + let sender = controller.get_request_sender(); + + let shutdown = ShutdownToken::new(); + let run_handle = tokio::spawn({ + let shutdown = shutdown.clone(); + async move { controller.run(shutdown).await } + }); + + // pruning on empty storage doesn't error + sender.prune().await.unwrap(); + + // pruning old ticketbooks leaves the storage empty + let ticketbook = ecash.ticketbook_with_expiration( + TicketType::V1WireguardEntry, + 42, + Date::from_calendar_date(2000, 1.try_into().unwrap(), 1).unwrap(), + ); + storage.insert_issued_ticketbook(&ticketbook).await.unwrap(); + sender.prune().await.unwrap(); + assert_eq!(storage.get_ticketbooks_info().await.unwrap().len(), 0); + + // pruning non-expired ticketbooks doesn't touch them + let ticketbook = ecash.ticketbook_with_expiration( + TicketType::V1WireguardEntry, + 42, + Date::from_calendar_date(2100, 1.try_into().unwrap(), 1).unwrap(), + ); + storage.insert_issued_ticketbook(&ticketbook).await.unwrap(); + sender.prune().await.unwrap(); + assert_ne!(storage.get_ticketbooks_info().await.unwrap().len(), 0); + + shutdown.cancel(); + let _ = run_handle.await; +} From 680b04c15b3055520d708f05d99a18cb3d01607c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Thu, 27 Aug 2026 13:16:46 +0300 Subject: [PATCH 2/2] Keep pruning internal to BandwidthController --- common/bandwidth-controller/src/config.rs | 1 + common/bandwidth-controller/src/controller.rs | 50 ++++++------------- .../bandwidth-controller/src/requests/mod.rs | 3 -- .../src/requests/sender.rs | 12 ----- .../nym-sdk-session/tests/support/prune.rs | 17 ++----- 5 files changed, 20 insertions(+), 63 deletions(-) diff --git a/common/bandwidth-controller/src/config.rs b/common/bandwidth-controller/src/config.rs index fdc0878d722..97a59243e74 100644 --- a/common/bandwidth-controller/src/config.rs +++ b/common/bandwidth-controller/src/config.rs @@ -10,6 +10,7 @@ use crate::ticketbooks::AvailableTicketbooks; #[derive(Debug, Clone)] pub struct BandwidthControllerConfig { // How often the controller proactively checks whether any ticket type needs restocking. + // It is also pruning any expired tickets from storage before restocking. pub topup_interval: Duration, // Threshold to determine if a ticket is soon expired pub soon_expiry_threshold: Duration, diff --git a/common/bandwidth-controller/src/controller.rs b/common/bandwidth-controller/src/controller.rs index 8f913d35125..0ad90695f21 100644 --- a/common/bandwidth-controller/src/controller.rs +++ b/common/bandwidth-controller/src/controller.rs @@ -155,6 +155,7 @@ impl BandwidthController { break; } _ = topup_interval.tick() => { + self.prune_expired().await; let _ = self.print_info().await; self.check_and_restock(self.config.managed_ticket_types.clone()).await; } @@ -228,9 +229,6 @@ impl BandwidthController { .await .map_err(BandwidthControllerError::credential_storage_error), ), - BandwidthControllerRequest::Prune(return_sender) => { - return_sender.send(self.handle_prune().await) - } BandwidthControllerRequest::GetAvailableTicketbooks(return_sender) => { return_sender.send(self.handle_get_available_ticketbooks().await) } @@ -299,37 +297,6 @@ impl BandwidthController { .map_err(BandwidthControllerError::credential_storage_error) } - // Removes expired ticketbooks from storage and expired ticketbooks that are still in pending. - async fn handle_prune(&mut self) -> Result<(), BandwidthControllerError> { - let expired_stored_ret = self - .storage - .cleanup_expired() - .await - .map_err(BandwidthControllerError::credential_storage_error); - - let expired_pending_ret = if let Some(fetcher) = &self.credential_fetcher { - Some( - fetcher - .prune() - .await - .map_err(BandwidthControllerError::fetcher_error), - ) - } else { - tracing::debug!("No credential fetcher set. No pruning possible there"); - None - }; - - // We propagate the errors after trying to prune as much expired data as possible - // Storage pruning failure is more important, as pending data eventually converts - // into stored data anyway, so we propagate potential storage pruning failure first. - expired_stored_ret?; - if let Some(expired_pending_ret) = expired_pending_ret { - expired_pending_ret?; - } - - Ok(()) - } - async fn handle_get_available_ticketbooks( &self, ) -> Result { @@ -530,6 +497,21 @@ impl BandwidthController { self.prefetch_global_data().await; } + // Removes expired ticketbooks from storage and expired ticketbooks that are still in pending. + pub async fn prune_expired(&self) { + if let Err(err) = self.storage.cleanup_expired().await { + tracing::warn!("Could not cleanup expired ticketbooks: {err}"); + } else { + tracing::debug!("Finished pruning of expired ticketbooks"); + } + + if let Some(fetcher) = &self.credential_fetcher { + if let Err(err) = fetcher.prune().await { + tracing::warn!("Could not prune fetcher expired ticketbooks: {err}"); + } + } + } + /// Spawns a background fetch for `ticket_type` unless one is already in flight for it. /// Non-blocking: the result is drained later in the `run` loop via `on_fetch_complete`. fn ensure_stocked(&mut self, ticket_type: TicketType) { diff --git a/common/bandwidth-controller/src/requests/mod.rs b/common/bandwidth-controller/src/requests/mod.rs index ed505b261d9..e3ebb4b9726 100644 --- a/common/bandwidth-controller/src/requests/mod.rs +++ b/common/bandwidth-controller/src/requests/mod.rs @@ -34,9 +34,6 @@ pub enum BandwidthControllerRequest { Reset(ReturnSender<()>), /// Removes the stored emergency (upgrade-mode) credentials only, leaving ticketbooks intact. ClearEmergencyCredentials(ReturnSender<()>), - /// Prunes expired ticketbooks from storage and stops in-flight retrieval of blinded ticketbook - /// shares whose validity has passed. - Prune(ReturnSender<()>), /// Returns the currently stored ticketbooks (also logs a stock summary). GetAvailableTicketbooks(ReturnSender), diff --git a/common/bandwidth-controller/src/requests/sender.rs b/common/bandwidth-controller/src/requests/sender.rs index 477febd0173..7a8b0c6bcc2 100644 --- a/common/bandwidth-controller/src/requests/sender.rs +++ b/common/bandwidth-controller/src/requests/sender.rs @@ -158,18 +158,6 @@ impl BandwidthControllerRequestSender { .map_err(|_| BandwidthControllerError::ChannelClosed)? } - /// Prunes expired ticketbooks from storage and stops in-flight retrieval of blinded ticketbook - /// shares whose validity has passed. - #[instrument(skip(self))] - pub async fn prune(&self) -> Result<(), BandwidthControllerError> { - let (tx, rx) = ReturnSender::new(); - self.command_tx - .send(BandwidthControllerRequest::Prune(tx)) - .map_err(|_| BandwidthControllerError::ChannelClosed)?; - rx.await - .map_err(|_| BandwidthControllerError::ChannelClosed)? - } - /// Returns the currently stored ticketbooks. #[instrument(skip(self))] pub async fn get_available_ticketbooks( diff --git a/sdk/rust/nym-sdk-session/tests/support/prune.rs b/sdk/rust/nym-sdk-session/tests/support/prune.rs index 6b2b9f3e7dd..994689d5306 100644 --- a/sdk/rust/nym-sdk-session/tests/support/prune.rs +++ b/sdk/rust/nym-sdk-session/tests/support/prune.rs @@ -1,6 +1,5 @@ use nym_bandwidth_controller::{BandwidthController, TicketType}; use nym_credential_storage::{initialise_ephemeral_storage, storage::Storage}; -use nym_task::ShutdownToken; use time::Date; use crate::support::TestEcash; @@ -12,16 +11,9 @@ async fn prune_empty_storage() { let ecash = TestEcash::new(); let storage = initialise_ephemeral_storage(); let controller = BandwidthController::new(storage.clone()); - let sender = controller.get_request_sender(); - - let shutdown = ShutdownToken::new(); - let run_handle = tokio::spawn({ - let shutdown = shutdown.clone(); - async move { controller.run(shutdown).await } - }); // pruning on empty storage doesn't error - sender.prune().await.unwrap(); + controller.prune_expired().await; // pruning old ticketbooks leaves the storage empty let ticketbook = ecash.ticketbook_with_expiration( @@ -30,7 +22,7 @@ async fn prune_empty_storage() { Date::from_calendar_date(2000, 1.try_into().unwrap(), 1).unwrap(), ); storage.insert_issued_ticketbook(&ticketbook).await.unwrap(); - sender.prune().await.unwrap(); + controller.prune_expired().await; assert_eq!(storage.get_ticketbooks_info().await.unwrap().len(), 0); // pruning non-expired ticketbooks doesn't touch them @@ -40,9 +32,6 @@ async fn prune_empty_storage() { Date::from_calendar_date(2100, 1.try_into().unwrap(), 1).unwrap(), ); storage.insert_issued_ticketbook(&ticketbook).await.unwrap(); - sender.prune().await.unwrap(); + controller.prune_expired().await; assert_ne!(storage.get_ticketbooks_info().await.unwrap().len(), 0); - - shutdown.cancel(); - let _ = run_handle.await; }